Refactor details grid component

This commit is contained in:
Oliver Walters 2026-06-30 13:48:06 +00:00
parent 9cab4c27f8
commit 8ca997f73b
18 changed files with 141 additions and 95 deletions

View File

@ -529,17 +529,19 @@ export function DetailsTableField({
);
}
export interface DetailsTableProps {
item: any;
fields: DetailsField[];
title?: string;
showIcons?: boolean;
}
export function DetailsTable({
item,
fields,
title,
showIcons = true
}: Readonly<{
item: any;
fields: DetailsField[];
title?: string;
showIcons?: boolean;
}>) {
}: Readonly<DetailsTableProps>) {
const visibleFields = useMemo(() => {
return fields.filter((field) => !field.hidden);
}, [fields]);

View File

@ -1,7 +1,20 @@
import { Paper, SimpleGrid } from '@mantine/core';
import type React from 'react';
import { useMemo } from 'react';
import { DetailsTable, type DetailsTableProps } from './Details';
export type { DetailsTableProps };
export function ItemDetailsGrid({
children,
tables
}: React.PropsWithChildren<{ tables?: DetailsTableProps[] }>) {
const visibleTables = useMemo(
() => tables?.filter((t) => t.fields.some((f) => !f.hidden)) ?? [],
[tables]
);
export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
return (
<Paper p='xs'>
<SimpleGrid
@ -10,7 +23,10 @@ export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
spacing='xs'
verticalSpacing='xs'
>
{props.children}
{children}
{visibleTables.map((props, index) => (
<DetailsTable key={index} {...props} />
))}
</SimpleGrid>
</Paper>
);

View File

@ -5,15 +5,15 @@ import { t } from '@lingui/core/macro';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useApi } from '../../contexts/ApiContext';
import { type DetailsField, DetailsTable } from './Details';
import type { DetailsField, DetailsTableProps } from './Details';
export function ParameterDetailsGrid({
export function useParameterDetailsGrid({
model_type,
model_id
}: Readonly<{
model_type: ModelType;
model_id: number | string | undefined;
}>) {
}>): DetailsTableProps {
const api = useApi();
const { data: parameters = [] } = useQuery({
@ -45,14 +45,10 @@ export function ParameterDetailsGrid({
return { fields, item };
}, [parameters]);
if (fields.length === 0) return null;
return (
<DetailsTable
title={t`Parameters`}
fields={fields}
item={item}
showIcons={false}
/>
);
return {
title: t`Parameters`,
fields: fields,
item: item,
showIcons: false
};
}

View File

@ -1,6 +1,7 @@
import { ModelType } from '@lib/enums/ModelType';
import type { ReactNode } from 'react';
import { PartPreviewComponent } from './models/PartPreview';
import { PurchaseOrderPreviewComponent } from './models/PurchaseOrderPreview';
import { StockPreviewComponent } from './models/StockPreview';
export interface PreviewType {
@ -28,6 +29,8 @@ export function getPreviewComponentForModel({
return PartPreviewComponent({ instance, modelId });
case ModelType.stockitem:
return StockPreviewComponent({ instance, modelId });
case ModelType.purchaseorder:
return PurchaseOrderPreviewComponent({ instance, modelId });
default:
return null;
}

View File

@ -439,7 +439,13 @@ export default function BuildDetail() {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: data },
{ fields: bl, item: data },
{ fields: br, item: data }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -454,9 +460,6 @@ export default function BuildDetail() {
</Grid>
<TagsList tags={build.tags} />
</Stack>
<DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={data} />
</ItemDetailsGrid>
);
}, [build, instanceQuery, partRequirements, partRequirementsQuery]);

View File

@ -155,7 +155,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid tables={[{ item: company, fields: tr }]}>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -176,7 +176,6 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
</Grid>
<TagsList tags={company.tags} />
</Stack>
<DetailsTable item={company} fields={tr} />
</ItemDetailsGrid>
);
}, [company, instanceQuery]);

View File

@ -134,7 +134,9 @@ export default function ManufacturerPartDetail() {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[{ title: t`Manufacturer Details`, fields: tr, item: data }]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -152,7 +154,6 @@ export default function ManufacturerPartDetail() {
</Grid>
<TagsList tags={manufacturerPart.tags} />
</Stack>
<DetailsTable title={t`Manufacturer Details`} fields={tr} item={data} />
</ItemDetailsGrid>
);
}, [manufacturerPart, instanceQuery]);

