diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b658577936..b819ffee73 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -367,6 +367,31 @@ class PartAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): 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): """Detail endpoint for PartTestTemplate model.""" @@ -375,45 +400,20 @@ class PartTestTemplateDetail(RetrieveUpdateDestroyAPI): class PartTestTemplateList(ListCreateAPI): - """API endpoint for listing (and creating) a PartTestTemplate. - - TODO: Add filterset class for this view - """ + """API endpoint for listing (and creating) a PartTestTemplate.""" queryset = PartTestTemplate.objects.all() serializer_class = part_serializers.PartTestTemplateSerializer - - 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 + filterset_class = PartTestTemplateFilter 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): """API endpoint for retrieving information on available Part thumbnails.""" diff --git a/src/frontend/src/components/tables/ColumnRenderers.tsx b/src/frontend/src/components/tables/ColumnRenderers.tsx index 0913f87efa..560614f07e 100644 --- a/src/frontend/src/components/tables/ColumnRenderers.tsx +++ b/src/frontend/src/components/tables/ColumnRenderers.tsx @@ -20,25 +20,38 @@ export function PartColumn(part: any) { export function BooleanColumn({ accessor, - title + title, + sortable, + switchable }: { accessor: string; title: string; + sortable?: boolean; + switchable?: boolean; }): TableColumn { return { accessor: accessor, title: title, - sortable: true, + sortable: sortable ?? true, + switchable: switchable ?? true, render: (record: any) => }; } -export function DescriptionColumn(): TableColumn { +export function DescriptionColumn({ + accessor, + sortable, + switchable +}: { + accessor?: string; + sortable?: boolean; + switchable?: boolean; +}): TableColumn { return { - accessor: 'description', + accessor: accessor ?? 'description', title: t`Description`, - sortable: false, - switchable: true + sortable: sortable ?? false, + switchable: switchable ?? true }; } diff --git a/src/frontend/src/components/tables/company/CompanyTable.tsx b/src/frontend/src/components/tables/company/CompanyTable.tsx index 12ca29fa84..a096497fce 100644 --- a/src/frontend/src/components/tables/company/CompanyTable.tsx +++ b/src/frontend/src/components/tables/company/CompanyTable.tsx @@ -44,7 +44,7 @@ export function CompanyTable({ ); } }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'website', title: t`Website`, diff --git a/src/frontend/src/components/tables/part/PartCategoryTable.tsx b/src/frontend/src/components/tables/part/PartCategoryTable.tsx index 8ec22999c0..78b984014b 100644 --- a/src/frontend/src/components/tables/part/PartCategoryTable.tsx +++ b/src/frontend/src/components/tables/part/PartCategoryTable.tsx @@ -27,7 +27,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) { sortable: true, switchable: false }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'pathstring', title: t`Path`, diff --git a/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx b/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx index de3fbff064..2e7df5db99 100644 --- a/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx +++ b/src/frontend/src/components/tables/part/PartParameterTemplateTable.tsx @@ -57,7 +57,7 @@ export default function PartParameterTemplateTable() { title: t`Units`, sortable: true }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'checkbox', title: t`Checkbox` diff --git a/src/frontend/src/components/tables/part/PartTable.tsx b/src/frontend/src/components/tables/part/PartTable.tsx index 0e0bfe0a2a..9f612e4c11 100644 --- a/src/frontend/src/components/tables/part/PartTable.tsx +++ b/src/frontend/src/components/tables/part/PartTable.tsx @@ -45,7 +45,7 @@ function partTableColumns(): TableColumn[] { sortable: true, title: t`Units` }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'category', title: t`Category`, diff --git a/src/frontend/src/components/tables/part/PartTestTemplateTable.tsx b/src/frontend/src/components/tables/part/PartTestTemplateTable.tsx new file mode 100644 index 0000000000..488b502cd8 --- /dev/null +++ b/src/frontend/src/components/tables/part/PartTestTemplateTable.tsx @@ -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 [ + + ]; + }, [user]); + + return ( + + ); +} diff --git a/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx index f5c5c56265..17276977ba 100644 --- a/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx +++ b/src/frontend/src/components/tables/purchasing/ManufacturerPartTable.tsx @@ -52,7 +52,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode { title: t`Manufacturer Part Number`, sortable: true }, - DescriptionColumn(), + DescriptionColumn({}), LinkColumn() ]; }, [params]); diff --git a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx index 0d9739508a..4582dbec13 100644 --- a/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/components/tables/purchasing/PurchaseOrderTable.tsx @@ -63,7 +63,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) { switchable: false // TODO: Display extra information if order is overdue }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'supplier__name', title: t`Supplier`, diff --git a/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx index b8456df01e..af8000378e 100644 --- a/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx +++ b/src/frontend/src/components/tables/purchasing/SupplierPartTable.tsx @@ -57,7 +57,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode { title: t`Supplier Part`, sortable: true }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'manufacturer', diff --git a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx index 2b5b05dbe3..6d0b7420dc 100644 --- a/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/ReturnOrderTable.tsx @@ -76,7 +76,7 @@ export function ReturnOrderTable({ params }: { params?: any }) { accessor: 'customer_reference', title: t`Customer Reference` }, - DescriptionColumn(), + DescriptionColumn({}), LineItemsProgressColumn(), StatusColumn(ModelType.returnorder), ProjectCodeColumn(), diff --git a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx index b6bd435d17..f510c8c5e2 100644 --- a/src/frontend/src/components/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/components/tables/sales/SalesOrderTable.tsx @@ -80,7 +80,7 @@ export function SalesOrderTable({ params }: { params?: any }) { accessor: 'customer_reference', title: t`Customer Reference` }, - DescriptionColumn(), + DescriptionColumn({}), LineItemsProgressColumn(), StatusColumn(ModelType.salesorder), ProjectCodeColumn(), diff --git a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx index e2f3e9dc64..9afadbe99e 100644 --- a/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx +++ b/src/frontend/src/components/tables/settings/ProjectCodeTable.tsx @@ -32,7 +32,7 @@ export default function ProjectCodeTable() { sortable: true, title: t`Project Code` }, - DescriptionColumn(), + DescriptionColumn({}), ResponsibleColumn() ]; }, []); diff --git a/src/frontend/src/components/tables/stock/StockLocationTable.tsx b/src/frontend/src/components/tables/stock/StockLocationTable.tsx index 66513cbcbc..285f6d917a 100644 --- a/src/frontend/src/components/tables/stock/StockLocationTable.tsx +++ b/src/frontend/src/components/tables/stock/StockLocationTable.tsx @@ -51,7 +51,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) { title: t`Name`, switchable: false }, - DescriptionColumn(), + DescriptionColumn({}), { accessor: 'pathstring', title: t`Path`, diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 071d9c6f8c..d5f676c73a 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -58,6 +58,7 @@ export enum ApiPaths { part_attachment_list = 'api-part-attachment-list', part_parameter_list = 'api-part-parameter-list', part_parameter_template_list = 'api-part-parameter-template-list', + part_test_template_list = 'api-part-test-template-list', // Company URLs company_list = 'api-company-list', diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx index 0a46a2c8de..d3f1475993 100644 --- a/src/frontend/src/forms/PartForms.tsx +++ b/src/frontend/src/forms/PartForms.tsx @@ -164,3 +164,16 @@ export function partParameterTemplateFields(): ApiFormFieldSet { checkbox: {} }; } + +export function partTestTemplateFields(): ApiFormFieldSet { + return { + part: { + hidden: true + }, + test_name: {}, + description: {}, + required: {}, + requires_value: {}, + requires_attachment: {} + }; +} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 6ef88f759f..98427f74a6 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Group, LoadingOverlay, Stack, Text } from '@mantine/core'; +import { Group, LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core'; import { IconBookmarks, IconBuilding, @@ -44,6 +44,7 @@ import { UsedInTable } from '../../components/tables/bom/UsedInTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { PartParameterTable } from '../../components/tables/part/PartParameterTable'; +import PartTestTemplateTable from '../../components/tables/part/PartTestTemplateTable'; import { PartVariantTable } from '../../components/tables/part/PartVariantTable'; import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable'; import { ManufacturerPartTable } from '../../components/tables/purchasing/ManufacturerPartTable'; @@ -189,12 +190,14 @@ export default function PartDetail() { label: t`Sales Orders`, icon: , hidden: !part.salable, - content: part.pk && ( + content: part.pk ? ( + ) : ( + ) }, { @@ -211,7 +214,12 @@ export default function PartDetail() { name: 'test_templates', label: t`Test Templates`, icon: , - hidden: !part.trackable + hidden: !part.trackable, + content: part?.pk ? ( + + ) : ( + + ) }, { name: 'related_parts', diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 0508242d2a..473e84bb6d 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -151,6 +151,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'part/related/'; case ApiPaths.part_attachment_list: return 'part/attachment/'; + case ApiPaths.part_test_template_list: + return 'part/test-template/'; case ApiPaths.company_list: return 'company/'; case ApiPaths.contact_list: