From 5abe0eaaad98f4d75994c55bdfad9bb4e1575171 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 9 Nov 2023 21:50:17 +1100 Subject: [PATCH] Parameter table updates (#5892) * Add some helper functions for role permission checks on frontend * Update PartParameterTable - Use new user role checks * Fix up more table action permissions * Add table for part parameter template * Add edit and delete actions to new table * Add ability to create new template from table * Fix for BomTable * Refactor RowActions - Require icon - Horizontal menu popout * Refactor row actions for existing tables * Fix BomTable * Bug fix for notifications table * Fix display of TableHoverCard * Disable PanelGroup tooltip when expanded * Fix unused variables --- .../src/components/nav/PanelGroup.tsx | 1 + .../src/components/tables/RowActions.tsx | 74 +++++++---- .../src/components/tables/TableHoverCard.tsx | 2 +- .../src/components/tables/bom/BomTable.tsx | 28 ++-- .../tables/part/PartParameterTable.tsx | 8 +- .../part/PartParameterTemplateTable.tsx | 121 ++++++++++++++++++ .../tables/part/RelatedPartTable.tsx | 43 ++++--- .../tables/plugin/PluginListTable.tsx | 10 +- .../purchasing/PurchaseOrderLineItemTable.tsx | 19 +-- .../tables/purchasing/SupplierPartTable.tsx | 5 +- .../tables/settings/CustomUnitsTable.tsx | 76 ++++++----- .../tables/settings/ProjectCodeTable.tsx | 74 ++++++----- src/frontend/src/forms/PartForms.tsx | 10 ++ .../pages/Index/Settings/SystemSettings.tsx | 4 +- src/frontend/src/pages/Notifications.tsx | 14 +- .../src/pages/company/CompanyDetail.tsx | 10 +- src/frontend/src/states/UserState.tsx | 46 ++++++- 17 files changed, 390 insertions(+), 155 deletions(-) create mode 100644 src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx diff --git a/src/frontend/src/components/nav/PanelGroup.tsx b/src/frontend/src/components/nav/PanelGroup.tsx index bf532bddeb..9a155b9a77 100644 --- a/src/frontend/src/components/nav/PanelGroup.tsx +++ b/src/frontend/src/components/nav/PanelGroup.tsx @@ -95,6 +95,7 @@ export function PanelGroup({ void; - tooltip?: string; hidden?: boolean; }; @@ -27,6 +27,7 @@ export function RowDuplicateAction({ title: t`Duplicate`, color: 'green', onClick: onClick, + icon: , hidden: hidden }; } @@ -43,6 +44,7 @@ export function RowEditAction({ title: t`Edit`, color: 'blue', onClick: onClick, + icon: , hidden: hidden }; } @@ -59,6 +61,7 @@ export function RowDeleteAction({ title: t`Delete`, color: 'red', onClick: onClick, + icon: , hidden: hidden }; } @@ -82,7 +85,7 @@ export function RowActions({ event?.preventDefault(); event?.stopPropagation(); event?.nativeEvent?.stopImmediatePropagation(); - setOpened(true); + setOpened(!opened); } const [opened, setOpened] = useState(false); @@ -91,11 +94,45 @@ export function RowActions({ return actions.filter((action) => !action.hidden); }, [actions]); + // Render a single action icon + function RowActionIcon(action: RowAction) { + return ( + + { + // Prevent clicking on the action from selecting the row itself + event?.preventDefault(); + event?.stopPropagation(); + event?.nativeEvent?.stopImmediatePropagation(); + + if (action.onClick) { + action.onClick(); + } else { + notYetImplemented(); + } + + setOpened(false); + }} + > + {action.icon} + + + ); + } + + // If only a single action is available, display that + if (visibleActions.length == 1) { + return ; + } + return ( visibleActions.length > 0 && ( @@ -112,28 +149,11 @@ export function RowActions({ - {title || t`Actions`} - {visibleActions.map((action, idx) => ( - { - // Prevent clicking on the action from selecting the row itself - event?.preventDefault(); - event?.stopPropagation(); - event?.nativeEvent?.stopImmediatePropagation(); - if (action.onClick) { - action.onClick(); - } else { - notYetImplemented(); - } - }} - title={action.tooltip || action.title} - > - - {action.title} - - - ))} + + {visibleActions.map((action, _idx) => ( + + ))} + ) diff --git a/src/frontend/src/components/tables/TableHoverCard.tsx b/src/frontend/src/components/tables/TableHoverCard.tsx index b43f134e7a..9975e3eae7 100644 --- a/src/frontend/src/components/tables/TableHoverCard.tsx +++ b/src/frontend/src/components/tables/TableHoverCard.tsx @@ -27,7 +27,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 29cebb4dcd..73ac081edf 100644 --- a/src/frontend/src/components/tables/bom/BomTable.tsx +++ b/src/frontend/src/components/tables/bom/BomTable.tsx @@ -1,5 +1,10 @@ import { t } from '@lingui/macro'; import { Text } from '@mantine/core'; +import { + IconArrowRight, + IconCircleCheck, + IconSwitch3 +} from '@tabler/icons-react'; import { ReactNode, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -7,7 +12,7 @@ import { bomItemFields } from '../../../forms/BomForms'; import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; -import { useUserState } from '../../../states/UserState'; +import { UserRoles, useUserState } from '../../../states/UserState'; import { Thumbnail } from '../../images/Thumbnail'; import { YesNoButton } from '../../items/YesNoButton'; import { TableColumn } from '../Column'; @@ -245,33 +250,34 @@ export function BomTable({ return [ { title: t`View BOM`, - onClick: () => navigate(`/part/${record.part}/`) + onClick: () => navigate(`/part/${record.part}/`), + icon: } ]; } - // TODO: Check user permissions here, - // TODO: to determine which actions are allowed - let actions: RowAction[] = []; // TODO: Enable BomItem validation actions.push({ - title: t`Validate`, - hidden: record.validated || !user.checkUserRole('part', 'change') + title: t`Validate BOM line`, + color: 'green', + hidden: record.validated || !user.hasChangeRole(UserRoles.part), + icon: }); // TODO: Enable editing of substitutes actions.push({ - title: t`Substitutes`, + title: t`Edit Substitutes`, color: 'blue', - hidden: !user.checkUserRole('part', 'change') + hidden: !user.hasChangeRole(UserRoles.part), + icon: }); // Action on edit actions.push( RowEditAction({ - hidden: !user.checkUserRole('part', 'change'), + hidden: !user.hasChangeRole(UserRoles.part), onClick: () => { openEditApiForm({ url: ApiPaths.bom_list, @@ -288,7 +294,7 @@ export function BomTable({ // Action on delete actions.push( RowDeleteAction({ - hidden: !user.checkUserRole('part', 'delete'), + hidden: !user.hasDeleteRole(UserRoles.part), onClick: () => { openDeleteApiForm({ url: ApiPaths.bom_list, diff --git a/src/frontend/src/components/tables/part/PartParameterTable.tsx b/src/frontend/src/components/tables/part/PartParameterTable.tsx index 855f00e763..fe7da2b1d0 100644 --- a/src/frontend/src/components/tables/part/PartParameterTable.tsx +++ b/src/frontend/src/components/tables/part/PartParameterTable.tsx @@ -9,6 +9,7 @@ import { } from '../../../functions/forms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { UserRoles, useUserState } from '../../../states/UserState'; import { AddItemButton } from '../../buttons/AddItemButton'; import { Thumbnail } from '../../images/Thumbnail'; import { YesNoButton } from '../../items/YesNoButton'; @@ -22,6 +23,8 @@ import { RowDeleteAction, RowEditAction } from '../RowActions'; export function PartParameterTable({ partId }: { partId: any }) { const { tableKey, refreshTable } = useTableRefresh('part-parameters'); + const user = useUserState(); + const tableColumns: TableColumn[] = useMemo(() => { return [ { @@ -94,7 +97,6 @@ export function PartParameterTable({ partId }: { partId: any }) { }, [partId]); // Callback for row actions - // TODO: Adjust based on user permissions const rowActions = useCallback( (record: any) => { // Actions not allowed for "variant" rows @@ -106,6 +108,7 @@ export function PartParameterTable({ partId }: { partId: any }) { actions.push( RowEditAction({ + hidden: !user.hasChangeRole(UserRoles.part), onClick: () => { openEditApiForm({ url: ApiPaths.part_parameter_list, @@ -127,6 +130,7 @@ export function PartParameterTable({ partId }: { partId: any }) { actions.push( RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.part), onClick: () => { openDeleteApiForm({ url: ApiPaths.part_parameter_list, @@ -144,7 +148,7 @@ export function PartParameterTable({ partId }: { partId: any }) { return actions; }, - [partId] + [partId, user] ); const addParameter = useCallback(() => { diff --git a/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx b/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx new file mode 100644 index 0000000000..46987253a2 --- /dev/null +++ b/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx @@ -0,0 +1,121 @@ +import { t } from '@lingui/macro'; +import { Text } from '@mantine/core'; +import { useCallback, useMemo } from 'react'; + +import { partParameterTemplateFields } from '../../../forms/PartForms'; +import { + openCreateApiForm, + openDeleteApiForm, + openEditApiForm +} from '../../../functions/forms'; +import { useTableRefresh } from '../../../hooks/TableRefresh'; +import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { UserRoles, useUserState } from '../../../states/UserState'; +import { AddItemButton } from '../../buttons/AddItemButton'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction, RowEditAction } from '../RowActions'; + +export function PartParameterTemplateTable() { + const { tableKey, refreshTable } = useTableRefresh( + 'part-parameter-templates' + ); + + const user = useUserState(); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'name', + title: t`Name`, + sortable: true, + switchable: false + }, + { + accessor: 'units', + title: t`Units`, + sortable: true + }, + { + accessor: 'description', + title: t`Description`, + sortbale: false + }, + { + accessor: 'checkbox', + title: t`Checkbox` + }, + { + accessor: 'choices', + title: t`Choices` + } + ]; + }, []); + + // Callback for row actions + const rowActions = useCallback( + (record: any) => { + return [ + RowEditAction({ + hidden: !user.hasChangeRole(UserRoles.part), + onClick: () => { + openEditApiForm({ + url: ApiPaths.part_parameter_template_list, + pk: record.pk, + title: t`Edit Parameter Template`, + fields: partParameterTemplateFields(), + successMessage: t`Parameter template updated`, + onFormSuccess: refreshTable + }); + } + }), + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.part), + onClick: () => { + openDeleteApiForm({ + url: ApiPaths.part_parameter_template_list, + pk: record.pk, + title: t`Delete Parameter Template`, + successMessage: t`Parameter template deleted`, + onFormSuccess: refreshTable, + preFormContent: {t`Remove parameter template`} + }); + } + }) + ]; + }, + [user] + ); + + const addParameterTemplate = useCallback(() => { + openCreateApiForm({ + url: ApiPaths.part_parameter_template_list, + title: t`Create Parameter Template`, + fields: partParameterTemplateFields(), + successMessage: t`Parameter template created`, + onFormSuccess: refreshTable + }); + }, []); + + const tableActions = useMemo(() => { + return [ + + ]; + }, [user]); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/part/RelatedPartTable.tsx b/src/frontend/src/components/tables/part/RelatedPartTable.tsx index 477be0a5f7..2b76f1e520 100644 --- a/src/frontend/src/components/tables/part/RelatedPartTable.tsx +++ b/src/frontend/src/components/tables/part/RelatedPartTable.tsx @@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom'; import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; +import { UserRoles, useUserState } from '../../../states/UserState'; import { Thumbnail } from '../../images/Thumbnail'; import { TableColumn } from '../Column'; import { InvenTreeTable } from '../InvenTreeTable'; @@ -20,6 +21,8 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { const navigate = useNavigate(); + const user = useUserState(); + // Construct table columns for this table const tableColumns: TableColumn[] = useMemo(() => { function getPart(record: any) { @@ -96,24 +99,28 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode { // Generate row actions // TODO: Hide if user does not have permission to edit parts - const rowActions = useCallback((record: any) => { - return [ - RowDeleteAction({ - onClick: () => { - openDeleteApiForm({ - url: ApiPaths.related_part_list, - pk: record.pk, - title: t`Delete Related Part`, - successMessage: t`Related part deleted`, - preFormContent: ( - {t`Are you sure you want to remove this relationship?`} - ), - onFormSuccess: refreshTable - }); - } - }) - ]; - }, []); + const rowActions = useCallback( + (record: any) => { + return [ + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.part), + onClick: () => { + openDeleteApiForm({ + url: ApiPaths.related_part_list, + pk: record.pk, + title: t`Delete Related Part`, + successMessage: t`Related part deleted`, + preFormContent: ( + {t`Are you sure you want to remove this relationship?`} + ), + onFormSuccess: refreshTable + }); + } + }) + ]; + }, + [user] + ); return ( { - notYetImplemented(); - } + icon: }); } else { actions.push({ title: t`Activate`, - onClick: () => { - notYetImplemented(); - } + color: 'green', + icon: }); } } diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx index c3d2746172..d758b31b71 100644 --- a/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -8,7 +8,7 @@ import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms'; import { openCreateApiForm, openEditApiForm } from '../../../functions/forms'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; -import { useUserState } from '../../../states/UserState'; +import { UserRoles, useUserState } from '../../../states/UserState'; import { ActionButton } from '../../buttons/ActionButton'; import { AddItemButton } from '../../buttons/AddItemButton'; import { Thumbnail } from '../../images/Thumbnail'; @@ -45,18 +45,17 @@ export function PurchaseOrderLineItemTable({ const rowActions = useCallback( (record: any) => { - // TODO: Hide certain actions if user does not have required permissions - let received = (record?.received ?? 0) >= (record?.quantity ?? 0); return [ { hidden: received, - title: t`Receive`, - tooltip: t`Receive line item`, + title: t`Receive line item`, + icon: , color: 'green' }, RowEditAction({ + hidden: !user.hasAddRole(UserRoles.purchase_order), onClick: () => { let supplier = record?.supplier_part_detail?.supplier; @@ -78,8 +77,12 @@ export function PurchaseOrderLineItemTable({ }); } }), - RowDuplicateAction({}), - RowDeleteAction({}) + RowDuplicateAction({ + hidden: !user.hasAddRole(UserRoles.purchase_order) + }), + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.purchase_order) + }) ]; }, [orderId, user] @@ -228,7 +231,7 @@ export function PurchaseOrderLineItemTable({