View File

@ -222,7 +222,13 @@ export default function SupplierPartDetail() {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ title: t`Supplier`, fields: bl, item: data },
{ title: t`Packaging`, fields: br, item: data },
{ title: t`Availability`, fields: tr, item: data }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -240,9 +246,6 @@ export default function SupplierPartDetail() {
</Grid>
<TagsList tags={supplierPart.tags} />
</Stack>
<DetailsTable title={t`Supplier`} fields={bl} item={data} />
<DetailsTable title={t`Packaging`} fields={br} item={data} />
<DetailsTable title={t`Availability`} fields={tr} item={data} />
</ItemDetailsGrid>
);
}, [supplierPart, instanceQuery.isFetching]);

View File

@ -7,10 +7,7 @@ import { Paper, Skeleton, Stack } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import {
type DetailsField,
DetailsTable
} from '../../components/details/Details';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {} from '../../components/items/ActionDropdown';
import { RoleTable, type RuleSet } from '../../components/items/RoleTable';
@ -48,8 +45,9 @@ export default function GroupDetail() {
];
return (
<ItemDetailsGrid>
<DetailsTable fields={tl} item={instance} title={t`Group Details`} />
<ItemDetailsGrid
tables={[{ fields: tl, item: instance, title: t`Group Details` }]}
>
<Paper p='xs' withBorder>
<Stack gap='xs'>
<StylishText size='lg'>{t`Group Roles`}</StylishText>

View File

@ -6,10 +6,7 @@ import { Badge, Group, Skeleton, Stack } from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import { type ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import {
type DetailsField,
DetailsTable
} from '../../components/details/Details';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {} from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
@ -171,13 +168,15 @@ export default function UserDetail() {
instance.location;
return (
<ItemDetailsGrid>
<DetailsTable fields={tl} item={instance} title={t`User Information`} />
<DetailsTable fields={tr} item={instance} title={t`User Permissions`} />
{hasProfile && settings.isSet('DISPLAY_PROFILE_INFO') && (
<DetailsTable fields={br} item={instance} title={t`User Profile`} />
)}
</ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tl, item: instance, title: t`User Information` },
{ fields: tr, item: instance, title: t`User Permissions` },
...(hasProfile && settings.isSet('DISPLAY_PROFILE_INFO')
? [{ fields: br, item: instance, title: t`User Profile` }]
: [])
]}
/>
);
}, [instance, userGroups, instanceQuery]);

View File

@ -20,10 +20,7 @@ import type { PanelType } from '@lib/types/Panel';
import { useLocalStorage } from '@mantine/hooks';
import AdminButton from '../../components/buttons/AdminButton';
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
import {
type DetailsField,
DetailsTable
} from '../../components/details/Details';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
DeleteItemAction,
@ -169,10 +166,16 @@ export default function CategoryDetail() {
];
return (
<ItemDetailsGrid>
{id && category?.pk && <DetailsTable item={category} fields={left} />}
{id && category?.pk && <DetailsTable item={category} fields={right} />}
</ItemDetailsGrid>
<ItemDetailsGrid
tables={
id && category?.pk
? [
{ item: category, fields: left },
{ item: category, fields: right }
]
: []
}
/>
);
}, [category, instanceQuery]);

View File

@ -14,7 +14,7 @@ import {
} from '../../components/details/Details';
import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { ParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
import { useParameterDetailsGrid } from '../../components/details/ParameterDetailsGrid';
import { formatPriceRange } from '../../defaults/formatters';
import { useInstance } from '../../hooks/UseInstance';
@ -322,10 +322,22 @@ export function PartDetailsPanel({
return fields;
}, [instance, data.latest_serial_number]);
const parametersTable = useParameterDetailsGrid({
model_type: ModelType.part,
model_id: instance?.pk
});
if (!instance?.pk) return <Skeleton />;
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: data },
{ fields: bl, item: data },
{ fields: br, item: data },
parametersTable
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -353,13 +365,6 @@ export function PartDetailsPanel({
<TagsList tags={instance.tags} />
{additionalContent}
</Stack>
<DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={data} />
<ParameterDetailsGrid
model_type={ModelType.part}
model_id={instance.pk}
/>
</ItemDetailsGrid>
);
}

