From 9705521cd2c147f4e08fa975731abdb2182d632b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Oct 2023 23:34:16 +1100 Subject: [PATCH] [React] Part parameters table (#5709) * Track current panel selection in local storage * Simplify part detail tabs * Fix instances * Handle missing model type for rendering * Add some more API endpoints * Add PartParameter table * Add callback to create new part parameter * Allow PartParameter list API endpoint to be searched * More PanelGroup collapse tweaks - Still requires more attention * Fix logic for related part table - Need to rebuild columns when part id changes * Further fixes for related part table * Re-implement change to PanelGroup - useLocalStorage - Change got clobbered in recent merge conflict * Add part thumbnail to StockItemTable * Add simple button - Can be improved later * Fix for PartTable * Allow CORS requests to /static/ endpoint * Updates to other existing tables * Update URLs for dashboard items --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/part/api.py | 7 + src/frontend/src/components/forms/ApiForm.tsx | 1 + .../src/components/items/YesNoButton.tsx | 18 ++ .../src/components/nav/PanelGroup.tsx | 49 ++--- .../src/components/render/Instance.tsx | 39 +++- .../src/components/render/ModelType.tsx | 7 + src/frontend/src/components/render/Part.tsx | 17 ++ .../src/components/tables/InvenTreeTable.tsx | 2 +- .../tables/part/PartParameterTable.tsx | 170 ++++++++++++++++++ .../src/components/tables/part/PartTable.tsx | 7 +- .../tables/part/RelatedPartTable.tsx | 7 +- .../tables/stock/StockItemTable.tsx | 19 +- .../tables/stock/StockLocationTable.tsx | 7 +- src/frontend/src/defaults/dashboardItems.tsx | 34 ++-- src/frontend/src/pages/Notifications.tsx | 2 +- src/frontend/src/pages/build/BuildDetail.tsx | 2 +- .../src/pages/part/CategoryDetail.tsx | 10 +- src/frontend/src/pages/part/PartDetail.tsx | 78 ++++---- .../src/pages/stock/LocationDetail.tsx | 8 +- src/frontend/src/pages/stock/StockDetail.tsx | 16 +- src/frontend/src/states/ApiState.tsx | 6 + 22 files changed, 383 insertions(+), 125 deletions(-) create mode 100644 src/frontend/src/components/items/YesNoButton.tsx create mode 100644 src/frontend/src/components/tables/part/PartParameterTable.tsx diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 0b7dba4cc9..32f6ebfc35 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -140,7 +140,7 @@ ALLOWED_HOSTS = get_setting( # Cross Origin Resource Sharing (CORS) options # Only allow CORS access to API and media endpoints -CORS_URLS_REGEX = r'^/(api|media)/.*$' +CORS_URLS_REGEX = r'^/(api|media|static)/.*$' # Extract CORS options from configuration file CORS_ORIGIN_ALLOW_ALL = get_boolean_setting( diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b69b090d04..cd3c18686f 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1509,6 +1509,13 @@ class PartParameterList(PartParameterAPIMixin, ListCreateAPI): 'data': ['data_numeric', 'data'], } + search_fields = [ + 'data', + 'template__name', + 'template__description', + 'template__units', + ] + filterset_fields = [ 'part', 'template', diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index d40610c816..a0670ad1f5 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -207,6 +207,7 @@ export function ApiForm({ // Data validation error form.setErrors(error.response.data); setNonFieldErrors(error.response.data.non_field_errors ?? []); + setIsLoading(false); break; default: // Unexpected state on form error diff --git a/src/frontend/src/components/items/YesNoButton.tsx b/src/frontend/src/components/items/YesNoButton.tsx new file mode 100644 index 0000000000..a8aed94c16 --- /dev/null +++ b/src/frontend/src/components/items/YesNoButton.tsx @@ -0,0 +1,18 @@ +import { t } from '@lingui/macro'; +import { Badge } from '@mantine/core'; + +export function YesNoButton(value: any) { + const bool = + String(value).toLowerCase().trim() in ['true', '1', 't', 'y', 'yes']; + + return ( + + {bool ? t`Yes` : t`No`} + + ); +} diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx index 89dfa17149..58e485c43f 100644 --- a/src/frontend/src/components/nav/PanelGroup.tsx +++ b/src/frontend/src/components/nav/PanelGroup.tsx @@ -1,4 +1,12 @@ -import { Divider, Paper, Stack, Tabs, Tooltip } from '@mantine/core'; +import { + ActionIcon, + Divider, + Paper, + Stack, + Tabs, + Tooltip +} from '@mantine/core'; +import { useLocalStorage } from '@mantine/hooks'; import { IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse @@ -29,29 +37,31 @@ export type PanelType = { * @returns */ export function PanelGroup({ + pageKey, panels, selectedPanel, onPanelChange }: { + pageKey: string; panels: PanelType[]; selectedPanel?: string; onPanelChange?: (panel: string) => void; }): ReactNode { - // Default to the provided panel name, or the first panel - const [activePanelName, setActivePanelName] = useState( - selectedPanel || panels.length > 0 ? panels[0].name : '' - ); + const [activePanel, setActivePanel] = useLocalStorage({ + key: `panel-group-active-panel-${pageKey}`, + defaultValue: selectedPanel || panels.length > 0 ? panels[0].name : '' + }); // Update the active panel when the selected panel changes useEffect(() => { if (selectedPanel) { - setActivePanelName(selectedPanel); + setActivePanel(selectedPanel); } }, [selectedPanel]); // Callback when the active panel changes function handlePanelChange(panel: string) { - setActivePanelName(panel); + setActivePanel(panel); // Optionally call external callback hook if (onPanelChange) { @@ -64,7 +74,7 @@ export function PanelGroup({ return ( ) )} - setExpanded(!expanded)} - icon={ - expanded ? ( - - ) : ( - - ) - } - /> + > + {expanded ? ( + + ) : ( + + )} + {panels.map( (panel, idx) => diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index e310c48f57..47c6222bed 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Alert } from '@mantine/core'; +import { Alert, Space } from '@mantine/core'; import { Group, Text } from '@mantine/core'; import { ReactNode } from 'react'; @@ -18,7 +18,11 @@ import { RenderSalesOrder, RenderSalesOrderShipment } from './Order'; -import { RenderPart, RenderPartCategory } from './Part'; +import { + RenderPart, + RenderPartCategory, + RenderPartParameterTemplate +} from './Part'; import { RenderStockItem, RenderStockLocation } from './Stock'; import { RenderOwner, RenderUser } from './User'; @@ -40,6 +44,7 @@ const RendererLookup: EnumDictionary< [ModelType.owner]: RenderOwner, [ModelType.part]: RenderPart, [ModelType.partcategory]: RenderPartCategory, + [ModelType.partparametertemplate]: RenderPartParameterTemplate, [ModelType.purchaseorder]: RenderPurchaseOrder, [ModelType.returnorder]: RenderReturnOrder, [ModelType.salesorder]: RenderSalesOrder, @@ -63,8 +68,18 @@ export function RenderInstance({ model: ModelType | undefined; instance: any; }): ReactNode { - if (model === undefined) return ; + if (model === undefined) { + console.error('RenderInstance: No model provided'); + return ; + } + const RenderComponent = RendererLookup[model]; + + if (!RenderComponent) { + console.error(`RenderInstance: No renderer for model ${model}`); + return ; + } + return ; } @@ -74,12 +89,14 @@ export function RenderInstance({ export function RenderInlineModel({ primary, secondary, + suffix, image, labels, url }: { primary: string; secondary?: string; + suffix?: string; image?: string; labels?: string[]; url?: string; @@ -88,10 +105,18 @@ export function RenderInlineModel({ // TODO: Handle URL return ( - - {image && Thumbnail({ src: image, size: 18 })} - {primary} - {secondary && {secondary}} + + + {image && Thumbnail({ src: image, size: 18 })} + {primary} + {secondary && {secondary}} + + {suffix && ( + <> + + {suffix} + + )} ); } diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 17a671fd40..3a250c3f35 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -5,6 +5,7 @@ export enum ModelType { supplierpart = 'supplierpart', manufacturerpart = 'manufacturerpart', partcategory = 'partcategory', + partparametertemplate = 'partparametertemplate', stockitem = 'stockitem', stocklocation = 'stocklocation', build = 'build', @@ -37,6 +38,12 @@ export const ModelInformationDict: ModelDictory = { url_overview: '/part', url_detail: '/part/:pk/' }, + partparametertemplate: { + label: t`Part Parameter Template`, + label_multiple: t`Part Parameter Templates`, + url_overview: '/partparametertemplate', + url_detail: '/partparametertemplate/:pk/' + }, supplierpart: { label: t`Supplier Part`, label_multiple: t`Supplier Parts`, diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx index cf0ec5d282..48af44aef6 100644 --- a/src/frontend/src/components/render/Part.tsx +++ b/src/frontend/src/components/render/Part.tsx @@ -30,3 +30,20 @@ export function RenderPartCategory({ instance }: { instance: any }): ReactNode { /> ); } + +/** + * Inline rendering of a PartParameterTemplate instance + */ +export function RenderPartParameterTemplate({ + instance +}: { + instance: any; +}): ReactNode { + return ( + + ); +} diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx index deab75d8c0..922982aa1d 100644 --- a/src/frontend/src/components/tables/InvenTreeTable.tsx +++ b/src/frontend/src/components/tables/InvenTreeTable.tsx @@ -387,7 +387,7 @@ export function InvenTreeTable({ fetchTableData, { refetchOnWindowFocus: false, - refetchOnMount: 'always' + refetchOnMount: true } ); diff --git a/src/frontend/src/components/tables/part/PartParameterTable.tsx b/src/frontend/src/components/tables/part/PartParameterTable.tsx new file mode 100644 index 0000000000..541f663ae5 --- /dev/null +++ b/src/frontend/src/components/tables/part/PartParameterTable.tsx @@ -0,0 +1,170 @@ +import { t } from '@lingui/macro'; +import { ActionIcon, Text, Tooltip } from '@mantine/core'; +import { IconTextPlus } from '@tabler/icons-react'; +import { useCallback, useMemo } from 'react'; + +import { + openCreateApiForm, + openDeleteApiForm, + openEditApiForm +} from '../../../functions/forms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { YesNoButton } from '../../items/YesNoButton'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; + +/** + * Construct a table listing parameters for a given part + */ +export function PartParameterTable({ partId }: { partId: any }) { + const { tableKey, refreshTable } = useTableRefresh('part-parameters'); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'name', + title: t`Parameter`, + switchable: false, + sortable: true, + render: (record) => record.template_detail?.name + }, + { + accessor: 'description', + title: t`Description`, + sortable: false, + switchable: true, + render: (record) => record.template_detail?.description + }, + { + accessor: 'data', + title: t`Value`, + switchable: false, + sortable: true, + render: (record) => { + let template = record.template_detail; + + if (template?.checkbox) { + return ; + } + + if (record.data_numeric) { + // TODO: Numeric data + } + + // TODO: Units + + return record.data; + } + }, + { + accessor: 'units', + title: t`Units`, + switchable: true, + sortable: true, + render: (record) => record.template_detail?.units + } + ]; + }, []); + + // Callback for row actions + // TODO: Adjust based on user permissions + const rowActions = useCallback((record: any) => { + let actions = []; + + actions.push({ + title: t`Edit`, + onClick: () => { + openEditApiForm({ + name: 'edit-part-parameter', + url: ApiPaths.part_parameter_list, + pk: record.pk, + title: t`Edit Part Parameter`, + fields: { + part: { + hidden: true + }, + template: {}, + data: {} + }, + successMessage: t`Part parameter updated`, + onFormSuccess: refreshTable + }); + } + }); + + actions.push({ + title: t`Delete`, + color: 'red', + onClick: () => { + openDeleteApiForm({ + name: 'delete-part-parameter', + url: ApiPaths.part_parameter_list, + pk: record.pk, + title: t`Delete Part Parameter`, + successMessage: t`Part parameter deleted`, + onFormSuccess: refreshTable, + preFormContent: ( + {t`Are you sure you want to remove this parameter?`} + ) + }); + } + }); + + return actions; + }, []); + + const addParameter = useCallback(() => { + if (!partId) { + return; + } + + openCreateApiForm({ + name: 'add-part-parameter', + url: ApiPaths.part_parameter_list, + title: t`Add Part Parameter`, + fields: { + part: { + hidden: true, + value: partId + }, + template: {}, + data: {} + }, + successMessage: t`Part parameter added`, + onFormSuccess: refreshTable + }); + }, [partId]); + + // Custom table actions + const tableActions = useMemo(() => { + let actions = []; + + // TODO: Hide if user does not have permission to edit parts + actions.push( + + + + + + ); + + return actions; + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx index 49bdb5d58d..92abbf5c4e 100644 --- a/src/frontend/src/components/tables/part/PartTable.tsx +++ b/src/frontend/src/components/tables/part/PartTable.tsx @@ -35,11 +35,6 @@ function partTableColumns(): TableColumn[] { /> {record.full_name} - // ); } }, @@ -68,7 +63,7 @@ function partTableColumns(): TableColumn[] { render: function (record: any) { // TODO: Link to the category detail page return shortenString({ - str: record.category_detail.pathstring + str: record.category_detail?.pathstring }); } }, diff --git a/src/frontend/src/components/tables/part/RelatedPartTable.tsx b/src/frontend/src/components/tables/part/RelatedPartTable.tsx index 4bd3c0eb71..1f87a5203b 100644 --- a/src/frontend/src/components/tables/part/RelatedPartTable.tsx +++ b/src/frontend/src/components/tables/part/RelatedPartTable.tsx @@ -11,6 +11,9 @@ import { Thumbnail } from '../../images/Thumbnail'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; +/** + * Construct a table listing related parts for a given part + */ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { const { tableKey, refreshTable } = useTableRefresh('relatedparts'); @@ -56,7 +59,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { } } ]; - }, []); + }, [partId]); const addRelatedPart = useCallback(() => { openCreateApiForm({ @@ -75,7 +78,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { successMessage: t`Related part added`, onFormSuccess: refreshTable }); - }, []); + }, [partId]); const customActions: ReactNode[] = useMemo(() => { // TODO: Hide if user does not have permission to edit parts diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx index bce8b77b82..b802dbfb90 100644 --- a/src/frontend/src/components/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx @@ -1,11 +1,12 @@ import { t } from '@lingui/macro'; -import { Text } from '@mantine/core'; +import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { notYetImplemented } from '../../../functions/notifications'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { Thumbnail } from '../../images/Thumbnail'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { RowAction } from '../RowActions'; @@ -21,14 +22,16 @@ function stockItemTableColumns(): TableColumn[] { sortable: true, title: t`Part`, render: function (record: any) { - let part = record.part_detail; + let part = record.part_detail ?? {}; return ( - {part.full_name} - // + + + {part.full_name} + ); } }, diff --git a/src/frontend/src/components/tables/stock/StockLocationTable.tsx b/src/frontend/src/components/tables/stock/StockLocationTable.tsx index cf411321e4..2ea8c562e2 100644 --- a/src/frontend/src/components/tables/stock/StockLocationTable.tsx +++ b/src/frontend/src/components/tables/stock/StockLocationTable.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { YesNoButton } from '../../items/YesNoButton'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -44,16 +45,14 @@ export function StockLocationTable({ params = {} }: { params?: any }) { title: t`Structural`, switchable: true, sortable: true, - render: (record: any) => (record.structural ? 'Y' : 'N') - // TODO: custom 'true / false' label, + render: (record: any) => }, { accessor: 'external', title: t`External`, switchable: true, sortable: true, - render: (record: any) => (record.structural ? 'Y' : 'N') - // TODO: custom 'true / false' label, + render: (record: any) => }, { accessor: 'location_type', diff --git a/src/frontend/src/defaults/dashboardItems.tsx b/src/frontend/src/defaults/dashboardItems.tsx index fe56a2450c..90004f1063 100644 --- a/src/frontend/src/defaults/dashboardItems.tsx +++ b/src/frontend/src/defaults/dashboardItems.tsx @@ -1,116 +1,118 @@ import { t } from '@lingui/macro'; +import { ApiPaths, apiUrl } from '../states/ApiState'; + export const dashboardItems = [ { id: 'starred-parts', text: t`Subscribed Parts`, icon: 'fa-bell', - url: 'part', + url: apiUrl(ApiPaths.part_list), params: { starred: true } }, { id: 'starred-categories', text: t`Subscribed Categories`, icon: 'fa-bell', - url: 'part/category', + url: apiUrl(ApiPaths.category_list), params: { starred: true } }, { id: 'latest-parts', text: t`Latest Parts`, icon: 'fa-newspaper', - url: 'part', + url: apiUrl(ApiPaths.part_list), params: { ordering: '-creation_date', limit: 10 } }, { id: 'bom-validation', text: t`BOM Waiting Validation`, icon: 'fa-times-circle', - url: 'part', + url: apiUrl(ApiPaths.part_list), params: { bom_valid: false } }, { id: 'recently-updated-stock', text: t`Recently Updated`, icon: 'fa-clock', - url: 'stock', + url: apiUrl(ApiPaths.stock_item_list), params: { part_detail: true, ordering: '-updated', limit: 10 } }, { id: 'low-stock', text: t`Low Stock`, icon: 'fa-flag', - url: 'part', + url: apiUrl(ApiPaths.part_list), params: { low_stock: true } }, { id: 'depleted-stock', text: t`Depleted Stock`, icon: 'fa-times', - url: 'part', + url: apiUrl(ApiPaths.part_list), params: { depleted_stock: true } }, { id: 'stock-to-build', text: t`Required for Build Orders`, icon: 'fa-bullhorn', - url: 'part', + url: apiUrl(ApiPaths.part_list), params: { stock_to_build: true } }, { id: 'expired-stock', text: t`Expired Stock`, icon: 'fa-calendar-times', - url: 'stock', + url: apiUrl(ApiPaths.stock_item_list), params: { expired: true } }, { id: 'stale-stock', text: t`Stale Stock`, icon: 'fa-stopwatch', - url: 'stock', + url: apiUrl(ApiPaths.stock_item_list), params: { stale: true, expired: true } }, { id: 'build-pending', text: t`Build Orders In Progress`, icon: 'fa-cogs', - url: 'build', + url: apiUrl(ApiPaths.build_order_list), params: { active: true } }, { id: 'build-overdue', text: t`Overdue Build Orders`, icon: 'fa-calendar-times', - url: 'build', + url: apiUrl(ApiPaths.build_order_list), params: { overdue: true } }, { id: 'po-outstanding', text: t`Outstanding Purchase Orders`, icon: 'fa-sign-in-alt', - url: 'order/po', + url: apiUrl(ApiPaths.purchase_order_list), params: { supplier_detail: true, outstanding: true } }, { id: 'po-overdue', text: t`Overdue Purchase Orders`, icon: 'fa-calendar-times', - url: 'order/po', + url: apiUrl(ApiPaths.purchase_order_list), params: { supplier_detail: true, overdue: true } }, { id: 'so-outstanding', text: t`Outstanding Sales Orders`, icon: 'fa-sign-out-alt', - url: 'order/so', + url: apiUrl(ApiPaths.sales_order_list), params: { customer_detail: true, outstanding: true } }, { id: 'so-overdue', text: t`Overdue Sales Orders`, icon: 'fa-calendar-times', - url: 'order/so', + url: apiUrl(ApiPaths.sales_order_list), params: { customer_detail: true, overdue: true } }, { diff --git a/src/frontend/src/pages/Notifications.tsx b/src/frontend/src/pages/Notifications.tsx index c6ac68b4a8..45f73eb878 100644 --- a/src/frontend/src/pages/Notifications.tsx +++ b/src/frontend/src/pages/Notifications.tsx @@ -88,7 +88,7 @@ export default function NotificationsPage() { <> - + ); diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index bf5f5ec765..05924b0dea 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -148,7 +148,7 @@ export default function BuildDetail() { actions={[]} /> - + ); diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index ea4f869d6e..5e418fb38b 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -43,7 +43,7 @@ export default function CategoryDetail({}: {}) { { name: 'parts', label: t`Parts`, - icon: , + icon: , content: ( , + label: t`Part Categories`, + icon: , content: ( , + icon: , content: } ], @@ -95,7 +95,7 @@ export default function CategoryDetail({}: {}) { detail={{category.name ?? 'Top level'}} breadcrumbs={breadcrumbs} /> - + ); } diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index e903c95085..030b07260e 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -18,22 +18,21 @@ import { IconPackages, IconPaperclip, IconShoppingCart, + IconStack2, IconTestPipe, IconTools, IconTruckDelivery, IconVersions } from '@tabler/icons-react'; -import React from 'react'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { api } from '../../App'; import { ApiImage } from '../../components/images/ApiImage'; -import { Thumbnail } from '../../components/images/Thumbnail'; import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { AttachmentTable } from '../../components/tables/AttachmentTable'; +import { PartParameterTable } from '../../components/tables/part/PartParameterTable'; import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; @@ -67,87 +66,99 @@ export default function PartDetail() { { name: 'details', label: t`Details`, - icon: , + icon: , content: }, + { + name: 'parameters', + label: t`Parameters`, + icon: , + content: + }, { name: 'stock', label: t`Stock`, - icon: , - content: partStockTab() + icon: , + content: ( + + ) }, { name: 'variants', label: t`Variants`, - icon: , + icon: , hidden: !part.is_template, content: }, { name: 'bom', label: t`Bill of Materials`, - icon: , + icon: , hidden: !part.assembly, content: }, { name: 'builds', label: t`Build Orders`, - icon: , + icon: , hidden: !part.assembly && !part.component, content: }, { name: 'used_in', label: t`Used In`, - icon: , + icon: , hidden: !part.component, content: }, { name: 'pricing', label: t`Pricing`, - icon: , + icon: , content: }, { name: 'suppliers', label: t`Suppliers`, - icon: , + icon: , hidden: !part.purchaseable, content: }, { name: 'purchase_orders', label: t`Purchase Orders`, - icon: , + icon: , content: , hidden: !part.purchaseable }, { name: 'sales_orders', label: t`Sales Orders`, - icon: , + icon: , content: , hidden: !part.salable }, { name: 'test_templates', label: t`Test Templates`, - icon: , + icon: , content: , hidden: !part.trackable }, { name: 'related_parts', label: t`Related Parts`, - icon: , + icon: , content: }, { name: 'attachments', label: t`Attachments`, - icon: , + icon: , content: ( , - content: partNotesTab() + icon: , + content: ( + + ) } ]; }, [part]); - function partNotesTab(): React.ReactNode { - // TODO: Set edit permission based on user permissions - return ( - - ); - } - - function partStockTab(): React.ReactNode { - return ( - - ); - } - const breadcrumbs = useMemo( () => [ { name: t`Parts`, url: '/part' }, @@ -239,7 +235,7 @@ export default function PartDetail() { ]} /> - + ); diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index d1587c7245..75244ef515 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -31,7 +31,7 @@ export default function Stock() { { name: 'stock-items', label: t`Stock Items`, - icon: , + icon: , content: ( , + label: t`Stock Locations`, + icon: , content: ( {location.name ?? 'Top level'}} breadcrumbs={breadcrumbs} /> - + ); diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 026df4e1da..ecd6cdbb17 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -42,37 +42,37 @@ export default function StockDetail() { { name: 'details', label: t`Details`, - icon: , + icon: , content: }, { name: 'tracking', label: t`Stock Tracking`, - icon: , + icon: , content: }, { name: 'allocations', label: t`Allocations`, - icon: , + icon: , content: }, { name: 'installed_items', label: t`Installed Items`, - icon: , + icon: , content: }, { name: 'child_items', label: t`Child Items`, - icon: , + icon: , content: }, { name: 'attachments', label: t`Attachments`, - icon: , + icon: , content: ( , + icon: , content: ( - + ); } diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 024357be2d..12ad8978a1 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -49,6 +49,8 @@ export enum ApiPaths { category_list = 'api-category-list', related_part_list = 'api-related-part-list', part_attachment_list = 'api-part-attachment-list', + part_parameter_list = 'api-part-parameter-list', + part_parameter_template_list = 'api-part-parameter-template-list', // Company URLs company_list = 'api-company-list', @@ -111,6 +113,10 @@ export function apiEndpoint(path: ApiPaths): string { return 'build/attachment/'; case ApiPaths.part_list: return 'part/'; + case ApiPaths.part_parameter_list: + return 'part/parameter/'; + case ApiPaths.part_parameter_template_list: + return 'part/parameter/template/'; case ApiPaths.category_list: return 'part/category/'; case ApiPaths.related_part_list: