2
0
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:
Oliver
2024-02-18 23:26:01 +11:00
committed by GitHub
parent ad1c1ae604
commit 0f51127adf
50 changed files with 1505 additions and 243 deletions

View 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
}
}}
/>
</>
);
}