diff --git a/src/frontend/src/components/details/ItemDetails.tsx b/src/frontend/src/components/details/ItemDetails.tsx
index d674cc7bcf..8252afc3a2 100644
--- a/src/frontend/src/components/details/ItemDetails.tsx
+++ b/src/frontend/src/components/details/ItemDetails.tsx
@@ -1,4 +1,5 @@
import { Grid, Group, Paper, SimpleGrid } from '@mantine/core';
+import React from 'react';
import { UserRoles } from '../../enums/Roles';
import { DetailsField, DetailsTable } from '../../tables/Details';
@@ -23,6 +24,16 @@ export type DetailsImageType = {
imageActions: DetailImageButtonProps;
};
+export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
+ return (
+
+
+ {props.children}
+
+
+ );
+}
+
/**
* Render a Details panel of the given model
* @param params Object with the data of the model to render
diff --git a/src/frontend/src/functions/urls.tsx b/src/frontend/src/functions/urls.tsx
index 5920439c89..8125f7b02d 100644
--- a/src/frontend/src/functions/urls.tsx
+++ b/src/frontend/src/functions/urls.tsx
@@ -7,7 +7,7 @@ import { ModelType } from '../enums/ModelType';
export function getDetailUrl(model: ModelType, pk: number | string): string {
const modelInfo = ModelInformationDict[model];
- if (modelInfo && modelInfo.url_detail) {
+ if (!!pk && modelInfo && modelInfo.url_detail) {
return modelInfo.url_detail.replace(':pk', pk.toString());
}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 7d4ce146d9..9d448a1902 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -31,7 +31,8 @@ import { api } from '../../App';
import {
DetailsImageType,
ItemDetailFields,
- ItemDetails
+ ItemDetails,
+ ItemDetailsGrid
} from '../../components/details/ItemDetails';
import {
ActionDropdown,
@@ -56,7 +57,7 @@ import { useEditApiFormModal } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
-import { DetailsField } from '../../tables/Details';
+import { DetailsField, DetailsTable } from '../../tables/Details';
import { BomTable } from '../../tables/bom/BomTable';
import { UsedInTable } from '../../tables/bom/UsedInTable';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
@@ -441,6 +442,192 @@ export default function PartDetail() {
return fields;
};
+ const detailsPanel = useMemo(() => {
+ if (instanceQuery.isFetching) {
+ return ;
+ }
+
+ // Construct the details tables
+ let tl: DetailsField[] = [
+ {
+ type: 'text',
+ name: 'description',
+ label: t`Description`,
+ copy: true
+ },
+ {
+ type: 'link',
+ name: 'variant_of',
+ label: t`Variant of`,
+ model: ModelType.part,
+ hidden: !part.variant_of
+ }
+ ];
+
+ let tr: DetailsField[] = [
+ {
+ type: 'string',
+ name: 'unallocated_stock',
+ unit: true,
+ label: t`Available Stock`
+ },
+ {
+ type: 'string',
+ name: 'total_in_stock',
+ unit: true,
+ label: t`In Stock`
+ },
+ {
+ type: 'string',
+ name: 'minimum_stock',
+ unit: true,
+ label: t`Minimum Stock`,
+ hidden: part.minimum_stock <= 0
+ },
+ {
+ type: 'string',
+ name: 'ordering',
+ label: t`On order`,
+ unit: true,
+ hidden: part.ordering <= 0
+ },
+ {
+ type: 'progressbar',
+ name: 'allocated_to_build_orders',
+ total: part.required_for_build_orders,
+ progress: part.allocated_to_build_orders,
+ label: t`Allocated to Build Orders`,
+ hidden:
+ !part.assembly ||
+ (part.allocated_to_build_orders <= 0 &&
+ part.required_for_build_orders <= 0)
+ },
+ {
+ type: 'progressbar',
+ name: 'allocated_to_sales_orders',
+ total: part.required_for_sales_orders,
+ progress: part.allocated_to_sales_orders,
+ label: t`Allocated to Sales Orders`,
+ hidden:
+ !part.salable ||
+ (part.allocated_to_sales_orders <= 0 &&
+ part.required_for_sales_orders <= 0)
+ },
+ {
+ type: 'string',
+ name: 'can_build',
+ unit: true,
+ label: t`Can Build`,
+ hidden: !part.assembly
+ },
+ {
+ type: 'string',
+ name: 'building',
+ unit: true,
+ label: t`Building`,
+ hidden: !part.assembly
+ }
+ ];
+
+ let bl: DetailsField[] = [
+ {
+ type: 'link',
+ name: 'category',
+ label: t`Category`,
+ model: ModelType.partcategory
+ },
+ {
+ type: 'string',
+ name: 'IPN',
+ label: t`IPN`,
+ copy: true,
+ hidden: !part.IPN
+ },
+ {
+ type: 'string',
+ name: 'revision',
+ label: t`Revision`,
+ copy: true,
+ hidden: !part.revision
+ },
+ {
+ type: 'string',
+ name: 'units',
+ label: t`Units`,
+ hidden: !part.units
+ },
+ {
+ type: 'string',
+ name: 'keywords',
+ label: t`Keywords`,
+ copy: true,
+ hidden: !part.keywords
+ },
+ {
+ type: 'string',
+ name: 'responsible',
+ label: t`Responsible`,
+ badge: 'owner',
+ hidden: !part.responsible
+ }
+ ];
+
+ let br: DetailsField[] = [
+ {
+ type: 'string',
+ name: 'creation_date',
+ label: t`Creation Date`
+ },
+ {
+ type: 'string',
+ name: 'creation_user',
+ badge: 'user'
+ },
+ {
+ type: 'link',
+ name: 'default_location',
+ label: t`Default Location`,
+ model: ModelType.stocklocation,
+ hidden: !part.default_location
+ },
+ {
+ type: 'link',
+ name: 'default_supplier',
+ label: t`Default Supplier`,
+ model: ModelType.supplierpart,
+ hidden: !part.default_supplier
+ },
+ {
+ type: 'link',
+ name: 'link',
+ label: t`Link`,
+ external: true,
+ copy: true,
+ hidden: !part.link
+ }
+ ];
+
+ return (
+
+
+
+
+
+
+ );
+
+ // content: !instanceQuery.isFetching && (
+ //
+ // )
+ }, [part, instanceQuery]);
+
// Part data panels (recalculate when part data changes)
const partPanels: PanelType[] = useMemo(() => {
return [
@@ -448,16 +635,7 @@ export default function PartDetail() {
name: 'details',
label: t`Details`,
icon: ,
- content: !instanceQuery.isFetching && (
-
- )
+ content: detailsPanel
},
{
name: 'parameters',
diff --git a/src/frontend/src/tables/Details.tsx b/src/frontend/src/tables/Details.tsx
index b062635f5f..c4302f87ff 100644
--- a/src/frontend/src/tables/Details.tsx
+++ b/src/frontend/src/tables/Details.tsx
@@ -37,6 +37,7 @@ export type PartIconsType = {
export type DetailsField =
| {
+ hidden?: boolean;
name: string;
label?: string;
badge?: BadgeType;
@@ -299,7 +300,7 @@ function TableAnchorValue(props: FieldProps) {
queryFn: async () => {
const modelDef = getModelInfo(props.field_data.model);
- if (!modelDef.api_endpoint) {
+ if (!modelDef?.api_endpoint) {
return {};
}
@@ -366,12 +367,12 @@ function CopyField({ value }: { value: string }) {
);
}
-function TableField({
+function xTableField({
field_data,
field_value,
unit = null
}: {
- field_data: DetailsField[];
+ field_data: DetailsField;
field_value: FieldValueType[];
unit?: string | null;
}) {
@@ -397,8 +398,8 @@ function TableField({
justifyContent: 'flex-start'
}}
>
-
- {field_data[0].label}
+
+ {field_data.label}
@@ -428,52 +429,64 @@ function TableField({
);
}
-export function DetailsTable({
+export function DetailsTableField({
item,
- fields,
- partIcons = false
+ field
}: {
item: any;
- fields: DetailsField[][];
- partIcons?: boolean;
+ field: DetailsField;
+}) {
+ function getFieldType(type: string) {
+ switch (type) {
+ case 'text':
+ case 'string':
+ return TableStringValue;
+ case 'link':
+ return TableAnchorValue;
+ case 'progressbar':
+ return ProgressBarValue;
+ default:
+ return TableStringValue;
+ }
+ }
+
+ const FieldType: any = getFieldType(field.type);
+
+ return (
+
+
+
+ {field.label}
+ |
+
+
+ |
+ {field.copy && } |
+
+ );
+}
+
+export function DetailsTable({
+ item,
+ fields
+}: {
+ item: any;
+ fields: DetailsField[];
}) {
return (
- {partIcons && (
-
-
-
- )}
- {fields.map((data: DetailsField[], index: number) => {
- let value: FieldValueType[] = [];
- for (const val of data) {
- if (val.value_formatter) {
- value.push(undefined);
- } else {
- value.push(item[val.name]);
- }
- }
-
- return (
-
- );
- })}
+ {fields.map((field: DetailsField, index: number) => (
+
+ ))}
|