mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	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:
		@@ -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}`}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) => {
 | 
					 | 
				
			||||||
                // 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>
 | 
				
			||||||
        </Menu.Dropdown>
 | 
					        </Menu.Dropdown>
 | 
				
			||||||
      </Menu>
 | 
					      </Menu>
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(() => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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,9 +99,11 @@ 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(
 | 
				
			||||||
 | 
					    (record: any) => {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        RowDeleteAction({
 | 
					        RowDeleteAction({
 | 
				
			||||||
 | 
					          hidden: !user.hasDeleteRole(UserRoles.part),
 | 
				
			||||||
          onClick: () => {
 | 
					          onClick: () => {
 | 
				
			||||||
            openDeleteApiForm({
 | 
					            openDeleteApiForm({
 | 
				
			||||||
              url: ApiPaths.related_part_list,
 | 
					              url: ApiPaths.related_part_list,
 | 
				
			||||||
@@ -113,7 +118,9 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
  }, []);
 | 
					    },
 | 
				
			||||||
 | 
					    [user]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <InvenTreeTable
 | 
					    <InvenTreeTable
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 />
 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 />} />
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,9 +46,11 @@ export function CustomUnitsTable() {
 | 
				
			|||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const rowActions = useCallback((record: any): RowAction[] => {
 | 
					  const rowActions = useCallback(
 | 
				
			||||||
 | 
					    (record: any): RowAction[] => {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        RowEditAction({
 | 
					        RowEditAction({
 | 
				
			||||||
 | 
					          hidden: !user.hasChangeRole(UserRoles.admin),
 | 
				
			||||||
          onClick: () => {
 | 
					          onClick: () => {
 | 
				
			||||||
            openEditApiForm({
 | 
					            openEditApiForm({
 | 
				
			||||||
              url: ApiPaths.custom_unit_list,
 | 
					              url: ApiPaths.custom_unit_list,
 | 
				
			||||||
@@ -62,6 +67,7 @@ export function CustomUnitsTable() {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        RowDeleteAction({
 | 
					        RowDeleteAction({
 | 
				
			||||||
 | 
					          hidden: !user.hasDeleteRole(UserRoles.admin),
 | 
				
			||||||
          onClick: () => {
 | 
					          onClick: () => {
 | 
				
			||||||
            openDeleteApiForm({
 | 
					            openDeleteApiForm({
 | 
				
			||||||
              url: ApiPaths.custom_unit_list,
 | 
					              url: ApiPaths.custom_unit_list,
 | 
				
			||||||
@@ -76,7 +82,9 @@ export function CustomUnitsTable() {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
  }, []);
 | 
					    },
 | 
				
			||||||
 | 
					    [user]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const addCustomUnit = useCallback(() => {
 | 
					  const addCustomUnit = useCallback(() => {
 | 
				
			||||||
    openCreateApiForm({
 | 
					    openCreateApiForm({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,9 +35,11 @@ export function ProjectCodeTable() {
 | 
				
			|||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const rowActions = useCallback((record: any): RowAction[] => {
 | 
					  const rowActions = useCallback(
 | 
				
			||||||
 | 
					    (record: any): RowAction[] => {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        RowEditAction({
 | 
					        RowEditAction({
 | 
				
			||||||
 | 
					          hidden: !user.hasChangeRole(UserRoles.admin),
 | 
				
			||||||
          onClick: () => {
 | 
					          onClick: () => {
 | 
				
			||||||
            openEditApiForm({
 | 
					            openEditApiForm({
 | 
				
			||||||
              url: ApiPaths.project_code_list,
 | 
					              url: ApiPaths.project_code_list,
 | 
				
			||||||
@@ -50,6 +55,7 @@ export function ProjectCodeTable() {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        RowDeleteAction({
 | 
					        RowDeleteAction({
 | 
				
			||||||
 | 
					          hidden: !user.hasDeleteRole(UserRoles.admin),
 | 
				
			||||||
          onClick: () => {
 | 
					          onClick: () => {
 | 
				
			||||||
            openDeleteApiForm({
 | 
					            openDeleteApiForm({
 | 
				
			||||||
              url: ApiPaths.project_code_list,
 | 
					              url: ApiPaths.project_code_list,
 | 
				
			||||||
@@ -64,7 +70,9 @@ export function ProjectCodeTable() {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
  }, []);
 | 
					    },
 | 
				
			||||||
 | 
					    [user]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const addProjectCode = useCallback(() => {
 | 
					  const addProjectCode = useCallback(() => {
 | 
				
			||||||
    openCreateApiForm({
 | 
					    openCreateApiForm({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -121,3 +121,13 @@ export function partCategoryFields({}: {}): ApiFormFieldSet {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return fields;
 | 
					  return fields;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function partParameterTemplateFields(): ApiFormFieldSet {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    name: {},
 | 
				
			||||||
 | 
					    description: {},
 | 
				
			||||||
 | 
					    units: {},
 | 
				
			||||||
 | 
					    choices: {},
 | 
				
			||||||
 | 
					    checkbox: {}
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user