2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00
Files
InvenTree/src/frontend/src/tables/stock/StockItemTestResultTable.tsx
Oliver 432e0c622c Single table for file attachments (#7420)
* Add basic model for handling generic attachments

* Refactor migration

* Data migration to convert old files across

* Admin updates

* Increase comment field max_length

* Adjust field name

* Remove legacy serializer classes / endpoints

* Expose new model to API

* Admin site list filters

* Remove legacy attachment models

- Add new mixin class to designate which models can have attachments

* Update data migration

- Ensure other apps are at the correct migration state beforehand

* Add migrations to remove legacy attachment tables

* Fix for "rename_attachment" callback

* Refactor model_type field

- ContentType does not allow easy API serialization

* Set allowed options for admin

* Update model verbose names

* Fix logic for file upload

* Add choices for serializer

* Add API filtering

* Fix for API filter

* Fix for attachment tables in PUI

- Still not solved permission issues

* Bump API version

* Record user when uploading attachment via API

* Refactor <AttachmentTable /> for PUI

* Display 'file_size' in PUI attachment table

* Fix company migrations

* Include permission informtion in roles API endpoint

* Read user permissions in PUI

* Simplify permission checks for <AttachmentTable />

* Automatically clean up old content types

* Cleanup PUI

* Fix typo in data migration

* Add reverse data migration

* Update unit tests

* Use InMemoryStorage for media files in test mode

* Data migration unit test

* Fix "model_type" field

- It is a required field after all

* Add permission check for serializer

* Fix permission check for CUI

* Fix PUI import

* Test python lib against specific branch

- Will be reverted once code is merged

* Revert STORAGES setting

- Might be worth looking into again

* Fix part unit test

* Fix unit test for sales order

* Use 'get_global_setting'

* Use 'get_global_setting'

* Update setting getter

* Unit tests

* Tweaks

* Revert change to settings.py

* More updates for get_global_setting

* Relax API query count requirement

* remove illegal chars and add unit tests

* Fix unit tests

* Fix frontend unit tests

* settings management updates

* Prevent db write under more conditions

* Simplify settings code

* Pop values before creating filters

* Prevent settings write under certain conditions

* Add debug msg

* Clear db on record import

* Refactor permissions checks

- Allows extension / customization of permission checks at a later date

* Unit test updates

* Prevent delete of attachment without correct permissions

* Adjust odcker.yaml

* Cleanup data migrations

* Tweak migration tests for build app

* Update data migration

- Handle case with missing data

* Prevent debug shell in TESTING mode

* Update migration dependencies

- Ensure all apps are "up to date" before removing legacy tables

* add file size test

* Update migration tests

* Revert some settings caching changes

* Fix incorrect logic in migration

* Update unit tests

* Prevent create on CURRENCY_CODES

- Seems to play havoc with bootup sequence

* Fix unit test

* Some refactoring

- Use get_global_setting

* Fix typo

* Revert change

* Add "tags" and "metadata"

* Include "tags" field in API serializer

* add "metadata" endpoint for attachments
2024-06-19 14:38:46 +10:00

454 lines
12 KiB
TypeScript

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 { PassFailButton } from '../../components/buttons/YesNoButton';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink';
import { RenderUser } from '../../components/render/User';
import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import { useTestResultFields } from '../../forms/StockForms';
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 { DateColumn, 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,
enabled: 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) => {
const enabled = record.enabled ?? record.template_detail?.enabled;
const installed =
record.stock_item != undefined && record.stock_item != itemId;
return (
<Group justify="space-between" wrap="nowrap">
<Text
style={{ fontStyle: installed ? 'italic' : undefined }}
c={enabled ? undefined : 'red'}
>
{!record.templateId && '- '}
{record.test_name ?? record.template_detail?.test_name}
</Text>
<Group justify="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({}),
DateColumn({}),
{
accessor: 'user',
title: t`User`,
sortable: false,
render: (record: any) =>
record.user_detail && <RenderUser instance={record.user_detail} />
},
{
accessor: 'test_station',
sortable: true,
title: t`Test station`
},
{
accessor: 'started_datetime',
sortable: true,
title: t`Started`,
render: (record: any) => {
return (
<Group justify="space-between">
{formatDate(record.started_datetime, {
showTime: true,
showSeconds: true
})}
</Group>
);
}
},
{
accessor: 'finished_datetime',
sortable: true,
title: t`Finished`,
render: (record: any) => {
return (
<Group justify="space-between">
{formatDate(record.finished_datetime, {
showTime: true,
showSeconds: true
})}
</Group>
);
}
}
];
}, [itemId]);
const resultFields: ApiFormFieldSet = useTestResultFields({
partId: partId,
itemId: itemId
});
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
undefined
);
const newTestModal = useCreateApiFormModal({
url: ApiEndpoints.stock_test_result_list,
fields: useMemo(() => ({ ...resultFields }), [resultFields]),
initialData: {
template: selectedTemplate,
result: true
},
title: t`Add Test Result`,
table: table,
successMessage: t`Test result added`
});
const [selectedTest, setSelectedTest] = useState<number>(0);
const editTestModal = useEditApiFormModal({
url: ApiEndpoints.stock_test_result_list,
pk: selectedTest,
fields: useMemo(() => ({ ...resultFields }), [resultFields]),
title: t`Edit Test Result`,
table: table,
successMessage: t`Test result updated`
});
const deleteTestModal = useDeleteApiFormModal({
url: ApiEndpoints.stock_test_result_list,
pk: selectedTest,
title: t`Delete Test Result`,
table: table,
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'
});
})
.catch(() => {
showNotification({
title: t`Error`,
message: t`Failed to record test result`,
color: 'red'
});
});
},
[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,
enabled: true
}
}}
/>
</>
);
}