mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
edad000d8e
commit
2fbb8c757f
@ -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."""
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export function CompanyTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
DescriptionColumn(),
|
DescriptionColumn({}),
|
||||||
{
|
{
|
||||||
accessor: 'website',
|
accessor: 'website',
|
||||||
title: t`Website`,
|
title: t`Website`,
|
||||||
|
@ -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`,
|
||||||
|
@ -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`
|
||||||
|
@ -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`,
|
||||||
|
@ -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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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]);
|
||||||
|
@ -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`,
|
||||||
|
@ -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',
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
|
@ -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(),
|
||||||
|
@ -32,7 +32,7 @@ export default function ProjectCodeTable() {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Project Code`
|
title: t`Project Code`
|
||||||
},
|
},
|
||||||
DescriptionColumn(),
|
DescriptionColumn({}),
|
||||||
ResponsibleColumn()
|
ResponsibleColumn()
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -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`,
|
||||||
|
@ -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',
|
||||||
|
@ -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: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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',
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user