[PUI] Test template table (#6311)

* Add PartTestTemplateTable

* Update PartTestTemplate API

- Improve filtering options
- Add sorting options
- Add search options

* Update table

* Add placeholders for editing and deleting test templates

* Update calls to DescriptionColumn

* CORS fixes:

- Update CORS headers in settings.py

* Add / edit / delete templates

* Fix for partId
This commit is contained in:
Oliver 2024-01-22 15:49:33 +11:00 committed by GitHub
parent edad000d8e
commit 2fbb8c757f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 239 additions and 53 deletions

View File

@ -367,6 +367,31 @@ class PartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
serializer_class = part_serializers.PartAttachmentSerializer serializer_class = part_serializers.PartAttachmentSerializer
class PartTestTemplateFilter(rest_filters.FilterSet):
"""Custom filterset class for the PartTestTemplateList endpoint."""
class Meta:
"""Metaclass options for this filterset."""
model = PartTestTemplate
fields = ['required', 'requires_value', 'requires_attachment']
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.filter(trackable=True),
label='Part',
field_name='part',
method='filter_part',
)
def filter_part(self, queryset, name, part):
"""Filter by the 'part' field.
Note that for the 'part' field, we also include any parts "above" the specified part.
"""
variants = part.get_ancestors(include_self=True)
return queryset.filter(part__in=variants)
class PartTestTemplateDetail(RetrieveUpdateDestroyAPI): class PartTestTemplateDetail(RetrieveUpdateDestroyAPI):
"""Detail endpoint for PartTestTemplate model.""" """Detail endpoint for PartTestTemplate model."""
@ -375,45 +400,20 @@ class PartTestTemplateDetail(RetrieveUpdateDestroyAPI):
class PartTestTemplateList(ListCreateAPI): class PartTestTemplateList(ListCreateAPI):
"""API endpoint for listing (and creating) a PartTestTemplate. """API endpoint for listing (and creating) a PartTestTemplate."""
TODO: Add filterset class for this view
"""
queryset = PartTestTemplate.objects.all() queryset = PartTestTemplate.objects.all()
serializer_class = part_serializers.PartTestTemplateSerializer serializer_class = part_serializers.PartTestTemplateSerializer
filterset_class = PartTestTemplateFilter
def filter_queryset(self, queryset):
"""Filter the test list queryset.
If filtering by 'part', we include results for any parts "above" the specified part.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
part = params.get('part', None)
# Filter by part
if part:
try:
part = Part.objects.get(pk=part)
queryset = queryset.filter(
part__in=part.get_ancestors(include_self=True)
)
except (ValueError, Part.DoesNotExist):
pass
# Filter by 'required' status
required = params.get('required', None)
if required is not None:
queryset = queryset.filter(required=str2bool(required))
return queryset
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
search_fields = ['test_name', 'description']
ordering_fields = ['test_name', 'required', 'requires_value', 'requires_attachment']
ordering = 'test_name'
class PartThumbs(ListAPI): class PartThumbs(ListAPI):
"""API endpoint for retrieving information on available Part thumbnails.""" """API endpoint for retrieving information on available Part thumbnails."""

View File

@ -20,25 +20,38 @@ export function PartColumn(part: any) {
export function BooleanColumn({ export function BooleanColumn({
accessor, accessor,
title title,
sortable,
switchable
}: { }: {
accessor: string; accessor: string;
title: string; title: string;
sortable?: boolean;
switchable?: boolean;
}): TableColumn { }): TableColumn {
return { return {
accessor: accessor, accessor: accessor,
title: title, title: title,
sortable: true, sortable: sortable ?? true,
switchable: switchable ?? true,
render: (record: any) => <YesNoButton value={record[accessor]} /> render: (record: any) => <YesNoButton value={record[accessor]} />
}; };
} }
export function DescriptionColumn(): TableColumn { export function DescriptionColumn({
accessor,
sortable,
switchable
}: {
accessor?: string;
sortable?: boolean;
switchable?: boolean;
}): TableColumn {
return { return {
accessor: 'description', accessor: accessor ?? 'description',
title: t`Description`, title: t`Description`,
sortable: false, sortable: sortable ?? false,
switchable: true switchable: switchable ?? true
}; };
} }

View File

@ -44,7 +44,7 @@ export function CompanyTable({
); );
} }
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'website', accessor: 'website',
title: t`Website`, title: t`Website`,

View File

@ -27,7 +27,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
sortable: true, sortable: true,
switchable: false switchable: false
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'pathstring', accessor: 'pathstring',
title: t`Path`, title: t`Path`,

View File

@ -57,7 +57,7 @@ export default function PartParameterTemplateTable() {
title: t`Units`, title: t`Units`,
sortable: true sortable: true
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'checkbox', accessor: 'checkbox',
title: t`Checkbox` title: t`Checkbox`

View File

@ -45,7 +45,7 @@ function partTableColumns(): TableColumn[] {
sortable: true, sortable: true,
title: t`Units` title: t`Units`
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'category', accessor: 'category',
title: t`Category`, title: t`Category`,

View File

@ -0,0 +1,149 @@
import { t } from '@lingui/macro';
import { useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { partTestTemplateFields } from '../../../forms/PartForms';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowDeleteAction, RowEditAction } from '../RowActions';
export default function PartTestTemplateTable({ partId }: { partId: number }) {
const table = useTable('part-test-template');
const user = useUserState();
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'test_name',
title: t`Test Name`,
switchable: false,
sortable: true
},
DescriptionColumn({
switchable: false
}),
BooleanColumn({
accessor: 'required',
title: t`Required`
}),
BooleanColumn({
accessor: 'requires_value',
title: t`Requires Value`
}),
BooleanColumn({
accessor: 'requires_attachment',
title: t`Requires Attachment`
})
];
}, []);
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'required',
label: t`Required`,
description: t`Show required tests`
},
{
name: 'requires_value',
label: t`Requires Value`,
description: t`Show tests that require a value`
},
{
name: 'requires_attachment',
label: t`Requires Attachment`,
description: t`Show tests that require an attachment`
}
];
}, []);
const rowActions = useCallback(
(record: any) => {
let can_edit = user.hasChangeRole(UserRoles.part);
let can_delete = user.hasDeleteRole(UserRoles.part);
return [
RowEditAction({
hidden: !can_edit,
onClick: () => {
openEditApiForm({
url: ApiPaths.part_test_template_list,
pk: record.pk,
title: t`Edit Test Template`,
fields: partTestTemplateFields(),
successMessage: t`Template updated`,
onFormSuccess: table.refreshTable
});
}
}),
RowDeleteAction({
hidden: !can_delete,
onClick: () => {
openDeleteApiForm({
url: ApiPaths.part_test_template_list,
pk: record.pk,
title: t`Delete Test Template`,
successMessage: t`Test Template deleted`,
onFormSuccess: table.refreshTable
});
}
})
];
},
[user]
);
const addTestTemplate = useCallback(() => {
let fields = partTestTemplateFields();
fields['part'].value = partId;
openCreateApiForm({
url: ApiPaths.part_test_template_list,
title: t`Create Test Template`,
fields: fields,
successMessage: t`Template created`,
onFormSuccess: table.refreshTable
});
}, [partId]);
const tableActions = useMemo(() => {
let can_add = user.hasAddRole(UserRoles.part);
return [
<AddItemButton
tooltip={t`Add Test Template`}
onClick={addTestTemplate}
disabled={!can_add}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.part_test_template_list)}
tableState={table}
columns={tableColumns}
props={{
params: {
part: partId
},
customFilters: tableFilters,
customActionGroups: tableActions,
rowActions: rowActions
}}
/>
);
}

View File

@ -52,7 +52,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
title: t`Manufacturer Part Number`, title: t`Manufacturer Part Number`,
sortable: true sortable: true
}, },
DescriptionColumn(), DescriptionColumn({}),
LinkColumn() LinkColumn()
]; ];
}, [params]); }, [params]);

View File

@ -63,7 +63,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
switchable: false switchable: false
// TODO: Display extra information if order is overdue // TODO: Display extra information if order is overdue
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'supplier__name', accessor: 'supplier__name',
title: t`Supplier`, title: t`Supplier`,

View File

@ -57,7 +57,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
title: t`Supplier Part`, title: t`Supplier Part`,
sortable: true sortable: true
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'manufacturer', accessor: 'manufacturer',

View File

@ -76,7 +76,7 @@ export function ReturnOrderTable({ params }: { params?: any }) {
accessor: 'customer_reference', accessor: 'customer_reference',
title: t`Customer Reference` title: t`Customer Reference`
}, },
DescriptionColumn(), DescriptionColumn({}),
LineItemsProgressColumn(), LineItemsProgressColumn(),
StatusColumn(ModelType.returnorder), StatusColumn(ModelType.returnorder),
ProjectCodeColumn(), ProjectCodeColumn(),

View File

@ -80,7 +80,7 @@ export function SalesOrderTable({ params }: { params?: any }) {
accessor: 'customer_reference', accessor: 'customer_reference',
title: t`Customer Reference` title: t`Customer Reference`
}, },
DescriptionColumn(), DescriptionColumn({}),
LineItemsProgressColumn(), LineItemsProgressColumn(),
StatusColumn(ModelType.salesorder), StatusColumn(ModelType.salesorder),
ProjectCodeColumn(), ProjectCodeColumn(),

View File

@ -32,7 +32,7 @@ export default function ProjectCodeTable() {
sortable: true, sortable: true,
title: t`Project Code` title: t`Project Code`
}, },
DescriptionColumn(), DescriptionColumn({}),
ResponsibleColumn() ResponsibleColumn()
]; ];
}, []); }, []);

View File

@ -51,7 +51,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
title: t`Name`, title: t`Name`,
switchable: false switchable: false
}, },
DescriptionColumn(), DescriptionColumn({}),
{ {
accessor: 'pathstring', accessor: 'pathstring',
title: t`Path`, title: t`Path`,

View File

@ -58,6 +58,7 @@ export enum ApiPaths {
part_attachment_list = 'api-part-attachment-list', part_attachment_list = 'api-part-attachment-list',
part_parameter_list = 'api-part-parameter-list', part_parameter_list = 'api-part-parameter-list',
part_parameter_template_list = 'api-part-parameter-template-list', part_parameter_template_list = 'api-part-parameter-template-list',
part_test_template_list = 'api-part-test-template-list',
// Company URLs // Company URLs
company_list = 'api-company-list', company_list = 'api-company-list',

View File

@ -164,3 +164,16 @@ export function partParameterTemplateFields(): ApiFormFieldSet {
checkbox: {} checkbox: {}
}; };
} }
export function partTestTemplateFields(): ApiFormFieldSet {
return {
part: {
hidden: true
},
test_name: {},
description: {},
required: {},
requires_value: {},
requires_attachment: {}
};
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Group, LoadingOverlay, Stack, Text } from '@mantine/core'; import { Group, LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
import { import {
IconBookmarks, IconBookmarks,
IconBuilding, IconBuilding,
@ -44,6 +44,7 @@ import { UsedInTable } from '../../components/tables/bom/UsedInTable';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { PartParameterTable } from '../../components/tables/part/PartParameterTable'; import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
import PartTestTemplateTable from '../../components/tables/part/PartTestTemplateTable';
import { PartVariantTable } from '../../components/tables/part/PartVariantTable'; import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
import { ManufacturerPartTable } from '../../components/tables/purchasing/ManufacturerPartTable'; import { ManufacturerPartTable } from '../../components/tables/purchasing/ManufacturerPartTable';
@ -189,12 +190,14 @@ export default function PartDetail() {
label: t`Sales Orders`, label: t`Sales Orders`,
icon: <IconTruckDelivery />, icon: <IconTruckDelivery />,
hidden: !part.salable, hidden: !part.salable,
content: part.pk && ( content: part.pk ? (
<SalesOrderTable <SalesOrderTable
params={{ params={{
part: part.pk ?? -1 part: part.pk ?? -1
}} }}
/> />
) : (
<Skeleton />
) )
}, },
{ {
@ -211,7 +214,12 @@ export default function PartDetail() {
name: 'test_templates', name: 'test_templates',
label: t`Test Templates`, label: t`Test Templates`,
icon: <IconTestPipe />, icon: <IconTestPipe />,
hidden: !part.trackable hidden: !part.trackable,
content: part?.pk ? (
<PartTestTemplateTable partId={part?.pk} />
) : (
<Skeleton />
)
}, },
{ {
name: 'related_parts', name: 'related_parts',

View File

@ -151,6 +151,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'part/related/'; return 'part/related/';
case ApiPaths.part_attachment_list: case ApiPaths.part_attachment_list:
return 'part/attachment/'; return 'part/attachment/';
case ApiPaths.part_test_template_list:
return 'part/test-template/';
case ApiPaths.company_list: case ApiPaths.company_list:
return 'company/'; return 'company/';
case ApiPaths.contact_list: case ApiPaths.contact_list: