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
label={panel.label}
key={`panel-tab-tooltip-${panel.name}`}
disabled={expanded}
>
<Tabs.Tab
key={`panel-tab-${panel.name}`}

View File

@ -1,7 +1,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 { ActionIcon, Group, Tooltip } from '@mantine/core';
import { Menu } from '@mantine/core';
import { IconCopy, IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react';
import { notYetImplemented } from '../../functions/notifications';
@ -10,8 +10,8 @@ import { notYetImplemented } from '../../functions/notifications';
export type RowAction = {
title: string;
color?: string;
icon: ReactNode;
onClick?: () => void;
tooltip?: string;
hidden?: boolean;
};
@ -27,6 +27,7 @@ export function RowDuplicateAction({
title: t`Duplicate`,
color: 'green',
onClick: onClick,
icon: <IconCopy />,
hidden: hidden
};
}
@ -43,6 +44,7 @@ export function RowEditAction({
title: t`Edit`,
color: 'blue',
onClick: onClick,
icon: <IconEdit />,
hidden: hidden
};
}
@ -59,6 +61,7 @@ export function RowDeleteAction({
title: t`Delete`,
color: 'red',
onClick: onClick,
icon: <IconTrash />,
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 (
<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 (
visibleActions.length > 0 && (
<Menu
withinPortal={true}
disabled={disabled}
position="left"
opened={opened}
onChange={setOpened}
>
@ -112,28 +149,11 @@ export function RowActions({
</Tooltip>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>{title || t`Actions`}</Menu.Label>
{visibleActions.map((action, idx) => (
<Menu.Item
key={idx}
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();
}
}}
title={action.tooltip || action.title}
>
<Text size="xs" color={action.color}>
{action.title}
</Text>
</Menu.Item>
))}
<Group position="right" spacing="xs" p={8}>
{visibleActions.map((action, _idx) => (
<RowActionIcon {...action} />
))}
</Group>
</Menu.Dropdown>
</Menu>
)

View File

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

View File

@ -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: <IconArrowRight />
}
];
}
// 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: <IconCircleCheck />
});
// 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: <IconSwitch3 />
});
// 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,

View File

@ -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(() => {

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 { 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: (
<Text>{t`Are you sure you want to remove this relationship?`}</Text>
),
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: (
<Text>{t`Are you sure you want to remove this relationship?`}</Text>
),
onFormSuccess: refreshTable
});
}
})
];
},
[user]
);
return (
<InvenTreeTable

View File

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

View File

@ -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: <IconSquareArrowRight />,
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({
<AddItemButton
tooltip={t`Add line item`}
onClick={addLine}
hidden={!user?.checkUserRole('purchaseorder', 'add')}
hidden={!user?.hasAddRole(UserRoles.purchase_order)}
/>,
<ActionButton text={t`Receive items`} icon={<IconSquareArrowRight />} />
];

View File

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

View File

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

View File

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

View File

@ -121,3 +121,13 @@ export function partCategoryFields({}: {}): ApiFormFieldSet {
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 { SettingsHeader } from '../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../components/settings/SettingList';
import { PartParameterTemplateTable } from '../../../components/tables/part/PartParameterTemplateTable';
import { CurrencyTable } from '../../../components/tables/settings/CurrencyTable';
import { CustomUnitsTable } from '../../../components/tables/settings/CustomUnitsTable';
import { ProjectCodeTable } from '../../../components/tables/settings/ProjectCodeTable';
@ -220,7 +221,8 @@ export default function SystemSettings() {
{
name: 'parameters',
label: t`Part Parameters`,
icon: <IconList />
icon: <IconList />,
content: <PartParameterTemplateTable />
},
{
name: 'stock',

View File

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

View File

@ -36,7 +36,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editCompany } from '../../forms/CompanyForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { UserRoles, useUserState } from '../../states/UserState';
export type CompanyDetailProps = {
title: string;
@ -161,10 +161,6 @@ export default function CompanyDetail(props: CompanyDetailProps) {
}, [id, company]);
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 [
<ActionDropdown
key="company"
@ -172,7 +168,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
icon={<IconDots />}
actions={[
EditItemAction({
disabled: !canEdit,
disabled: !user.hasChangeRole(UserRoles.purchase_order),
onClick: () => {
if (company?.pk) {
editCompany({
@ -183,7 +179,7 @@ export default function CompanyDetail(props: CompanyDetailProps) {
}
}),
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 { 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 {
user: UserProps | undefined;
username: () => string;
setUser: (newUser: UserProps) => 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);
});
},
checkUserRole: (role: string, permission: string) => {
checkUserRole: (role: UserRoles, permission: UserPermissions) => {
// Check if the user has the specified permission for the specified role
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;
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);
}
}));