mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-13 22:21:37 +00:00
Bulk add test results (#10146)
* Bulk creation of test results - Add BulkCreateMixin class - Add frontend support * Refactor test result serializer - Allow lookup by template name * Updated unit test * Add unit tests * Add row actions * Docs * Fix failing tests * Bump API version * Fix playwright tests
This commit is contained in:
@@ -356,12 +356,7 @@ export function ApiForm({
|
||||
|
||||
let hasFiles = false;
|
||||
|
||||
// Optionally pre-process the data before submitting it
|
||||
if (props.processFormData) {
|
||||
data = props.processFormData(data, form);
|
||||
}
|
||||
|
||||
const jsonData = { ...data };
|
||||
let jsonData = { ...data };
|
||||
const formData = new FormData();
|
||||
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
@@ -397,6 +392,11 @@ export function ApiForm({
|
||||
}
|
||||
});
|
||||
|
||||
// Optionally pre-process the data before submitting it
|
||||
if (props.processFormData) {
|
||||
jsonData = props.processFormData(jsonData, form);
|
||||
}
|
||||
|
||||
/* Set the timeout for the request:
|
||||
* - If a timeout is provided in the props, use that
|
||||
* - If the form contains files, use a longer timeout
|
||||
|
@@ -209,6 +209,10 @@ export function RenderInlineModel({
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof suffix === 'string') {
|
||||
suffix = <Text size='xs'>{suffix}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group gap='xs' justify='space-between' wrap='nowrap' title={tooltip}>
|
||||
<Group gap='xs' justify='left' wrap='nowrap'>
|
||||
@@ -226,7 +230,7 @@ export function RenderInlineModel({
|
||||
{suffix && (
|
||||
<>
|
||||
<Space />
|
||||
<div style={{ fontSize: 'xs', lineHeight: 'xs' }}>{suffix}</div>
|
||||
{suffix}
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
@@ -125,7 +125,7 @@ export function RenderPartTestTemplate({
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.test_name}
|
||||
secondary={instance.description}
|
||||
suffix={instance.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -237,8 +237,7 @@ export default function SystemSettings() {
|
||||
'STOCK_SHOW_INSTALLED_ITEMS',
|
||||
'STOCK_ENFORCE_BOM_INSTALLATION',
|
||||
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER',
|
||||
'TEST_STATION_DATA',
|
||||
'TEST_UPLOAD_CREATE_TEMPLATE'
|
||||
'TEST_STATION_DATA'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
@@ -2,16 +2,24 @@ import { t } from '@lingui/core/macro';
|
||||
import { ActionIcon, Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
|
||||
import { PassFailButton } from '@lib/components/YesNoButton';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import { AddItemButton } from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
@@ -62,7 +70,9 @@ export default function BuildOrderTestTable({
|
||||
}, [testTemplates]);
|
||||
|
||||
const [selectedOutput, setSelectedOutput] = useState<number>(0);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const testResultFields: ApiFormFieldSet = useTestResultFields({
|
||||
partId: partId,
|
||||
@@ -82,6 +92,48 @@ export default function BuildOrderTestTable({
|
||||
successMessage: t`Test result added`
|
||||
});
|
||||
|
||||
const multipleTestResultFields: ApiFormFieldSet = useMemo(() => {
|
||||
const fields: ApiFormFieldSet = { ...testResultFields };
|
||||
|
||||
// Do not allow attachment for multiple test results
|
||||
delete fields.attachment;
|
||||
delete fields.stock_item;
|
||||
|
||||
fields.template.disabled = false;
|
||||
|
||||
return fields;
|
||||
}, [partId, testResultFields]);
|
||||
|
||||
const generateTestResults = useCallback(
|
||||
(data: any, form: UseFormReturn) => {
|
||||
// Generate a list of test results for each selected output
|
||||
const results = table.selectedRecords.map((record: any) => {
|
||||
return {
|
||||
...data,
|
||||
stock_item: record.pk
|
||||
};
|
||||
});
|
||||
|
||||
return results;
|
||||
},
|
||||
[table.selectedIds]
|
||||
);
|
||||
|
||||
const createTestResultMultiple = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.stock_test_result_list),
|
||||
title: t`Add Test Results`,
|
||||
fields: multipleTestResultFields,
|
||||
initialData: {
|
||||
result: true
|
||||
},
|
||||
onFormSuccess: () => {
|
||||
table.clearSelectedRecords();
|
||||
table.refreshTable();
|
||||
},
|
||||
processFormData: generateTestResults,
|
||||
successMessage: t`Test results added`
|
||||
});
|
||||
|
||||
// Generate a table column for each test template
|
||||
const testColumns: TableColumn[] = useMemo(() => {
|
||||
if (!testTemplates || testTemplates.length == 0) {
|
||||
@@ -112,6 +164,7 @@ export default function BuildOrderTestTable({
|
||||
<ActionIcon
|
||||
size='lg'
|
||||
color='green'
|
||||
aria-label='add-test-result'
|
||||
variant='transparent'
|
||||
onClick={(event: any) => {
|
||||
cancelEvent(event);
|
||||
@@ -224,12 +277,37 @@ export default function BuildOrderTestTable({
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [];
|
||||
return [
|
||||
<AddItemButton
|
||||
key='add-test-result'
|
||||
tooltip={t`Add Test Result`}
|
||||
disabled={!table.hasSelectedRecords}
|
||||
onClick={(event: any) => {
|
||||
createTestResultMultiple.open();
|
||||
}}
|
||||
/>
|
||||
];
|
||||
}, [table.hasSelectedRecords]);
|
||||
|
||||
const rowActions = useCallback((record: any) => {
|
||||
return [
|
||||
{
|
||||
icon: <IconCirclePlus />,
|
||||
color: 'green',
|
||||
title: t`Add Test Result`,
|
||||
onClick: (event: any) => {
|
||||
setSelectedOutput(record.pk);
|
||||
setSelectedTemplate(undefined);
|
||||
createTestResult.open();
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createTestResult.modal}
|
||||
{createTestResultMultiple.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.stock_item_list)}
|
||||
tableState={table}
|
||||
@@ -241,6 +319,8 @@ export default function BuildOrderTestTable({
|
||||
tests: true,
|
||||
build: buildId
|
||||
},
|
||||
enableSelection: true,
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.stockitem
|
||||
|
@@ -79,11 +79,14 @@ test('Build Order - Basic Tests', async ({ browser }) => {
|
||||
await loadTab(page, 'Test Results');
|
||||
await page.getByText('Quantity: 25').waitFor();
|
||||
await page.getByText('Continuity Checks').waitFor();
|
||||
await page
|
||||
|
||||
const button = await page
|
||||
.getByRole('row', { name: 'Quantity: 16' })
|
||||
.getByRole('button')
|
||||
.hover();
|
||||
await page.getByText('Add Test Result').waitFor();
|
||||
.getByLabel('add-test-result');
|
||||
|
||||
await button.click();
|
||||
await page.getByRole('textbox', { name: 'text-field-value' }).waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Click through to the "parent" build
|
||||
await loadTab(page, 'Build Details');
|
||||
|
Reference in New Issue
Block a user