mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-16 17:56:30 +00:00
[WIP] Test result table (#6430)
* Add basic table for stock item test results * Improve custom data formatter callback * Custom data formatter for returned results * Update YesNoButton functionality - Add PassFailButton with custom text * Enhancements for stock item test result table - Render all data * Add placeholder row actions * Fix table link * Add option to filter parttesttemplate table by "inherited" * Navigate through to parent part * Update PartTestTemplate model - Save 'key' value to database - Update whenever model is saved - Custom data migration * Custom migration step in tasks.py - Add custom management command - Wraps migration step in maintenance mode * Improve uniqueness validation for PartTestTemplate * Add 'template' field to StockItemTestResult - Links to a PartTestTemplate instance - Add migrations to link existing PartTestTemplates * Add "results" count to PartTestTemplate API - Include in rendered tables * Add 'results' column to test result table - Allow filtering too * Update serializer for StockItemTestResult - Include template information - Update CUI and PUI tables * Control template_detail field with query params * Update ref in api_version.py * Update data migration - Ensure new template is created for top level assembly * Fix admin integration * Update StockItemTestResult table - Remove 'test' field - Make 'template' field non-nullable - Previous data migrations should have accounted for this * Implement "legacy" API support - Create test result by providing test name - Lookup existing template * PUI: Cleanup table * Update tasks.py - Exclude temporary settings when exporting data * Fix unique validation check * Remove duplicate code * CUI: Fix data rendering * More refactoring of PUI table * More fixes for PUI table * Get row expansion working (kinda) * Improve rendering of subtable * More PUI updates: - Edit existing results - Add new results * allow delete of test result * Fix typo * Updates for admin integration * Unit tests for stock migrations * Added migration test for PartTestTemplate * Fix for AttachmentTable - Rebuild actions when permissions are recalculated * Update test fixtures * Add ModelType information * Fix TableState * Fix dataFormatter type def * Improve table rendering * Correctly filter "edit" and "delete" buttons * Loosen requirements for dataFormatter * Fixtures for report tests * Better API filtering for StocokItemTestResult list - Add Filter class - Add option for filtering against legacy "name" data * Cleanup API filter * Fix unit tests * Further unit test fixes * Include test results for installed stock items * Improve rendering of test result table * Fix filtering for getTestResults * More unit test fixes * Fix more unit tests * FIx part unit test * More fixes * More unit test fixes * Rebuild stock item trees when merging * Helper function for adding a test result to a stock item * Set init fix * Code cleanup * Cleanup unused variables * Add docs and more unit tests * Update build unit test
This commit is contained in:
428
src/frontend/src/tables/stock/StockItemTestResultTable.tsx
Normal file
428
src/frontend/src/tables/stock/StockItemTestResultTable.tsx
Normal file
@@ -0,0 +1,428 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconCircleCheck,
|
||||
IconCirclePlus,
|
||||
IconInfoCircle
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { DataTable } from 'mantine-datatable';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import { AttachmentLink } from '../../components/items/AttachmentLink';
|
||||
import { PassFailButton } from '../../components/items/YesNoButton';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { renderDate } from '../../defaults/formatters';
|
||||
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 { DescriptionColumn, NoteColumn } from '../ColumnRenderers';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowActions, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
|
||||
export default function StockItemTestResultTable({
|
||||
partId,
|
||||
itemId
|
||||
}: {
|
||||
partId: number;
|
||||
itemId: number;
|
||||
}) {
|
||||
const user = useUserState();
|
||||
const table = useTable('stocktests');
|
||||
|
||||
// Fetch the test templates required for this stock item
|
||||
const { data: testTemplates } = useQuery({
|
||||
queryKey: ['stocktesttemplates', partId, itemId],
|
||||
queryFn: async () => {
|
||||
if (!partId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.part_test_template_list), {
|
||||
params: {
|
||||
part: partId,
|
||||
include_inherited: true
|
||||
}
|
||||
})
|
||||
.then((response) => response.data)
|
||||
.catch((_error) => []);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
table.refreshTable();
|
||||
}, [testTemplates]);
|
||||
|
||||
// Format the test results based on the returned data
|
||||
const formatRecords = useCallback(
|
||||
(records: any[]): any[] => {
|
||||
// Construct a list of test templates
|
||||
let results = testTemplates.map((template: any) => {
|
||||
return {
|
||||
...template,
|
||||
templateId: template.pk,
|
||||
results: []
|
||||
};
|
||||
});
|
||||
|
||||
// If any of the tests results point to templates which we do not have, add them in
|
||||
records.forEach((record) => {
|
||||
if (!results.find((r: any) => r.templateId == record.template)) {
|
||||
results.push({
|
||||
...record.template_detail,
|
||||
templateId: record.template,
|
||||
results: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Iterate through the returned records
|
||||
// Note that the results are sorted by oldest first,
|
||||
// to ensure that the most recent result is displayed "on top"
|
||||
records
|
||||
.sort((a: any, b: any) => {
|
||||
return a.pk > b.pk ? 1 : -1;
|
||||
})
|
||||
.forEach((record) => {
|
||||
// Find matching template
|
||||
let idx = results.findIndex(
|
||||
(r: any) => r.templateId == record.template
|
||||
);
|
||||
if (idx >= 0) {
|
||||
results[idx] = {
|
||||
...results[idx],
|
||||
...record
|
||||
};
|
||||
|
||||
results[idx].results.push(record);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
[partId, itemId, testTemplates]
|
||||
);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'test',
|
||||
title: t`Test`,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
let required = record.required ?? record.template_detail?.required;
|
||||
let installed =
|
||||
record.stock_item != undefined && record.stock_item != itemId;
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<Text italic={installed} fw={required && 700}>
|
||||
{!record.templateId && '- '}
|
||||
{record.test_name ?? record.template_detail?.test_name}
|
||||
</Text>
|
||||
<Group position="right">
|
||||
{record.results && record.results.length > 1 && (
|
||||
<Tooltip label={t`Test Results`}>
|
||||
<Badge color="lightblue" variant="filled">
|
||||
{record.results.length}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{installed && (
|
||||
<Tooltip label={t`Test result for installed stock item`}>
|
||||
<IconInfoCircle size={16} color="blue" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'result',
|
||||
title: t`Result`,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
if (record.result === undefined) {
|
||||
return (
|
||||
<Badge color="lightblue" variant="filled">{t`No Result`}</Badge>
|
||||
);
|
||||
} else {
|
||||
return <PassFailButton value={record.result} />;
|
||||
}
|
||||
}
|
||||
},
|
||||
DescriptionColumn({
|
||||
accessor: 'description'
|
||||
}),
|
||||
{
|
||||
accessor: 'value',
|
||||
title: t`Value`
|
||||
},
|
||||
{
|
||||
accessor: 'attachment',
|
||||
title: t`Attachment`,
|
||||
render: (record: any) =>
|
||||
record.attachment && <AttachmentLink attachment={record.attachment} />
|
||||
},
|
||||
NoteColumn(),
|
||||
{
|
||||
accessor: 'date',
|
||||
sortable: true,
|
||||
title: t`Date`,
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Group position="apart">
|
||||
{renderDate(record.date)}
|
||||
{record.user_detail && (
|
||||
<RenderUser instance={record.user_detail} />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}, [itemId]);
|
||||
|
||||
const resultFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
template: {
|
||||
filters: {
|
||||
include_inherited: true,
|
||||
part: partId
|
||||
}
|
||||
},
|
||||
result: {},
|
||||
value: {},
|
||||
attachment: {},
|
||||
notes: {},
|
||||
stock_item: {
|
||||
value: itemId,
|
||||
hidden: true
|
||||
}
|
||||
};
|
||||
}, [partId, itemId]);
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const newTestModal = useCreateApiFormModal({
|
||||
url: ApiEndpoints.stock_test_result_list,
|
||||
fields: resultFields,
|
||||
initialData: {
|
||||
template: selectedTemplate,
|
||||
result: true
|
||||
},
|
||||
title: t`Add Test Result`,
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result added`
|
||||
});
|
||||
|
||||
const [selectedTest, setSelectedTest] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const editTestModal = useEditApiFormModal({
|
||||
url: ApiEndpoints.stock_test_result_list,
|
||||
pk: selectedTest,
|
||||
fields: resultFields,
|
||||
title: t`Edit Test Result`,
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result updated`
|
||||
});
|
||||
|
||||
const deleteTestModal = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.stock_test_result_list,
|
||||
pk: selectedTest,
|
||||
title: t`Delete Test Result`,
|
||||
onFormSuccess: () => table.refreshTable(),
|
||||
successMessage: t`Test result deleted`
|
||||
});
|
||||
|
||||
const passTest = useCallback(
|
||||
(templateId: number) => {
|
||||
api
|
||||
.post(apiUrl(ApiEndpoints.stock_test_result_list), {
|
||||
template: templateId,
|
||||
stock_item: itemId,
|
||||
result: true
|
||||
})
|
||||
.then(() => {
|
||||
table.refreshTable();
|
||||
showNotification({
|
||||
title: t`Test Passed`,
|
||||
message: t`Test result has been recorded`,
|
||||
color: 'green'
|
||||
});
|
||||
});
|
||||
},
|
||||
[itemId]
|
||||
);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
if (record.stock_item != undefined && record.stock_item != itemId) {
|
||||
// Test results for other stock items cannot be edited
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: t`Pass Test`,
|
||||
color: 'green',
|
||||
icon: <IconCircleCheck />,
|
||||
hidden:
|
||||
!record.templateId ||
|
||||
record?.requires_attachment ||
|
||||
record?.requires_value ||
|
||||
record.result,
|
||||
onClick: () => passTest(record.templateId)
|
||||
},
|
||||
{
|
||||
title: t`Add`,
|
||||
tooltip: t`Add Test Result`,
|
||||
color: 'green',
|
||||
icon: <IconCirclePlus />,
|
||||
hidden: !user.hasAddRole(UserRoles.stock) || !record.templateId,
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record.templateId);
|
||||
newTestModal.open();
|
||||
}
|
||||
},
|
||||
RowEditAction({
|
||||
tooltip: t`Edit Test Result`,
|
||||
hidden:
|
||||
!user.hasChangeRole(UserRoles.stock) || !record.template_detail,
|
||||
onClick: () => {
|
||||
setSelectedTest(record.pk);
|
||||
editTestModal.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
tooltip: t`Delete Test Result`,
|
||||
hidden:
|
||||
!user.hasDeleteRole(UserRoles.stock) || !record.template_detail,
|
||||
onClick: () => {
|
||||
setSelectedTest(record.pk);
|
||||
deleteTestModal.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user, itemId]
|
||||
);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'required',
|
||||
label: t`Required`,
|
||||
description: t`Show results for required tests`
|
||||
},
|
||||
{
|
||||
name: 'include_installed',
|
||||
label: t`Include Installed`,
|
||||
description: t`Show results for installed stock items`
|
||||
},
|
||||
{
|
||||
name: 'result',
|
||||
label: t`Passed`,
|
||||
description: t`Show only passed tests`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
tooltip={t`Add Test Result`}
|
||||
onClick={() => {
|
||||
setSelectedTemplate(undefined);
|
||||
newTestModal.open();
|
||||
}}
|
||||
hidden={!user.hasAddRole(UserRoles.stock)}
|
||||
/>
|
||||
];
|
||||
}, [user]);
|
||||
|
||||
// Row expansion controller
|
||||
const rowExpansion: any = useMemo(() => {
|
||||
const cols: any = [
|
||||
...tableColumns,
|
||||
{
|
||||
accessor: 'actions',
|
||||
title: ' ',
|
||||
hidden: false,
|
||||
switchable: false,
|
||||
width: 50,
|
||||
render: (record: any) => (
|
||||
<RowActions actions={rowActions(record) ?? []} />
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
allowMultiple: true,
|
||||
content: ({ record }: { record: any }) => {
|
||||
if (!record || !record.results || record.results.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = record?.results ?? [];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
key={record.pk}
|
||||
noHeader
|
||||
columns={cols}
|
||||
records={results.slice(0, -1)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newTestModal.modal}
|
||||
{editTestModal.modal}
|
||||
{deleteTestModal.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_test_result_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
dataFormatter: formatRecords,
|
||||
enablePagination: false,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
rowActions: rowActions,
|
||||
rowExpansion: rowExpansion,
|
||||
params: {
|
||||
stock_item: itemId,
|
||||
user_detail: true,
|
||||
attachment_detail: true,
|
||||
template_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user