From f71322ecd31866236db76bdc34a77cdf2e6a4714 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 2 Nov 2023 12:10:03 +1100 Subject: [PATCH] [React] SupplierPart table (#5833) * Fix TableHoverCard component * Improving handling of very wide table cells * Update panels for PartDetail * Refactor component * Add SupplierPart table * Refactor forms - Do not need to specify custom form name any more * More fixes for modal forms * Refactor forms field code * Add generic row action components for edit and delete * Add placeholder comments * Add ability to edit supplier part from table * Create supplier part * Add missing import * Revert scroll behaviour for wide cells - Does not play nice on chrome * Add placeholder panel for part manufacturers * Fix inline renderer for manufacturerpart * Cleanup unused imports * Add icons to supplier part fields * Increase size of form titles * Another fix --- InvenTree/company/serializers.py | 1 + src/frontend/src/components/forms/ApiForm.tsx | 14 +- .../src/components/images/Thumbnail.tsx | 45 +-- .../src/components/render/Company.tsx | 23 +- .../src/components/render/Instance.tsx | 5 +- .../src/components/settings/SettingItem.tsx | 1 - .../src/components/tables/InvenTreeTable.tsx | 6 +- .../src/components/tables/RowActions.tsx | 45 ++- .../src/components/tables/TableHoverCard.tsx | 2 +- .../src/components/tables/bom/BomTable.tsx | 13 +- .../tables/general/AttachmentTable.tsx | 51 ++-- .../tables/part/PartParameterTable.tsx | 73 +++-- .../src/components/tables/part/PartTable.tsx | 14 +- .../tables/part/RelatedPartTable.tsx | 10 +- .../tables/purchasing/PurchaseOrderTable.tsx | 10 +- .../tables/purchasing/SupplierPartTable.tsx | 258 ++++++++++++++++++ .../tables/sales/ReturnOrderTable.tsx | 10 +- .../tables/sales/SalesOrderTable.tsx | 10 +- .../tables/settings/CustomUnitsTable.tsx | 15 +- .../tables/settings/ProjectCodeTable.tsx | 16 +- .../tables/stock/StockItemTable.tsx | 9 +- .../{functions => }/forms/AttachmentForms.tsx | 9 +- src/frontend/src/forms/CompanyForms.tsx | 106 +++++++ .../src/{functions => }/forms/PartForms.tsx | 8 +- .../src/{functions => }/forms/StockForms.tsx | 9 +- src/frontend/src/functions/forms.tsx | 6 +- .../src/functions/forms/CompanyForms.tsx | 57 ---- src/frontend/src/pages/Index/Playground.tsx | 8 +- .../src/pages/company/CompanyDetail.tsx | 2 +- src/frontend/src/pages/part/PartDetail.tsx | 28 +- src/frontend/src/pages/stock/StockDetail.tsx | 2 +- 31 files changed, 608 insertions(+), 258 deletions(-) create mode 100644 src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx rename src/frontend/src/{functions => }/forms/AttachmentForms.tsx (91%) create mode 100644 src/frontend/src/forms/CompanyForms.tsx rename src/frontend/src/{functions => }/forms/PartForms.tsx (90%) rename src/frontend/src/{functions => }/forms/StockForms.tsx (91%) delete mode 100644 src/frontend/src/functions/forms/CompanyForms.tsx diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index a91176d862..860818f8dd 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -372,6 +372,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer): # Annotated field showing total in-stock quantity in_stock = serializers.FloatField(read_only=True) + available = serializers.FloatField(required=False) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 3370f368eb..3c7e526694 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -16,7 +16,6 @@ import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField'; /** * Properties for the ApiForm component - * @param name : The name (identifier) for this form * @param url : The API endpoint to fetch the form data from * @param pk : Optional primary-key value when editing an existing object * @param title : The title to display in the form header @@ -35,7 +34,6 @@ import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField'; * @param onFormError : A callback function to call when the form is submitted with errors. */ export interface ApiFormProps { - name: string; url: ApiPaths; pk?: number | string | undefined; title: string; @@ -104,7 +102,7 @@ export function ApiForm({ // Query manager for retrieiving initial data from the server const initialDataQuery = useQuery({ enabled: false, - queryKey: ['form-initial-data', props.name, props.url, props.pk], + queryKey: ['form-initial-data', modalId, props.method, props.url, props.pk], queryFn: async () => { return api .get(url) @@ -150,7 +148,13 @@ export function ApiForm({ // Fetch initial data if the fetchInitialData property is set if (props.fetchInitialData) { queryClient.removeQueries({ - queryKey: ['form-initial-data', props.name, props.url, props.pk] + queryKey: [ + 'form-initial-data', + modalId, + props.method, + props.url, + props.pk + ] }); initialDataQuery.refetch(); } @@ -159,7 +163,7 @@ export function ApiForm({ // Query manager for submitting data const submitQuery = useQuery({ enabled: false, - queryKey: ['form-submit', props.name, props.url, props.pk], + queryKey: ['form-submit', modalId, props.method, props.url, props.pk], queryFn: async () => { let method = props.method?.toLowerCase() ?? 'get'; diff --git a/src/frontend/src/components/images/Thumbnail.tsx b/src/frontend/src/components/images/Thumbnail.tsx index 43f0effb72..80a4f7a9f7 100644 --- a/src/frontend/src/components/images/Thumbnail.tsx +++ b/src/frontend/src/components/images/Thumbnail.tsx @@ -2,35 +2,43 @@ import { t } from '@lingui/macro'; import { Anchor } from '@mantine/core'; import { Group } from '@mantine/core'; import { Text } from '@mantine/core'; -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { ApiImage } from './ApiImage'; +/* + * Render an image, loaded via the API + */ export function Thumbnail({ src, alt = t`Thumbnail`, - size = 20 + size = 20, + text }: { src?: string | undefined; alt?: string; size?: number; + text?: ReactNode; }) { - // TODO: Use HoverCard to display a larger version of the image + const backup_image = '/static/img/blank_image.png'; return ( - + + + {text} + ); } @@ -39,7 +47,7 @@ export function ThumbnailHoverCard({ text, link = '', alt = t`Thumbnail`, - size = 24 + size = 20 }: { src: string; text: string; @@ -56,12 +64,13 @@ export function ThumbnailHoverCard({ ); }, [src, text, alt, size]); - if (link) + if (link) { return ( {card} ); + } return
{card}
; } diff --git a/src/frontend/src/components/render/Company.tsx b/src/frontend/src/components/render/Company.tsx index 06485839f2..a1d2b40e87 100644 --- a/src/frontend/src/components/render/Company.tsx +++ b/src/frontend/src/components/render/Company.tsx @@ -70,15 +70,20 @@ export function RenderSupplierPart({ instance }: { instance: any }): ReactNode { /** * Inline rendering of a single ManufacturerPart instance */ -export function ManufacturerPart({ instance }: { instance: any }): ReactNode { - let supplier = instance.supplier_detail ?? {}; +export function RenderManufacturerPart({ + instance +}: { + instance: any; +}): ReactNode { let part = instance.part_detail ?? {}; + let manufacturer = instance.manufacturer_detail ?? {}; - let text = instance.SKU; - - if (supplier.name) { - text = `${supplier.name} | ${text}`; - } - - return ; + return ( + + ); } diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 277a901364..c87e61c394 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -9,6 +9,7 @@ import { RenderAddress, RenderCompany, RenderContact, + RenderManufacturerPart, RenderSupplierPart } from './Company'; import { ModelType } from './ModelType'; @@ -41,6 +42,7 @@ const RendererLookup: EnumDictionary< [ModelType.build]: RenderBuildOrder, [ModelType.company]: RenderCompany, [ModelType.contact]: RenderContact, + [ModelType.manufacturerpart]: RenderManufacturerPart, [ModelType.owner]: RenderOwner, [ModelType.part]: RenderPart, [ModelType.partcategory]: RenderPartCategory, @@ -54,8 +56,7 @@ const RendererLookup: EnumDictionary< [ModelType.stockitem]: RenderStockItem, [ModelType.stockhistory]: RenderStockItem, [ModelType.supplierpart]: RenderSupplierPart, - [ModelType.user]: RenderUser, - [ModelType.manufacturerpart]: RenderPart + [ModelType.user]: RenderUser }; // import { ApiFormFieldType } from "../forms/fields/ApiFormField"; diff --git a/src/frontend/src/components/settings/SettingItem.tsx b/src/frontend/src/components/settings/SettingItem.tsx index 76a9609390..b92df30879 100644 --- a/src/frontend/src/components/settings/SettingItem.tsx +++ b/src/frontend/src/components/settings/SettingItem.tsx @@ -51,7 +51,6 @@ function SettingValue({ } openModalApiForm({ - name: 'setting-edit', url: settingsState.endpoint, pk: setting.key, method: 'PATCH', diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx index 19be7e362f..4c51d4b19f 100644 --- a/src/frontend/src/components/tables/InvenTreeTable.tsx +++ b/src/frontend/src/components/tables/InvenTreeTable.tsx @@ -524,7 +524,11 @@ export function InvenTreeTable({ onRowClick={tableProps.onRowClick} defaultColumnProps={{ noWrap: true, - textAlignment: 'left' + textAlignment: 'left', + cellsStyle: { + // TODO: Need a better way of handling "wide" cells, + overflow: 'hidden' + } }} /> diff --git a/src/frontend/src/components/tables/RowActions.tsx b/src/frontend/src/components/tables/RowActions.tsx index 8a6df573bb..1c0392c6ab 100644 --- a/src/frontend/src/components/tables/RowActions.tsx +++ b/src/frontend/src/components/tables/RowActions.tsx @@ -2,7 +2,7 @@ import { t } from '@lingui/macro'; import { ActionIcon, Tooltip } from '@mantine/core'; import { Menu, Text } from '@mantine/core'; import { IconDots } from '@tabler/icons-react'; -import { ReactNode, useState } from 'react'; +import { ReactNode, useMemo, useState } from 'react'; import { notYetImplemented } from '../../functions/notifications'; @@ -12,9 +12,41 @@ export type RowAction = { color?: string; onClick?: () => void; tooltip?: string; - icon?: ReactNode; + hidden?: boolean; }; +// Component for ediitng a row in a table +export function RowEditAction({ + onClick, + hidden +}: { + onClick?: () => void; + hidden?: boolean; +}): RowAction { + return { + title: t`Edit`, + color: 'green', + onClick: onClick, + hidden: hidden + }; +} + +// Component for deleting a row in a table +export function RowDeleteAction({ + onClick, + hidden +}: { + onClick?: () => void; + hidden?: boolean; +}): RowAction { + return { + title: t`Delete`, + color: 'red', + onClick: onClick, + hidden: hidden + }; +} + /** * Component for displaying actions for a row in a table. * Displays a simple dropdown menu with a list of actions. @@ -39,8 +71,12 @@ export function RowActions({ const [opened, setOpened] = useState(false); + const visibleActions = useMemo(() => { + return actions.filter((action) => !action.hidden); + }, [actions]); + return ( - actions.length > 0 && ( + visibleActions.length > 0 && ( {title || t`Actions`} - {actions.map((action, idx) => ( + {visibleActions.map((action, idx) => ( { @@ -75,7 +111,6 @@ export function RowActions({ notYetImplemented(); } }} - icon={action.icon} title={action.tooltip || action.title} > diff --git a/src/frontend/src/components/tables/TableHoverCard.tsx b/src/frontend/src/components/tables/TableHoverCard.tsx index d3efac13ce..d91fe3407a 100644 --- a/src/frontend/src/components/tables/TableHoverCard.tsx +++ b/src/frontend/src/components/tables/TableHoverCard.tsx @@ -23,7 +23,7 @@ export function TableHoverCard({ return ( - + {value} diff --git a/src/frontend/src/components/tables/bom/BomTable.tsx b/src/frontend/src/components/tables/bom/BomTable.tsx index cd7bf93bde..cf05870946 100644 --- a/src/frontend/src/components/tables/bom/BomTable.tsx +++ b/src/frontend/src/components/tables/bom/BomTable.tsx @@ -11,7 +11,7 @@ import { YesNoButton } from '../../items/YesNoButton'; import { TableColumn } from '../Column'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; -import { RowAction } from '../RowActions'; +import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; import { TableHoverCard } from '../TableHoverCard'; export function BomTable({ @@ -204,14 +204,11 @@ export function BomTable({ }); } - actions.push({ - title: t`Edit` - }); + // TODO: Action on edit + actions.push(RowEditAction({})); - actions.push({ - title: t`Delete`, - color: 'red' - }); + // TODO: Action on delete + actions.push(RowDeleteAction({})); return actions; }, diff --git a/src/frontend/src/components/tables/general/AttachmentTable.tsx b/src/frontend/src/components/tables/general/AttachmentTable.tsx index ac7b3388ff..f8b9dc89c5 100644 --- a/src/frontend/src/components/tables/general/AttachmentTable.tsx +++ b/src/frontend/src/components/tables/general/AttachmentTable.tsx @@ -11,13 +11,13 @@ import { addAttachment, deleteAttachment, editAttachment -} from '../../../functions/forms/AttachmentForms'; +} from '../../../forms/AttachmentForms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { AttachmentLink } from '../../items/AttachmentLink'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; -import { RowAction } from '../RowActions'; +import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; /** * Define set of columns to display for the attachment table @@ -113,32 +113,33 @@ export function AttachmentTable({ let actions: RowAction[] = []; if (allowEdit) { - actions.push({ - title: t`Edit`, - onClick: () => { - editAttachment({ - endpoint: endpoint, - model: model, - pk: record.pk, - attachmentType: record.attachment ? 'file' : 'link', - callback: refreshTable - }); - } - }); + actions.push( + RowEditAction({ + onClick: () => { + editAttachment({ + endpoint: endpoint, + model: model, + pk: record.pk, + attachmentType: record.attachment ? 'file' : 'link', + callback: refreshTable + }); + } + }) + ); } if (allowDelete) { - actions.push({ - title: t`Delete`, - color: 'red', - onClick: () => { - deleteAttachment({ - endpoint: endpoint, - pk: record.pk, - callback: refreshTable - }); - } - }); + actions.push( + RowDeleteAction({ + onClick: () => { + deleteAttachment({ + endpoint: endpoint, + pk: record.pk, + callback: refreshTable + }); + } + }) + ); } return actions; diff --git a/src/frontend/src/components/tables/part/PartParameterTable.tsx b/src/frontend/src/components/tables/part/PartParameterTable.tsx index 1a346ea6aa..2823a45791 100644 --- a/src/frontend/src/components/tables/part/PartParameterTable.tsx +++ b/src/frontend/src/components/tables/part/PartParameterTable.tsx @@ -14,6 +14,7 @@ import { Thumbnail } from '../../images/Thumbnail'; import { YesNoButton } from '../../items/YesNoButton'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction, RowEditAction } from '../RowActions'; /** * Construct a table listing parameters for a given part @@ -103,44 +104,43 @@ export function PartParameterTable({ partId }: { partId: 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 + actions.push( + RowEditAction({ + onClick: () => { + openEditApiForm({ + url: ApiPaths.part_parameter_list, + pk: record.pk, + title: t`Edit Part Parameter`, + fields: { + part: { + hidden: true + }, + template: {}, + data: {} }, - template: {}, - data: {} - }, - successMessage: t`Part parameter updated`, - onFormSuccess: refreshTable - }); - } - }); + 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?`} - ) - }); - } - }); + actions.push( + RowDeleteAction({ + onClick: () => { + openDeleteApiForm({ + 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; }, @@ -153,7 +153,6 @@ export function PartParameterTable({ partId }: { partId: any }) { } openCreateApiForm({ - name: 'add-part-parameter', url: ApiPaths.part_parameter_list, title: t`Add Part Parameter`, fields: { diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx index d59ded46c6..fb2466d1d8 100644 --- a/src/frontend/src/components/tables/part/PartTable.tsx +++ b/src/frontend/src/components/tables/part/PartTable.tsx @@ -23,16 +23,12 @@ function partTableColumns(): TableColumn[] { noWrap: true, title: t`Part`, render: function (record: any) { - // TODO - Link to the part detail page return ( - - - {record.full_name} - + ); } }, diff --git a/src/frontend/src/components/tables/part/RelatedPartTable.tsx b/src/frontend/src/components/tables/part/RelatedPartTable.tsx index 1f87a5203b..477be0a5f7 100644 --- a/src/frontend/src/components/tables/part/RelatedPartTable.tsx +++ b/src/frontend/src/components/tables/part/RelatedPartTable.tsx @@ -10,6 +10,7 @@ import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { Thumbnail } from '../../images/Thumbnail'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction } from '../RowActions'; /** * Construct a table listing related parts for a given part @@ -33,7 +34,6 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { { accessor: 'part', title: t`Part`, - noWrap: true, render: (record: any) => { let part = getPart(record); return ( @@ -63,7 +63,6 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { const addRelatedPart = useCallback(() => { openCreateApiForm({ - name: 'add-related-part', title: t`Add Related Part`, url: ApiPaths.related_part_list, fields: { @@ -99,12 +98,9 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { // TODO: Hide if user does not have permission to edit parts const rowActions = useCallback((record: any) => { return [ - { - title: t`Delete`, - color: 'red', + RowDeleteAction({ onClick: () => { openDeleteApiForm({ - name: 'delete-related-part', url: ApiPaths.related_part_list, pk: record.pk, title: t`Delete Related Part`, @@ -115,7 +111,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { onFormSuccess: refreshTable }); } - } + }) ]; }, []); diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx index 57111ee6ec..9c7247082c 100644 --- a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx @@ -1,5 +1,4 @@ import { t } from '@lingui/macro'; -import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -45,10 +44,11 @@ export function PurchaseOrderTable({ params }: { params?: any }) { let supplier = record.supplier_detail ?? {}; return ( - - - {supplier?.name} - + ); } }, diff --git a/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx new file mode 100644 index 0000000000..5b2c6723f2 --- /dev/null +++ b/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx @@ -0,0 +1,258 @@ +import { t } from '@lingui/macro'; +import { ActionIcon, Stack, Text, Tooltip } from '@mantine/core'; +import { IconCirclePlus } from '@tabler/icons-react'; +import { ReactNode, useCallback, useMemo } from 'react'; + +import { supplierPartFields } from '../../../forms/CompanyForms'; +import { + openCreateApiForm, + openDeleteApiForm, + openEditApiForm +} from '../../../functions/forms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { useUserState } from '../../../states/UserState'; +import { Thumbnail } from '../../images/Thumbnail'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction, RowEditAction } from '../RowActions'; +import { TableHoverCard } from '../TableHoverCard'; + +/* + * Construct a table listing supplier parts + */ + +export function SupplierPartTable({ params }: { params: any }): ReactNode { + const { tableKey, refreshTable } = useTableRefresh('supplierparts'); + + const user = useUserState(); + + // Construct table columns for this table + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'part', + title: t`Part`, + switchable: 'part' in params, + sortable: true, + render: (record: any) => { + let part = record?.part_detail ?? {}; + + return ( + + ); + } + }, + { + accessor: 'supplier', + title: t`Supplier`, + sortable: true, + render: (record: any) => { + let supplier = record?.supplier_detail ?? {}; + + return ( + + ); + } + }, + { + accessor: 'SKU', + title: t`Supplier Part`, + sortable: true + }, + { + accessor: 'description', + title: t`Description`, + sortable: false, + switchable: true + }, + { + accessor: 'manufacturer', + switchable: true, + sortable: true, + title: t`Manufacturer`, + render: (record: any) => { + let manufacturer = record?.manufacturer_detail ?? {}; + + return ( + + ); + } + }, + { + accessor: 'MPN', + switchable: true, + sortable: true, + title: t`MPN`, + render: (record: any) => record?.manufacturer_part_detail?.MPN + }, + { + accessor: 'in_stock', + title: t`In Stock`, + sortable: true, + switchable: true + }, + { + accessor: 'packaging', + title: t`Packaging`, + sortable: true, + switchable: true + }, + { + accessor: 'pack_quantity', + title: t`Pack Quantity`, + sortable: true, + switchable: true, + render: (record: any) => { + let part = record?.part_detail ?? {}; + + let extra = []; + + if (part.units) { + extra.push( + + {t`Base units`} : {part.units} + + ); + } + + return ( + 0 && {extra}} + title={t`Pack Quantity`} + /> + ); + } + }, + { + accessor: 'link', + title: t`Link`, + sortable: false, + switchable: true + // TODO: custom link renderer? + }, + { + accessor: 'note', + title: t`Notes`, + sortable: false, + switchable: true + }, + { + accessor: 'available', + title: t`Availability`, + sortable: true, + switchable: true, + render: (record: any) => { + let extra = []; + + if (record.availablility_updated) { + extra.push( + + {t`Updated`} : {record.availablility_updated} + + ); + } + + return ( + 0 && {extra}} + /> + ); + } + } + ]; + }, [params]); + + const addSupplierPart = useCallback(() => { + let fields = supplierPartFields(); + + fields.part.value = params?.part; + fields.supplier.value = params?.supplier; + + openCreateApiForm({ + url: ApiPaths.supplier_part_list, + title: t`Add Supplier Part`, + fields: fields, + onFormSuccess: refreshTable, + successMessage: t`Supplier part created` + }); + }, [params]); + + // Table actions + const tableActions = useMemo(() => { + // TODO: Hide actions based on user permissions + + return [ + // TODO: Refactor this component out to something reusable + + + + + + ]; + }, [user]); + + // Row action callback + const rowActions = useCallback( + (record: any) => { + // TODO: Adjust actions based on user permissions + return [ + RowEditAction({ + onClick: () => { + record.pk && + openEditApiForm({ + url: ApiPaths.supplier_part_list, + pk: record.pk, + title: t`Edit Supplier Part`, + fields: supplierPartFields(), + onFormSuccess: refreshTable, + successMessage: t`Supplier part updated` + }); + } + }), + RowDeleteAction({ + onClick: () => { + record.pk && + openDeleteApiForm({ + url: ApiPaths.supplier_part_list, + pk: record.pk, + title: t`Delete Supplier Part`, + successMessage: t`Supplier part deleted`, + onFormSuccess: refreshTable, + preFormContent: ( + {t`Are you sure you want to remove this supplier part?`} + ) + }); + } + }) + ]; + }, + [user] + ); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx index 3c8a94cbe1..73d6e9174c 100644 --- a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx @@ -1,5 +1,4 @@ import { t } from '@lingui/macro'; -import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -41,10 +40,11 @@ export function ReturnOrderTable({ params }: { params?: any }) { let customer = record.customer_detail ?? {}; return ( - - - {customer?.name} - + ); } }, diff --git a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx index 5f0589a366..505d1f3091 100644 --- a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx @@ -1,5 +1,4 @@ import { t } from '@lingui/macro'; -import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -42,10 +41,11 @@ export function SalesOrderTable({ params }: { params?: any }) { let customer = record.customer_detail ?? {}; return ( - - - {customer?.name} - + ); } }, diff --git a/src/frontend/src/components/tables/settings/CustomUnitsTable.tsx b/src/frontend/src/components/tables/settings/CustomUnitsTable.tsx index a71aeb21bf..ce9a62b32d 100644 --- a/src/frontend/src/components/tables/settings/CustomUnitsTable.tsx +++ b/src/frontend/src/components/tables/settings/CustomUnitsTable.tsx @@ -12,7 +12,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; -import { RowAction } from '../RowActions'; +import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; /** * Table for displaying list of custom physical units @@ -45,11 +45,9 @@ export function CustomUnitsTable() { const rowActions = useCallback((record: any): RowAction[] => { return [ - { - title: t`Edit`, + RowEditAction({ onClick: () => { openEditApiForm({ - name: 'edit-custom-unit', url: ApiPaths.custom_unit_list, pk: record.pk, title: t`Edit custom unit`, @@ -62,12 +60,10 @@ export function CustomUnitsTable() { successMessage: t`Custom unit updated` }); } - }, - { - title: t`Delete`, + }), + RowDeleteAction({ onClick: () => { openDeleteApiForm({ - name: 'delete-custom-unit', url: ApiPaths.custom_unit_list, pk: record.pk, title: t`Delete custom unit`, @@ -78,13 +74,12 @@ export function CustomUnitsTable() { ) }); } - } + }) ]; }, []); const addCustomUnit = useCallback(() => { openCreateApiForm({ - name: 'add-custom-unit', url: ApiPaths.custom_unit_list, title: t`Add custom unit`, fields: { diff --git a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx index 8333f60c6e..794303f7c0 100644 --- a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx +++ b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx @@ -12,7 +12,7 @@ import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; -import { RowAction } from '../RowActions'; +import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; /** * Table for displaying list of project codes @@ -37,11 +37,9 @@ export function ProjectCodeTable() { const rowActions = useCallback((record: any): RowAction[] => { return [ - { - title: t`Edit`, + RowEditAction({ onClick: () => { openEditApiForm({ - name: 'edit-project-code', url: ApiPaths.project_code_list, pk: record.pk, title: t`Edit project code`, @@ -53,13 +51,10 @@ export function ProjectCodeTable() { successMessage: t`Project code updated` }); } - }, - { - title: t`Delete`, - color: 'red', + }), + RowDeleteAction({ onClick: () => { openDeleteApiForm({ - name: 'delete-project-code', url: ApiPaths.project_code_list, pk: record.pk, title: t`Delete project code`, @@ -70,13 +65,12 @@ export function ProjectCodeTable() { ) }); } - } + }) ]; }, []); const addProjectCode = useCallback(() => { openCreateApiForm({ - name: 'add-project-code', url: ApiPaths.project_code_list, title: t`Add project code`, fields: { diff --git a/src/frontend/src/components/tables/stock/StockItemTable.tsx b/src/frontend/src/components/tables/stock/StockItemTable.tsx index ad342c835c..951391bef5 100644 --- a/src/frontend/src/components/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/components/tables/stock/StockItemTable.tsx @@ -3,7 +3,6 @@ import { Group, Stack, Text } from '@mantine/core'; import { ReactNode, 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'; @@ -219,13 +218,7 @@ export function StockItemTable({ params = {} }: { params?: any }) { function stockItemRowActions(record: any): RowAction[] { let actions: RowAction[] = []; - actions.push({ - title: t`Edit`, - onClick: () => { - notYetImplemented(); - } - }); - + // TODO: Custom row actions for stock table return actions; } diff --git a/src/frontend/src/functions/forms/AttachmentForms.tsx b/src/frontend/src/forms/AttachmentForms.tsx similarity index 91% rename from src/frontend/src/functions/forms/AttachmentForms.tsx rename to src/frontend/src/forms/AttachmentForms.tsx index 02d6daf38f..653c4556dc 100644 --- a/src/frontend/src/functions/forms/AttachmentForms.tsx +++ b/src/frontend/src/forms/AttachmentForms.tsx @@ -1,13 +1,13 @@ import { t } from '@lingui/macro'; import { Text } from '@mantine/core'; -import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; -import { ApiPaths } from '../../states/ApiState'; +import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { openCreateApiForm, openDeleteApiForm, openEditApiForm -} from '../forms'; +} from '../functions/forms'; +import { ApiPaths } from '../states/ApiState'; export function attachmentFields(editing: boolean): ApiFormFieldSet { let fields: ApiFormFieldSet = { @@ -59,7 +59,6 @@ export function addAttachment({ let message = attachmentType === 'file' ? t`File added` : t`Link added`; openCreateApiForm({ - name: 'attachment-add', title: title, url: endpoint, successMessage: message, @@ -102,7 +101,6 @@ export function editAttachment({ let message = attachmentType === 'file' ? t`File updated` : t`Link updated`; openEditApiForm({ - name: 'attachment-edit', title: title, url: endpoint, pk: pk, @@ -124,7 +122,6 @@ export function deleteAttachment({ openDeleteApiForm({ url: endpoint, pk: pk, - name: 'attachment-edit', title: t`Delete Attachment`, successMessage: t`Attachment deleted`, onFormSuccess: callback, diff --git a/src/frontend/src/forms/CompanyForms.tsx b/src/frontend/src/forms/CompanyForms.tsx new file mode 100644 index 0000000000..26344c5f3b --- /dev/null +++ b/src/frontend/src/forms/CompanyForms.tsx @@ -0,0 +1,106 @@ +import { t } from '@lingui/macro'; +import { + IconAt, + IconCurrencyDollar, + IconGlobe, + IconHash, + IconLink, + IconNote, + IconPackage, + IconPhone +} from '@tabler/icons-react'; + +import { + ApiFormData, + ApiFormFieldSet +} from '../components/forms/fields/ApiFormField'; +import { openEditApiForm } from '../functions/forms'; +import { ApiPaths } from '../states/ApiState'; + +/** + * Field set for SupplierPart instance + */ +export function supplierPartFields(): ApiFormFieldSet { + return { + part: { + filters: { + purchaseable: true + } + }, + manufacturer_part: { + filters: { + part_detail: true, + manufacturer_detail: true + }, + adjustFilters: (filters: any, form: ApiFormData) => { + let part = form.values.part; + + if (part) { + filters.part = part; + } + + return filters; + } + }, + supplier: {}, + SKU: { + icon: + }, + description: {}, + link: { + icon: + }, + note: { + icon: + }, + pack_quantity: {}, + packaging: { + icon: + } + }; +} + +/** + * Field set for editing a company instance + */ +export function companyFields(): ApiFormFieldSet { + return { + name: {}, + description: {}, + website: { + icon: + }, + currency: { + icon: + }, + phone: { + icon: + }, + email: { + icon: + }, + is_supplier: {}, + is_manufacturer: {}, + is_customer: {} + }; +} + +/** + * Edit a company instance + */ +export function editCompany({ + pk, + callback +}: { + pk: number; + callback?: () => void; +}) { + openEditApiForm({ + title: t`Edit Company`, + url: ApiPaths.company_list, + pk: pk, + fields: companyFields(), + successMessage: t`Company updated`, + onFormSuccess: callback + }); +} diff --git a/src/frontend/src/functions/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx similarity index 90% rename from src/frontend/src/functions/forms/PartForms.tsx rename to src/frontend/src/forms/PartForms.tsx index 00fc4f3d99..49d6b6f5d9 100644 --- a/src/frontend/src/functions/forms/PartForms.tsx +++ b/src/frontend/src/forms/PartForms.tsx @@ -1,8 +1,8 @@ import { t } from '@lingui/macro'; -import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; -import { ApiPaths } from '../../states/ApiState'; -import { openCreateApiForm, openEditApiForm } from '../forms'; +import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; +import { openCreateApiForm, openEditApiForm } from '../functions/forms'; +import { ApiPaths } from '../states/ApiState'; /** * Construct a set of fields for creating / editing a Part instance @@ -70,7 +70,6 @@ export function partFields({ */ export function createPart() { openCreateApiForm({ - name: 'part-create', title: t`Create Part`, url: ApiPaths.part_list, successMessage: t`Part created`, @@ -90,7 +89,6 @@ export function editPart({ callback?: () => void; }) { openEditApiForm({ - name: 'part-edit', title: t`Edit Part`, url: ApiPaths.part_list, pk: part_id, diff --git a/src/frontend/src/functions/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx similarity index 91% rename from src/frontend/src/functions/forms/StockForms.tsx rename to src/frontend/src/forms/StockForms.tsx index 83d00da992..28c6dd1387 100644 --- a/src/frontend/src/functions/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -4,9 +4,9 @@ import { ApiFormChangeCallback, ApiFormData, ApiFormFieldSet -} from '../../components/forms/fields/ApiFormField'; -import { ApiPaths } from '../../states/ApiState'; -import { openCreateApiForm, openEditApiForm } from '../forms'; +} from '../components/forms/fields/ApiFormField'; +import { openCreateApiForm, openEditApiForm } from '../functions/forms'; +import { ApiPaths } from '../states/ApiState'; /** * Construct a set of fields for creating / editing a StockItem instance @@ -30,7 +30,6 @@ export function stockFields({ }, supplier_part: { // TODO: icon - // TODO: implement adjustFilters filters: { part_detail: true, supplier_detail: true @@ -107,7 +106,6 @@ export function stockFields({ */ export function createStockItem() { openCreateApiForm({ - name: 'stockitem-create', url: ApiPaths.stock_item_list, fields: stockFields({ create: true }), title: t`Create Stock Item` @@ -126,7 +124,6 @@ export function editStockItem({ callback?: () => void; }) { openEditApiForm({ - name: 'stockitem-edit', url: ApiPaths.stock_item_list, pk: item_id, fields: stockFields({ create: false }), diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index e2bbf57970..d9fb12a256 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -115,10 +115,12 @@ export function openModalApiForm(props: ApiFormProps) { } // Generate a random modal ID for controller - let modalId: string = `modal-${props.title}-` + generateUniqueId(); + let modalId: string = + `modal-${props.title}-${props.url}-${props.method}` + + generateUniqueId(); modals.open({ - title: {props.title}, + title: {props.title}, modalId: modalId, size: 'xl', onClose: () => { diff --git a/src/frontend/src/functions/forms/CompanyForms.tsx b/src/frontend/src/functions/forms/CompanyForms.tsx deleted file mode 100644 index 84a87554f1..0000000000 --- a/src/frontend/src/functions/forms/CompanyForms.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { t } from '@lingui/macro'; -import { - IconAt, - IconCurrencyDollar, - IconGlobe, - IconPhone -} from '@tabler/icons-react'; - -import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; -import { ApiPaths } from '../../states/ApiState'; -import { openEditApiForm } from '../forms'; - -/** - * Field set for editing a company instance - */ -export function companyFields(): ApiFormFieldSet { - return { - name: {}, - description: {}, - website: { - icon: - }, - currency: { - icon: - }, - phone: { - icon: - }, - email: { - icon: - }, - is_supplier: {}, - is_manufacturer: {}, - is_customer: {} - }; -} - -/** - * Edit a company instance - */ -export function editCompany({ - pk, - callback -}: { - pk: number; - callback?: () => void; -}) { - openEditApiForm({ - name: 'company-edit', - title: t`Edit Company`, - url: ApiPaths.company_list, - pk: pk, - fields: companyFields(), - successMessage: t`Company updated`, - onFormSuccess: callback - }); -} diff --git a/src/frontend/src/pages/Index/Playground.tsx b/src/frontend/src/pages/Index/Playground.tsx index b97bda0110..0fb7ae8fd4 100644 --- a/src/frontend/src/pages/Index/Playground.tsx +++ b/src/frontend/src/pages/Index/Playground.tsx @@ -9,13 +9,13 @@ import { PlaceholderPill } from '../../components/items/Placeholder'; import { StylishText } from '../../components/items/StylishText'; import { ModelType } from '../../components/render/ModelType'; import { StatusRenderer } from '../../components/renderers/StatusRenderer'; -import { openCreateApiForm, openEditApiForm } from '../../functions/forms'; import { createPart, editPart, partCategoryFields -} from '../../functions/forms/PartForms'; -import { createStockItem } from '../../functions/forms/StockForms'; +} from '../../forms/PartForms'; +import { createStockItem } from '../../forms/StockForms'; +import { openCreateApiForm, openEditApiForm } from '../../functions/forms'; import { ApiPaths } from '../../states/ApiState'; // Generate some example forms using the modal API forms interface @@ -23,7 +23,6 @@ function ApiFormsPlayground() { let fields = partCategoryFields({}); const editCategoryForm: ApiFormProps = { - name: 'partcategory', url: ApiPaths.category_list, pk: 2, title: 'Edit Category', @@ -31,7 +30,6 @@ function ApiFormsPlayground() { }; const createAttachmentForm: ApiFormProps = { - name: 'createattachment', url: ApiPaths.part_attachment_list, title: 'Create Attachment', successMessage: 'Attachment uploaded', diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index 304e219d6d..b9305f6223 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -31,7 +31,7 @@ import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; -import { editCompany } from '../../functions/forms/CompanyForms'; +import { editCompany } from '../../forms/CompanyForms'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 2864f0063e..0c37ac0b1c 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,7 +1,9 @@ import { t } from '@lingui/macro'; import { Group, LoadingOverlay, Stack, Text } from '@mantine/core'; import { + IconBookmarks, IconBuilding, + IconBuildingFactory2, IconCalendarStats, IconClipboardList, IconCopy, @@ -44,10 +46,11 @@ import { AttachmentTable } from '../../components/tables/general/AttachmentTable import { PartParameterTable } from '../../components/tables/part/PartParameterTable'; import { PartVariantTable } from '../../components/tables/part/PartVariantTable'; import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; +import { SupplierPartTable } from '../../components/tables/purchasing/SupplierPartTable'; import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; -import { editPart } from '../../functions/forms/PartForms'; +import { editPart } from '../../forms/PartForms'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; @@ -108,6 +111,12 @@ export default function PartDetail() { hidden: !part.is_template, content: }, + { + name: 'allocations', + label: t`Allocations`, + icon: , + hidden: !part.component && !part.salable + }, { name: 'bom', label: t`Bill of Materials`, @@ -119,7 +128,7 @@ export default function PartDetail() { name: 'builds', label: t`Build Orders`, icon: , - hidden: !part.assembly && !part.component, + hidden: !part.assembly, content: ( }, + { + name: 'manufacturers', + label: t`Manufacturers`, + icon: , + hidden: !part.purchaseable + }, { name: 'suppliers', label: t`Suppliers`, icon: , - hidden: !part.purchaseable + hidden: !part.purchaseable, + content: part.pk && ( + + ) }, { name: 'purchase_orders', diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index e02ceb49b5..0b62e26f69 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -35,7 +35,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; -import { editStockItem } from '../../functions/forms/StockForms'; +import { editStockItem } from '../../forms/StockForms'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState';