Forms refactor (#7981)

* Refactor "receive stock" table

- Display errors
- Fix infinite rendering loop
- Correctly set values to undefined on close

* Refactor stock operations table

* Fix for "change stock status" form

* Fix default values

* Unit test fix
This commit is contained in:
Oliver
2024-08-25 11:04:18 +10:00
committed by GitHub
parent eec53ffd82
commit 2cf959cb8d
4 changed files with 157 additions and 146 deletions
+70 -67
View File
@@ -5,7 +5,6 @@ import {
FocusTrap,
Group,
Modal,
NumberInput,
Table,
TextInput
} from '@mantine/core';
@@ -34,7 +33,10 @@ import {
ApiFormAdjustFilterType,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
import { TableFieldExtraRow } from '../components/forms/fields/TableField';
import {
TableFieldExtraRow,
TableFieldRowProps
} from '../components/forms/fields/TableField';
import { Thumbnail } from '../components/images/Thumbnail';
import { ProgressBar } from '../components/items/ProgressBar';
import { StylishText } from '../components/items/StylishText';
@@ -192,67 +194,53 @@ export function usePurchaseOrderFields(): ApiFormFieldSet {
* Render a table row for a single TableField entry
*/
function LineItemFormRow({
input,
props,
record,
statuses
}: {
input: any;
props: TableFieldRowProps;
record: any;
statuses: any;
}) {
// Barcode Modal state
const [opened, { open, close }] = useDisclosure(false);
const [opened, { open, close }] = useDisclosure(false, {
onClose: () => props.changeFn(props.idx, 'barcode', undefined)
});
// Location value
const [location, setLocation] = useState(
input.item.location ??
record.part_detail.default_location ??
record.part_detail.category_default_location
);
const [locationOpen, locationHandlers] = useDisclosure(
location ? true : false,
{
onClose: () => input.changeFn(input.idx, 'location', null),
onOpen: () => input.changeFn(input.idx, 'location', location)
}
);
// Change form value when state is altered
useEffect(() => {
input.changeFn(input.idx, 'location', location);
}, [location]);
const [locationOpen, locationHandlers] = useDisclosure(false, {
onClose: () => props.changeFn(props.idx, 'location', undefined)
});
// Batch code generator
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
if (!batchCode) {
setBatchCode(value);
if (value) {
props.changeFn(props.idx, 'batch_code', value);
}
});
// Serial numbebr generator
const serialNumberGenerator = useSerialNumberGenerator((value: any) => {
if (!serials) {
setSerials(value);
if (value) {
props.changeFn(props.idx, 'serial_numbers', value);
}
});
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
onClose: () => {
input.changeFn(input.idx, 'packaging', undefined);
props.changeFn(props.idx, 'packaging', undefined);
}
});
const [noteOpen, noteHandlers] = useDisclosure(false, {
onClose: () => {
input.changeFn(input.idx, 'note', undefined);
props.changeFn(props.idx, 'note', undefined);
}
});
// State for serializing
const [batchCode, setBatchCode] = useState<string>('');
const [serials, setSerials] = useState<string>('');
const [batchOpen, batchHandlers] = useDisclosure(false, {
onClose: () => {
input.changeFn(input.idx, 'batch_code', undefined);
input.changeFn(input.idx, 'serial_numbers', '');
props.changeFn(props.idx, 'batch_code', undefined);
props.changeFn(props.idx, 'serial_numbers', undefined);
},
onOpen: () => {
// Generate a new batch code
@@ -263,23 +251,23 @@ function LineItemFormRow({
// Generate new serial numbers
serialNumberGenerator.update({
part: record?.supplier_part_detail?.part,
quantity: input.item.quantity
quantity: props.item.quantity
});
}
});
// Status value
const [statusOpen, statusHandlers] = useDisclosure(false, {
onClose: () => input.changeFn(input.idx, 'status', 10)
onClose: () => props.changeFn(props.idx, 'status', undefined)
});
// Barcode value
const [barcodeInput, setBarcodeInput] = useState<any>('');
const [barcode, setBarcode] = useState(null);
const [barcode, setBarcode] = useState<String | undefined>(undefined);
// Change form value when state is altered
useEffect(() => {
input.changeFn(input.idx, 'barcode', barcode);
props.changeFn(props.idx, 'barcode', barcode);
}, [barcode]);
// Update location field description on state change
@@ -371,13 +359,16 @@ function LineItemFormRow({
progressLabel
/>
</Table.Td>
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
<NumberInput
value={input.item.quantity}
style={{ width: '100px' }}
max={input.item.quantity}
min={0}
onChange={(value) => input.changeFn(input.idx, 'quantity', value)}
<Table.Td style={{ whiteSpace: 'nowrap' }}>
<StandaloneField
fieldName="quantity"
fieldDefinition={{
field_type: 'number',
value: props.item.quantity,
onValueChange: (value) =>
props.changeFn(props.idx, 'quantity', value)
}}
error={props.rowErrors?.quantity?.message}
/>
</Table.Td>
<Table.Td style={{ width: '1%', whiteSpace: 'nowrap' }}>
@@ -404,6 +395,7 @@ function LineItemFormRow({
size="sm"
icon={<InvenTreeIcon icon="packaging" />}
tooltip={t`Adjust Packaging`}
tooltipAlignment="top"
onClick={() => packagingHandlers.toggle()}
variant={packagingOpen ? 'filled' : 'transparent'}
/>
@@ -428,7 +420,7 @@ function LineItemFormRow({
tooltipAlignment="top"
variant="filled"
color="red"
onClick={() => setBarcode(null)}
onClick={() => setBarcode(undefined)}
/>
) : (
<ActionButton
@@ -439,7 +431,7 @@ function LineItemFormRow({
onClick={() => open()}
/>
)}
<RemoveRowButton onClick={() => input.removeFn(input.idx)} />
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Flex>
</Table.Td>
</Table.Tr>
@@ -459,7 +451,7 @@ function LineItemFormRow({
structural: false
},
onValueChange: (value) => {
setLocation(value);
props.changeFn(props.idx, 'location', value);
},
description: locationDescription,
value: location,
@@ -480,7 +472,9 @@ function LineItemFormRow({
icon={<InvenTreeIcon icon="default_location" />}
tooltip={t`Store at default location`}
onClick={() =>
setLocation(
props.changeFn(
props.idx,
'location',
record.part_detail.default_location ??
record.part_detail.category_default_location
)
@@ -492,7 +486,9 @@ function LineItemFormRow({
<ActionButton
icon={<InvenTreeIcon icon="destination" />}
tooltip={t`Store at line item destination `}
onClick={() => setLocation(record.destination)}
onClick={() =>
props.changeFn(props.idx, 'location', record.destination)
}
tooltipAlignment="top"
/>
)}
@@ -502,7 +498,13 @@ function LineItemFormRow({
<ActionButton
icon={<InvenTreeIcon icon="repeat_destination" />}
tooltip={t`Store with already received stock`}
onClick={() => setLocation(record.destination_detail.pk)}
onClick={() =>
props.changeFn(
props.idx,
'location',
record.destination_detail.pk
)
}
tooltipAlignment="top"
/>
)}
@@ -513,51 +515,56 @@ function LineItemFormRow({
)}
<TableFieldExtraRow
visible={batchOpen}
onValueChange={(value) => input.changeFn(input.idx, 'batch', value)}
onValueChange={(value) => props.changeFn(props.idx, 'batch', value)}
fieldDefinition={{
field_type: 'string',
label: t`Batch Code`,
value: batchCode
value: props.item.batch_code
}}
error={props.rowErrors?.batch_code?.message}
/>
<TableFieldExtraRow
visible={batchOpen && record.trackable}
onValueChange={(value) =>
input.changeFn(input.idx, 'serial_numbers', value)
props.changeFn(props.idx, 'serial_numbers', value)
}
fieldDefinition={{
field_type: 'string',
label: t`Serial numbers`,
value: serials
value: props.item.serial_numbers
}}
error={props.rowErrors?.serial_numbers?.message}
/>
<TableFieldExtraRow
visible={packagingOpen}
onValueChange={(value) => input.changeFn(input.idx, 'packaging', value)}
onValueChange={(value) => props.changeFn(props.idx, 'packaging', value)}
fieldDefinition={{
field_type: 'string',
label: t`Packaging`
}}
defaultValue={record?.supplier_part_detail?.packaging}
error={props.rowErrors?.packaging?.message}
/>
<TableFieldExtraRow
visible={statusOpen}
defaultValue={10}
onValueChange={(value) => input.changeFn(input.idx, 'status', value)}
onValueChange={(value) => props.changeFn(props.idx, 'status', value)}
fieldDefinition={{
field_type: 'choice',
api_url: apiUrl(ApiEndpoints.stock_status),
choices: statuses,
label: t`Status`
}}
error={props.rowErrors?.status?.message}
/>
<TableFieldExtraRow
visible={noteOpen}
onValueChange={(value) => input.changeFn(input.idx, 'note', value)}
onValueChange={(value) => props.changeFn(props.idx, 'note', value)}
fieldDefinition={{
field_type: 'string',
label: t`Note`
}}
error={props.rowErrors?.note?.message}
/>
</>
);
@@ -619,12 +626,12 @@ export function useReceiveLineItems(props: LineItemsForm) {
barcode: null
};
}),
modelRenderer: (instance) => {
const record = records[instance.item.line_item];
modelRenderer: (row: TableFieldRowProps) => {
const record = records[row.item.line_item];
return (
<LineItemFormRow
input={instance}
props={row}
record={record}
statuses={data}
key={record.pk}
@@ -640,18 +647,14 @@ export function useReceiveLineItems(props: LineItemsForm) {
}
};
const url = apiUrl(ApiEndpoints.purchase_order_receive, null, {
id: props.orderPk
});
return useCreateApiFormModal({
...props.formProps,
url: url,
url: apiUrl(ApiEndpoints.purchase_order_receive, props.orderPk),
title: t`Receive Line Items`,
fields: fields,
initialData: {
location: null
},
size: 'xl'
size: '80%'
});
}