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
This commit is contained in:
Oliver 2023-11-09 21:50:17 +11:00 committed by GitHub
parent 0597ea9216
commit 5abe0eaaad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 390 additions and 155 deletions

View File

@ -95,6 +95,7 @@ export function PanelGroup({
<Tooltip <Tooltip
label={panel.label} label={panel.label}
key={`panel-tab-tooltip-${panel.name}`} key={`panel-tab-tooltip-${panel.name}`}
disabled={expanded}
> >
<Tabs.Tab <Tabs.Tab
key={`panel-tab-${panel.name}`} key={`panel-tab-${panel.name}`}

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ActionIcon, Tooltip } from '@mantine/core'; import { ActionIcon, Group, Tooltip } from '@mantine/core';
import { Menu, Text } from '@mantine/core'; import { Menu } from '@mantine/core';
import { IconDots } from '@tabler/icons-react'; import { IconCopy, IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
@ -10,8 +10,8 @@ import { notYetImplemented } from '../../functions/notifications';
export type RowAction = { export type RowAction = {
title: string; title: string;
color?: string; color?: string;
icon: ReactNode;
onClick?: () => void; onClick?: () => void;
tooltip?: string;
hidden?: boolean; hidden?: boolean;
}; };
@ -27,6 +27,7 @@ export function RowDuplicateAction({
title: t`Duplicate`, title: t`Duplicate`,
color: 'green', color: 'green',
onClick: onClick, onClick: onClick,
icon: <IconCopy />,
hidden: hidden hidden: hidden
}; };
} }
@ -43,6 +44,7 @@ export function RowEditAction({
title: t`Edit`, title: t`Edit`,
color: 'blue', color: 'blue',
onClick: onClick, onClick: onClick,
icon: <IconEdit />,
hidden: hidden hidden: hidden
}; };
} }
@ -59,6 +61,7 @@ export function RowDeleteAction({
title: t`Delete`, title: t`Delete`,
color: 'red', color: 'red',
onClick: onClick, onClick: onClick,
icon: <IconTrash />,
hidden: hidden hidden: hidden
}; };
} }
@ -82,7 +85,7 @@ export function RowActions({
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();
event?.nativeEvent?.stopImmediatePropagation(); event?.nativeEvent?.stopImmediatePropagation();
setOpened(true); setOpened(!opened);
} }
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
@ -91,11 +94,45 @@ export function RowActions({
return actions.filter((action) => !action.hidden); return actions.filter((action) => !action.hidden);
}, [actions]); }, [actions]);
// Render a single action icon
function RowActionIcon(action: RowAction) {
return (
<Tooltip withinPortal={true} label={action.title} key={action.title}>
<ActionIcon
color={action.color}
size={20}
onClick={(event) => {
// 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}
</ActionIcon>
</Tooltip>
);
}
// If only a single action is available, display that
if (visibleActions.length == 1) {
return <RowActionIcon {...visibleActions[0]} />;
}
return ( return (
visibleActions.length > 0 && ( visibleActions.length > 0 && (
<Menu <Menu
withinPortal={true} withinPortal={true}
disabled={disabled} disabled={disabled}
position="left"
opened={opened} opened={opened}
onChange={setOpened} onChange={setOpened}
> >
@ -112,28 +149,11 @@ export function RowActions({
</Tooltip> </Tooltip>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Label>{title || t`Actions`}</Menu.Label> <Group position="right" spacing="xs" p={8}>
{visibleActions.map((action, idx) => ( {visibleActions.map((action, _idx) => (
<Menu.Item <RowActionIcon {...action} />
key={idx} ))}
onClick={(event) => { </Group>
// 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}
>
<Text size="xs" color={action.color}>
{action.title}
</Text>
</Menu.Item>
))}
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
) )

View File

@ -27,7 +27,7 @@ export function TableHoverCard({
} }
return ( return (
<HoverCard> <HoverCard withinPortal={true}>
<HoverCard.Target> <HoverCard.Target>
<Group spacing="xs" position="apart" noWrap={true}> <Group spacing="xs" position="apart" noWrap={true}>
{value} {value}

View File

@ -1,5 +1,10 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import {
IconArrowRight,
IconCircleCheck,
IconSwitch3
} from '@tabler/icons-react';
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -7,7 +12,7 @@ import { bomItemFields } from '../../../forms/BomForms';
import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms'; import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { UserRoles, useUserState } from '../../../states/UserState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -245,33 +250,34 @@ export function BomTable({
return [ return [
{ {
title: t`View BOM`, title: t`View BOM`,
onClick: () => navigate(`/part/${record.part}/`) onClick: () => navigate(`/part/${record.part}/`),
icon: <IconArrowRight />
} }
]; ];
} }
// TODO: Check user permissions here,
// TODO: to determine which actions are allowed
let actions: RowAction[] = []; let actions: RowAction[] = [];
// TODO: Enable BomItem validation // TODO: Enable BomItem validation
actions.push({ actions.push({
title: t`Validate`, title: t`Validate BOM line`,
hidden: record.validated || !user.checkUserRole('part', 'change') color: 'green',
hidden: record.validated || !user.hasChangeRole(UserRoles.part),
icon: <IconCircleCheck />
}); });
// TODO: Enable editing of substitutes // TODO: Enable editing of substitutes
actions.push({ actions.push({
title: t`Substitutes`, title: t`Edit Substitutes`,
color: 'blue', color: 'blue',
hidden: !user.checkUserRole('part', 'change') hidden: !user.hasChangeRole(UserRoles.part),
icon: <IconSwitch3 />
}); });
// Action on edit // Action on edit
actions.push( actions.push(
RowEditAction({ RowEditAction({
hidden: !user.checkUserRole('part', 'change'), hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => { onClick: () => {
openEditApiForm({ openEditApiForm({
url: ApiPaths.bom_list, url: ApiPaths.bom_list,
@ -288,7 +294,7 @@ export function BomTable({
// Action on delete // Action on delete
actions.push( actions.push(
RowDeleteAction({ RowDeleteAction({
hidden: !user.checkUserRole('part', 'delete'), hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => { onClick: () => {
openDeleteApiForm({ openDeleteApiForm({
url: ApiPaths.bom_list, url: ApiPaths.bom_list,

View File

@ -9,6 +9,7 @@ import {
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
@ -22,6 +23,8 @@ import { RowDeleteAction, RowEditAction } from '../RowActions';
export function PartParameterTable({ partId }: { partId: any }) { export function PartParameterTable({ partId }: { partId: any }) {
const { tableKey, refreshTable } = useTableRefresh('part-parameters'); const { tableKey, refreshTable } = useTableRefresh('part-parameters');
const user = useUserState();
const tableColumns: TableColumn[] = useMemo(() => { const tableColumns: TableColumn[] = useMemo(() => {
return [ return [
{ {
@ -94,7 +97,6 @@ export function PartParameterTable({ partId }: { partId: any }) {
}, [partId]); }, [partId]);
// Callback for row actions // Callback for row actions
// TODO: Adjust based on user permissions
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
// Actions not allowed for "variant" rows // Actions not allowed for "variant" rows
@ -106,6 +108,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
actions.push( actions.push(
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.part),
onClick: () => { onClick: () => {
openEditApiForm({ openEditApiForm({
url: ApiPaths.part_parameter_list, url: ApiPaths.part_parameter_list,
@ -127,6 +130,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
actions.push( actions.push(
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.part),
onClick: () => { onClick: () => {
openDeleteApiForm({ openDeleteApiForm({
url: ApiPaths.part_parameter_list, url: ApiPaths.part_parameter_list,
@ -144,7 +148,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
return actions; return actions;
}, },
[partId] [partId, user]
); );
const addParameter = useCallback(() => { const addParameter = useCallback(() => {

View File

@ -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: <Text>{t`Remove parameter template`}</Text>
});
}
})
];
},
[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 [
<AddItemButton
tooltip={t`Add parameter template`}
onClick={addParameterTemplate}
disabled={!user.hasAddRole(UserRoles.part)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.part_parameter_template_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
rowActions: rowActions,
customActionGroups: tableActions
}}
/>
);
}

View File

@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -20,6 +21,8 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState();
// Construct table columns for this table // Construct table columns for this table
const tableColumns: TableColumn[] = useMemo(() => { const tableColumns: TableColumn[] = useMemo(() => {
function getPart(record: any) { function getPart(record: any) {
@ -96,24 +99,28 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
// Generate row actions // Generate row actions
// TODO: Hide if user does not have permission to edit parts // TODO: Hide if user does not have permission to edit parts
const rowActions = useCallback((record: any) => { const rowActions = useCallback(
return [ (record: any) => {
RowDeleteAction({ return [
onClick: () => { RowDeleteAction({
openDeleteApiForm({ hidden: !user.hasDeleteRole(UserRoles.part),
url: ApiPaths.related_part_list, onClick: () => {
pk: record.pk, openDeleteApiForm({
title: t`Delete Related Part`, url: ApiPaths.related_part_list,
successMessage: t`Related part deleted`, pk: record.pk,
preFormContent: ( title: t`Delete Related Part`,
<Text>{t`Are you sure you want to remove this relationship?`}</Text> successMessage: t`Related part deleted`,
), preFormContent: (
onFormSuccess: refreshTable <Text>{t`Are you sure you want to remove this relationship?`}</Text>
}); ),
} onFormSuccess: refreshTable
}) });
]; }
}, []); })
];
},
[user]
);
return ( return (
<InvenTreeTable <InvenTreeTable

View File

@ -7,7 +7,6 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -102,16 +101,13 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
actions.push({ actions.push({
title: t`Deactivate`, title: t`Deactivate`,
color: 'red', color: 'red',
onClick: () => { icon: <IconCircleX />
notYetImplemented();
}
}); });
} else { } else {
actions.push({ actions.push({
title: t`Activate`, title: t`Activate`,
onClick: () => { color: 'green',
notYetImplemented(); icon: <IconCircleCheck />
}
}); });
} }
} }

View File

@ -8,7 +8,7 @@ import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms'; import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { UserRoles, useUserState } from '../../../states/UserState';
import { ActionButton } from '../../buttons/ActionButton'; import { ActionButton } from '../../buttons/ActionButton';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
@ -45,18 +45,17 @@ export function PurchaseOrderLineItemTable({
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
// TODO: Hide certain actions if user does not have required permissions
let received = (record?.received ?? 0) >= (record?.quantity ?? 0); let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
return [ return [
{ {
hidden: received, hidden: received,
title: t`Receive`, title: t`Receive line item`,
tooltip: t`Receive line item`, icon: <IconSquareArrowRight />,
color: 'green' color: 'green'
}, },
RowEditAction({ RowEditAction({
hidden: !user.hasAddRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
let supplier = record?.supplier_part_detail?.supplier; let supplier = record?.supplier_part_detail?.supplier;
@ -78,8 +77,12 @@ export function PurchaseOrderLineItemTable({
}); });
} }
}), }),
RowDuplicateAction({}), RowDuplicateAction({
RowDeleteAction({}) hidden: !user.hasAddRole(UserRoles.purchase_order)
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order)
})
]; ];
}, },
[orderId, user] [orderId, user]
@ -228,7 +231,7 @@ export function PurchaseOrderLineItemTable({
<AddItemButton <AddItemButton
tooltip={t`Add line item`} tooltip={t`Add line item`}
onClick={addLine} onClick={addLine}
hidden={!user?.checkUserRole('purchaseorder', 'add')} hidden={!user?.hasAddRole(UserRoles.purchase_order)}
/>, />,
<ActionButton text={t`Receive items`} icon={<IconSquareArrowRight />} /> <ActionButton text={t`Receive items`} icon={<IconSquareArrowRight />} />
]; ];

View File

@ -10,7 +10,7 @@ import {
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { UserRoles, useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -180,9 +180,9 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
// Row action callback // Row action callback
const rowActions = useCallback( const rowActions = useCallback(
(record: any) => { (record: any) => {
// TODO: Adjust actions based on user permissions
return [ return [
RowEditAction({ RowEditAction({
hidden: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && record.pk &&
openEditApiForm({ openEditApiForm({
@ -196,6 +196,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
} }
}), }),
RowDeleteAction({ RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
record.pk && record.pk &&
openDeleteApiForm({ openDeleteApiForm({

View File

@ -9,6 +9,7 @@ import {
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
@ -20,6 +21,8 @@ import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
export function CustomUnitsTable() { export function CustomUnitsTable() {
const { tableKey, refreshTable } = useTableRefresh('custom-units'); const { tableKey, refreshTable } = useTableRefresh('custom-units');
const user = useUserState();
const columns: TableColumn[] = useMemo(() => { const columns: TableColumn[] = useMemo(() => {
return [ return [
{ {
@ -43,40 +46,45 @@ export function CustomUnitsTable() {
]; ];
}, []); }, []);
const rowActions = useCallback((record: any): RowAction[] => { const rowActions = useCallback(
return [ (record: any): RowAction[] => {
RowEditAction({ return [
onClick: () => { RowEditAction({
openEditApiForm({ hidden: !user.hasChangeRole(UserRoles.admin),
url: ApiPaths.custom_unit_list, onClick: () => {
pk: record.pk, openEditApiForm({
title: t`Edit custom unit`, url: ApiPaths.custom_unit_list,
fields: { pk: record.pk,
name: {}, title: t`Edit custom unit`,
definition: {}, fields: {
symbol: {} name: {},
}, definition: {},
onFormSuccess: refreshTable, symbol: {}
successMessage: t`Custom unit updated` },
}); onFormSuccess: refreshTable,
} successMessage: t`Custom unit updated`
}), });
RowDeleteAction({ }
onClick: () => { }),
openDeleteApiForm({ RowDeleteAction({
url: ApiPaths.custom_unit_list, hidden: !user.hasDeleteRole(UserRoles.admin),
pk: record.pk, onClick: () => {
title: t`Delete custom unit`, openDeleteApiForm({
successMessage: t`Custom unit deleted`, url: ApiPaths.custom_unit_list,
onFormSuccess: refreshTable, pk: record.pk,
preFormContent: ( title: t`Delete custom unit`,
<Text>{t`Are you sure you want to remove this custom unit?`}</Text> successMessage: t`Custom unit deleted`,
) onFormSuccess: refreshTable,
}); preFormContent: (
} <Text>{t`Are you sure you want to remove this custom unit?`}</Text>
}) )
]; });
}, []); }
})
];
},
[user]
);
const addCustomUnit = useCallback(() => { const addCustomUnit = useCallback(() => {
openCreateApiForm({ openCreateApiForm({

View File

@ -9,6 +9,7 @@ import {
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';
@ -21,6 +22,8 @@ import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
export function ProjectCodeTable() { export function ProjectCodeTable() {
const { tableKey, refreshTable } = useTableRefresh('project-code'); const { tableKey, refreshTable } = useTableRefresh('project-code');
const user = useUserState();
const columns: TableColumn[] = useMemo(() => { const columns: TableColumn[] = useMemo(() => {
return [ return [
{ {
@ -32,39 +35,44 @@ export function ProjectCodeTable() {
]; ];
}, []); }, []);
const rowActions = useCallback((record: any): RowAction[] => { const rowActions = useCallback(
return [ (record: any): RowAction[] => {
RowEditAction({ return [
onClick: () => { RowEditAction({
openEditApiForm({ hidden: !user.hasChangeRole(UserRoles.admin),
url: ApiPaths.project_code_list, onClick: () => {
pk: record.pk, openEditApiForm({
title: t`Edit project code`, url: ApiPaths.project_code_list,
fields: { pk: record.pk,
code: {}, title: t`Edit project code`,
description: {} fields: {
}, code: {},
onFormSuccess: refreshTable, description: {}
successMessage: t`Project code updated` },
}); onFormSuccess: refreshTable,
} successMessage: t`Project code updated`
}), });
RowDeleteAction({ }
onClick: () => { }),
openDeleteApiForm({ RowDeleteAction({
url: ApiPaths.project_code_list, hidden: !user.hasDeleteRole(UserRoles.admin),
pk: record.pk, onClick: () => {
title: t`Delete project code`, openDeleteApiForm({
successMessage: t`Project code deleted`, url: ApiPaths.project_code_list,
onFormSuccess: refreshTable, pk: record.pk,
preFormContent: ( title: t`Delete project code`,
<Text>{t`Are you sure you want to remove this project code?`}</Text> successMessage: t`Project code deleted`,
) onFormSuccess: refreshTable,
}); preFormContent: (
} <Text>{t`Are you sure you want to remove this project code?`}</Text>
}) )
]; });
}, []); }
})
];
},
[user]
);
const addProjectCode = useCallback(() => { const addProjectCode = useCallback(() => {
openCreateApiForm({ openCreateApiForm({

View File

@ -121,3 +121,13 @@ export function partCategoryFields({}: {}): ApiFormFieldSet {
return fields; return fields;
} }
export function partParameterTemplateFields(): ApiFormFieldSet {
return {
name: {},
description: {},
units: {},
choices: {},
checkbox: {}
};
}

View File

@ -25,6 +25,7 @@ import { StylishText } from '../../../components/items/StylishText';
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../components/settings/SettingList'; import { GlobalSettingList } from '../../../components/settings/SettingList';
import { PartParameterTemplateTable } from '../../../components/tables/part/PartParameterTemplateTable';
import { CurrencyTable } from '../../../components/tables/settings/CurrencyTable'; import { CurrencyTable } from '../../../components/tables/settings/CurrencyTable';
import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable'; import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable';
import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable'; import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable';
@ -220,7 +221,8 @@ export default function SystemSettings() {
{ {
name: 'parameters', name: 'parameters',
label: t`Part Parameters`, label: t`Part Parameters`,
icon: <IconList /> icon: <IconList />,
content: <PartParameterTemplateTable />
}, },
{ {
name: 'stock', name: 'stock',

View File

@ -1,6 +1,12 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import { IconBellCheck, IconBellExclamation } from '@tabler/icons-react'; import {
IconBellCheck,
IconBellExclamation,
IconCircleCheck,
IconCircleX,
IconTrash
} from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { api } from '../App'; import { api } from '../App';
@ -27,6 +33,8 @@ export default function NotificationsPage() {
actions={(record) => [ actions={(record) => [
{ {
title: t`Mark as read`, title: t`Mark as read`,
color: 'green',
icon: <IconCircleCheck />,
onClick: () => { onClick: () => {
let url = apiUrl(ApiPaths.notifications_list, record.pk); let url = apiUrl(ApiPaths.notifications_list, record.pk);
api api
@ -53,6 +61,7 @@ export default function NotificationsPage() {
actions={(record) => [ actions={(record) => [
{ {
title: t`Mark as unread`, title: t`Mark as unread`,
icon: <IconCircleX />,
onClick: () => { onClick: () => {
let url = apiUrl(ApiPaths.notifications_list, record.pk); let url = apiUrl(ApiPaths.notifications_list, record.pk);
@ -68,9 +77,10 @@ export default function NotificationsPage() {
{ {
title: t`Delete`, title: t`Delete`,
color: 'red', color: 'red',
icon: <IconTrash />,
onClick: () => { onClick: () => {
api api
.delete(`/notifications/${record.pk}/`) .delete(apiUrl(ApiPaths.notifications_list, record.pk))
.then((response) => { .then((response) => {
historyRefresh.refreshTable(); historyRefresh.refreshTable();
}); });

View File

@ -36,7 +36,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editCompany } from '../../forms/CompanyForms'; import { editCompany } from '../../forms/CompanyForms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { UserRoles, useUserState } from '../../states/UserState';
export type CompanyDetailProps = { export type CompanyDetailProps = {
title: string; title: string;
@ -161,10 +161,6 @@ export default function CompanyDetail(props: CompanyDetailProps) {
}, [id, company]); }, [id, company]);
const companyActions = useMemo(() => { const companyActions = useMemo(() => {
// TODO: Finer fidelity on these permissions, perhaps?
let canEdit = user.checkUserRole('purchase_order', 'change');
let canDelete = user.checkUserRole('purchase_order', 'delete');
return [ return [
<ActionDropdown <ActionDropdown
key="company" key="company"
@ -172,7 +168,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
icon={<IconDots />} icon={<IconDots />}
actions={[ actions={[
EditItemAction({ EditItemAction({
disabled: !canEdit, disabled: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => { onClick: () => {
if (company?.pk) { if (company?.pk) {
editCompany({ editCompany({
@ -183,7 +179,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
} }
}), }),
DeleteItemAction({ DeleteItemAction({
disabled: !canDelete disabled: !user.hasDeleteRole(UserRoles.purchase_order)
}) })
]} ]}
/> />

View File

@ -5,12 +5,42 @@ import { doClassicLogout } from '../functions/auth';
import { ApiPaths, apiUrl } from './ApiState'; import { ApiPaths, apiUrl } from './ApiState';
import { UserProps } from './states'; import { UserProps } from './states';
/*
* Enumeration of available user role groups
*/
export enum UserRoles {
admin = 'admin',
build = 'build',
part = 'part',
part_category = 'part_category',
purchase_order = 'purchase_order',
return_order = 'return_order',
sales_order = 'sales_order',
stock = 'stock',
stock_location = 'stocklocation',
stocktake = 'stocktake'
}
/*
* Enumeration of available user permissions within each role group
*/
export enum UserPermissions {
view = 'view',
add = 'add',
change = 'change',
delete = 'delete'
}
interface UserStateProps { interface UserStateProps {
user: UserProps | undefined; user: UserProps | undefined;
username: () => string; username: () => string;
setUser: (newUser: UserProps) => void; setUser: (newUser: UserProps) => void;
fetchUserState: () => void; fetchUserState: () => void;
checkUserRole: (role: string, permission: string) => boolean; checkUserRole: (role: UserRoles, permission: UserPermissions) => boolean;
hasDeleteRole: (role: UserRoles) => boolean;
hasChangeRole: (role: UserRoles) => boolean;
hasAddRole: (role: UserRoles) => boolean;
hasViewRole: (role: UserRoles) => boolean;
} }
/** /**
@ -65,7 +95,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
console.error('Error fetching user roles:', error); console.error('Error fetching user roles:', error);
}); });
}, },
checkUserRole: (role: string, permission: string) => { checkUserRole: (role: UserRoles, permission: UserPermissions) => {
// Check if the user has the specified permission for the specified role // Check if the user has the specified permission for the specified role
const user: UserProps = get().user as UserProps; const user: UserProps = get().user as UserProps;
@ -74,5 +104,17 @@ export const useUserState = create<UserStateProps>((set, get) => ({
if (user.roles[role] === undefined) return false; if (user.roles[role] === undefined) return false;
return user.roles[role].includes(permission); return user.roles[role].includes(permission);
},
hasDeleteRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.delete);
},
hasChangeRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.change);
},
hasAddRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.add);
},
hasViewRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.view);
} }
})); }));