[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
This commit is contained in:
Oliver 2024-03-21 23:05:49 +11:00 committed by GitHub
parent 4eac4902ba
commit e551e2e753
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 202 additions and 7 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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/',

View File

@ -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: <CustomUnitsTable />
},
{
name: 'parameters',
name: 'part-parameters',
label: t`Part Parameters`,
icon: <IconList />,
content: <PartParameterTemplateTable />
},
{
name: 'category-parameters',
label: t`Category Parameters`,
icon: <IconSitemap />,
content: <PartCategoryTemplateTable />
},
{
name: 'templates',
label: t`Templates`,

View File

@ -378,7 +378,7 @@ export function InvenTreeTable<T = any>({
return api
.get(url, {
params: queryParams,
timeout: 30 * 1000
timeout: 5 * 1000
})
.then(function (response) {
switch (response.status) {

View File

@ -184,7 +184,7 @@ export function AddressTable({
<AddItemButton
tooltip={t`Add Address`}
onClick={() => newAddress.open()}
disabled={!can_add}
hidden={!can_add}
/>
];
}, [user]);

View File

@ -130,7 +130,7 @@ export function ContactTable({
<AddItemButton
tooltip={t`Add contact`}
onClick={() => newContact.open()}
disabled={!can_add}
hidden={!can_add}
/>
];
}, [user]);

View File

@ -105,7 +105,7 @@ export function PartCategoryTable({ parentId }: { parentId?: any }) {
<AddItemButton
tooltip={t`Add Part Category`}
onClick={() => newCategory.open()}
disabled={!can_add}
hidden={!can_add}
/>
];
}, [user]);

View File

@ -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<number>(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 (
<Group position="apart" grow>
<Text>{record.default_value}</Text>
{units && <Text size="xs">{units}</Text>}
</Group>
);
}
}
];
}, []);
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 [
<AddItemButton
tooltip={t`Add Category Parameter`}
onClick={() => newTemplate.open()}
hidden={!user.hasAddRole(UserRoles.part)}
/>
];
}, [user]);
return (
<>
{newTemplate.modal}
{editTemplate.modal}
{deleteTemplate.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.category_parameter_list)}
tableState={table}
columns={tableColumns}
props={{
rowActions: rowActions,
tableFilters: tableFilters,
tableActions: tableActions
}}
/>
</>
);
}

View File

@ -134,7 +134,7 @@ export default function PartParameterTemplateTable() {
<AddItemButton
tooltip={t`Add parameter template`}
onClick={() => newTemplate.open()}
disabled={!user.hasAddRole(UserRoles.part)}
hidden={!user.hasAddRole(UserRoles.part)}
/>
];
}, [user]);

View File

@ -190,7 +190,7 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
<AddItemButton
tooltip={t`Add Test Template`}
onClick={() => newTestTemplate.open()}
disabled={!can_add}
hidden={!can_add}
/>
];
}, [user]);