View File

@ -297,7 +297,13 @@ export default function ReturnOrderDetail() {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: order },
{ fields: bl, item: order },
{ fields: br, item: order }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -312,9 +318,6 @@ export default function ReturnOrderDetail() {
</Grid>
<TagsList tags={order.tags} />
</Stack>
<DetailsTable fields={tr} item={order} />
<DetailsTable fields={bl} item={order} />
<DetailsTable fields={br} item={order} />
</ItemDetailsGrid>
);
}, [order, instanceQuery]);

View File

@ -288,7 +288,13 @@ export default function SalesOrderDetail() {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: order },
{ fields: bl, item: order },
{ fields: br, item: order }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -303,9 +309,6 @@ export default function SalesOrderDetail() {
</Grid>
<TagsList tags={order.tags} />
</Stack>
<DetailsTable fields={tr} item={order} />
<DetailsTable fields={bl} item={order} />
<DetailsTable fields={br} item={order} />
</ItemDetailsGrid>
);
}, [order, orderCurrency, instanceQuery]);

View File

@ -222,7 +222,13 @@ export default function SalesOrderShipmentDetail() {
return (
<>
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: data },
{ fields: bl, item: data },
{ fields: br, item: data }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -243,9 +249,6 @@ export default function SalesOrderShipmentDetail() {
</Grid>
<TagsList tags={shipment.tags} />
</Stack>
<DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={data} />
</ItemDetailsGrid>
</>
);

View File

@ -24,10 +24,7 @@ import { useBarcodeScanDialog } from '../../components/barcodes/BarcodeScanDialo
import AdminButton from '../../components/buttons/AdminButton';
import { PrintingActions } from '../../components/buttons/PrintingActions';
import OrderCalendar from '../../components/calendar/OrderCalendar';
import {
type DetailsField,
DetailsTable
} from '../../components/details/Details';
import type { DetailsField } from '../../components/details/Details';
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import {
BarcodeActionDropdown,
@ -185,10 +182,16 @@ export default function Stock() {
];
return (
<ItemDetailsGrid>
{id && location?.pk && <DetailsTable item={location} fields={left} />}
{id && location?.pk && <DetailsTable item={location} fields={right} />}
</ItemDetailsGrid>
<ItemDetailsGrid
tables={
id && location?.pk
? [
{ item: location, fields: left },
{ item: location, fields: right }
]
: []
}
/>
);
}, [location, instanceQuery]);

View File

@ -377,7 +377,13 @@ export function StockDetailsPanel({
return (
<>
{showSerialNav && findBySerialNumber.modal}
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: data },
{ fields: bl, item: data },
{ fields: br, item: data }
]}
>
<Stack gap='xs'>
<Grid grow>
<DetailsImage
@ -401,9 +407,6 @@ export function StockDetailsPanel({
</Grid>
<TagsList tags={instance?.tags} />
</Stack>
<DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={data} />
</ItemDetailsGrid>
</>
);

View File

@ -234,7 +234,13 @@ export default function TransferOrderDetail() {
];
return (
<ItemDetailsGrid>
<ItemDetailsGrid
tables={[
{ fields: tr, item: order },
{ fields: bl, item: order },
{ fields: br, item: order }
]}
>
<Stack gap='xs'>
<Grid grow>
{/* TODO: what image do we show for a Transfer Order? */}
@ -250,9 +256,6 @@ export default function TransferOrderDetail() {
</Grid>
<TagsList tags={order.tags} />
</Stack>
<DetailsTable fields={tr} item={order} />
<DetailsTable fields={bl} item={order} />
<DetailsTable fields={br} item={order} />
</ItemDetailsGrid>
);
}, [order, instanceQuery]);