From e551e2e75307417abb2e919104a93646a33b6820 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 21 Mar 2024 23:05:49 +1100 Subject: [PATCH] [PUI] Category params (#6767) * Validate default value field for PartCategoryParameterTemplate - Only if unit checks are enforced - Only if default value is not blank * Add basic table for part category parameter templates * Add functions to create / edit / delete via table * Fix unit testing --- InvenTree/part/models.py | 22 +++ InvenTree/part/test_api.py | 5 + src/frontend/src/enums/ApiEndpoints.tsx | 1 + .../Index/Settings/AdminCenter/Index.tsx | 13 +- src/frontend/src/tables/InvenTreeTable.tsx | 2 +- .../src/tables/company/AddressTable.tsx | 2 +- .../src/tables/company/ContactTable.tsx | 2 +- .../src/tables/part/PartCategoryTable.tsx | 2 +- .../tables/part/PartCategoryTemplateTable.tsx | 156 ++++++++++++++++++ .../part/PartParameterTemplateTable.tsx | 2 +- .../src/tables/part/PartTestTemplateTable.tsx | 2 +- 11 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 src/frontend/src/tables/part/PartCategoryTemplateTable.tsx diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b20eff7453..2f2b34a409 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -3833,6 +3833,28 @@ class PartCategoryParameterTemplate(InvenTree.models.InvenTreeMetadataModel): return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}' return f'{self.category.name} | {self.parameter_template.name}' + def clean(self): + """Validate this PartCategoryParameterTemplate instance. + + Checks the provided 'default_value', and (if not blank), ensure it is valid. + """ + super().clean() + + self.default_value = ( + '' if self.default_value is None else str(self.default_value.strip()) + ) + + if self.default_value and InvenTreeSetting.get_setting( + 'PART_PARAMETER_ENFORCE_UNITS', True, cache=False, create=False + ): + if self.parameter_template.units: + try: + InvenTree.conversion.convert_physical_value( + self.default_value, self.parameter_template.units + ) + except ValidationError as e: + raise ValidationError({'default_value': e.message}) + category = models.ForeignKey( PartCategory, on_delete=models.CASCADE, diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 4862750ae8..216d424066 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -196,6 +196,11 @@ class PartCategoryAPITest(InvenTreeAPITestCase): # Add some more category templates via the API n = PartParameterTemplate.objects.count() + # Ensure validation of parameter values is disabled for these checks + InvenTreeSetting.set_setting( + 'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None + ) + for template in PartParameterTemplate.objects.all(): response = self.post( url, diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 5a35fa87cd..f0910099da 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -63,6 +63,7 @@ export enum ApiEndpoints { part_stocktake_list = 'part/stocktake/', category_list = 'part/category/', category_tree = 'part/category/tree/', + category_parameter_list = 'part/category/parameters/', related_part_list = 'part/related/', part_attachment_list = 'part/attachment/', part_test_template_list = 'part/test-template/', diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 67becb3fc9..7298f76b92 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -9,6 +9,7 @@ import { IconListDetails, IconPlugConnected, IconScale, + IconSitemap, IconTemplate, IconUsersGroup } from '@tabler/icons-react'; @@ -52,6 +53,10 @@ const PartParameterTemplateTable = Loadable( lazy(() => import('../../../../tables/part/PartParameterTemplateTable')) ); +const PartCategoryTemplateTable = Loadable( + lazy(() => import('../../../../tables/part/PartCategoryTemplateTable')) +); + const CurrencyTable = Loadable( lazy(() => import('../../../../tables/settings/CurrencyTable')) ); @@ -106,11 +111,17 @@ export default function AdminCenter() { content: }, { - name: 'parameters', + name: 'part-parameters', label: t`Part Parameters`, icon: , content: }, + { + name: 'category-parameters', + label: t`Category Parameters`, + icon: , + content: + }, { name: 'templates', label: t`Templates`, diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 16fc9c8df3..f27299bc82 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -378,7 +378,7 @@ export function InvenTreeTable({ return api .get(url, { params: queryParams, - timeout: 30 * 1000 + timeout: 5 * 1000 }) .then(function (response) { switch (response.status) { diff --git a/src/frontend/src/tables/company/AddressTable.tsx b/src/frontend/src/tables/company/AddressTable.tsx index 8a5d8fc598..96af3c7d9f 100644 --- a/src/frontend/src/tables/company/AddressTable.tsx +++ b/src/frontend/src/tables/company/AddressTable.tsx @@ -184,7 +184,7 @@ export function AddressTable({ newAddress.open()} - disabled={!can_add} + hidden={!can_add} /> ]; }, [user]); diff --git a/src/frontend/src/tables/company/ContactTable.tsx b/src/frontend/src/tables/company/ContactTable.tsx index f5a5ed7938..6641131e39 100644 --- a/src/frontend/src/tables/company/ContactTable.tsx +++ b/src/frontend/src/tables/company/ContactTable.tsx @@ -130,7 +130,7 @@ export function ContactTable({ newContact.open()} - disabled={!can_add} + hidden={!can_add} /> ]; }, [user]); diff --git a/src/frontend/src/tables/part/PartCategoryTable.tsx b/src/frontend/src/tables/part/PartCategoryTable.tsx index 7b698e72c3..46cbe72b2f 100644 --- a/src/frontend/src/tables/part/PartCategoryTable.tsx +++ b/src/frontend/src/tables/part/PartCategoryTable.tsx @@ -105,7 +105,7 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) { newCategory.open()} - disabled={!can_add} + hidden={!can_add} /> ]; }, [user]); diff --git a/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx new file mode 100644 index 0000000000..9987cc509c --- /dev/null +++ b/src/frontend/src/tables/part/PartCategoryTemplateTable.tsx @@ -0,0 +1,156 @@ +import { t } from '@lingui/macro'; +import { Group, Text } from '@mantine/core'; +import { useCallback, useMemo, useState } from 'react'; +import { set } from 'react-hook-form'; + +import { AddItemButton } from '../../components/buttons/AddItemButton'; +import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { UserRoles } from '../../enums/Roles'; +import { + useCreateApiFormModal, + useDeleteApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { TableFilter } from '../Filter'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowDeleteAction, RowEditAction } from '../RowActions'; + +export default function PartCategoryTemplateTable({}: {}) { + const table = useTable('part-category-parameter-templates'); + const user = useUserState(); + + const formFields: ApiFormFieldSet = useMemo(() => { + return { + category: {}, + parameter_template: {}, + default_value: {} + }; + }, []); + + const [selectedTemplate, setSelectedTemplate] = useState(0); + + const newTemplate = useCreateApiFormModal({ + url: ApiEndpoints.category_parameter_list, + title: t`Add Category Parameter`, + fields: formFields, + onFormSuccess: table.refreshTable + }); + + const editTemplate = useEditApiFormModal({ + url: ApiEndpoints.category_parameter_list, + pk: selectedTemplate, + title: t`Edit Category Parameter`, + fields: formFields, + onFormSuccess: (record: any) => table.updateRecord(record) + }); + + const deleteTemplate = useDeleteApiFormModal({ + url: ApiEndpoints.category_parameter_list, + pk: selectedTemplate, + title: t`Delete Category Parameter`, + onFormSuccess: table.refreshTable + }); + + const tableFilters: TableFilter[] = useMemo(() => { + // TODO + return []; + }, []); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'category_detail.name', + title: t`Category`, + sortable: true, + switchable: false + }, + { + accessor: 'category_detail.pathstring' + }, + { + accessor: 'parameter_template_detail.name', + title: t`Parameter Template`, + sortable: true, + switchable: false + }, + { + accessor: 'default_value', + sortable: true, + switchable: false, + render: (record: any) => { + if (!record?.default_value) { + return '-'; + } + + let units = ''; + + if (record?.parameter_template_detail?.units) { + units = t`[${record.parameter_template_detail.units}]`; + } + + return ( + + {record.default_value} + {units && {units}} + + ); + } + } + ]; + }, []); + + const rowActions = useCallback( + (record: any) => { + return [ + RowEditAction({ + hidden: !user.hasChangeRole(UserRoles.part), + onClick: () => { + setSelectedTemplate(record.pk); + editTemplate.open(); + } + }), + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.part), + onClick: () => { + setSelectedTemplate(record.pk); + deleteTemplate.open(); + } + }) + ]; + }, + [user] + ); + + const tableActions = useMemo(() => { + return [ + newTemplate.open()} + hidden={!user.hasAddRole(UserRoles.part)} + /> + ]; + }, [user]); + + return ( + <> + {newTemplate.modal} + {editTemplate.modal} + {deleteTemplate.modal} + + + ); +} diff --git a/src/frontend/src/tables/part/PartParameterTemplateTable.tsx b/src/frontend/src/tables/part/PartParameterTemplateTable.tsx index 13d0fbe13a..0442eb36de 100644 --- a/src/frontend/src/tables/part/PartParameterTemplateTable.tsx +++ b/src/frontend/src/tables/part/PartParameterTemplateTable.tsx @@ -134,7 +134,7 @@ export default function PartParameterTemplateTable() { newTemplate.open()} - disabled={!user.hasAddRole(UserRoles.part)} + hidden={!user.hasAddRole(UserRoles.part)} /> ]; }, [user]); diff --git a/src/frontend/src/tables/part/PartTestTemplateTable.tsx b/src/frontend/src/tables/part/PartTestTemplateTable.tsx index ebd20c08fb..411dca9e60 100644 --- a/src/frontend/src/tables/part/PartTestTemplateTable.tsx +++ b/src/frontend/src/tables/part/PartTestTemplateTable.tsx @@ -190,7 +190,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) { newTestTemplate.open()} - disabled={!can_add} + hidden={!can_add} /> ]; }, [user]);