From 2cf959cb8d1fd31f8a58e5989b20671614eefb55 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 25 Aug 2024 11:04:18 +1000 Subject: [PATCH] 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 --- .../components/forms/fields/TableField.tsx | 4 + src/frontend/src/forms/BuildForms.tsx | 2 + src/frontend/src/forms/PurchaseOrderForms.tsx | 137 +++++++-------- src/frontend/src/forms/StockForms.tsx | 160 +++++++++--------- 4 files changed, 157 insertions(+), 146 deletions(-) diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx index 7fb6e3fa39..333ed40cac 100644 --- a/src/frontend/src/components/forms/fields/TableField.tsx +++ b/src/frontend/src/components/forms/fields/TableField.tsx @@ -34,6 +34,7 @@ export function TableField({ const onRowFieldChange = (idx: number, key: string, value: any) => { const val = field.value; val[idx][key] = value; + field.onChange(val); }; @@ -114,11 +115,13 @@ export function TableFieldExtraRow({ fieldDefinition, defaultValue, emptyValue, + error, onValueChange }: { visible: boolean; fieldDefinition: ApiFormFieldType; defaultValue?: any; + error?: string; emptyValue?: any; onValueChange: (value: any) => void; }) { @@ -151,6 +154,7 @@ export function TableFieldExtraRow({ diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index fd86e6b4b8..b5b69e718d 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -554,12 +554,14 @@ function BuildAllocateLineRow({ diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index a6579a91b9..1d0b4c3019 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -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(''); - const [serials, setSerials] = useState(''); 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(''); - const [barcode, setBarcode] = useState(null); + const [barcode, setBarcode] = useState(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 /> - - input.changeFn(input.idx, 'quantity', value)} + + + props.changeFn(props.idx, 'quantity', value) + }} + error={props.rowErrors?.quantity?.message} /> @@ -404,6 +395,7 @@ function LineItemFormRow({ size="sm" icon={} 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)} /> ) : ( open()} /> )} - input.removeFn(input.idx)} /> + props.removeFn(props.idx)} /> @@ -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={} 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({ } 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({ } 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({ )} 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} /> - 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} /> 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} /> 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} /> 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 ( setBatchCode(value) }, - status_custom_key: {}, + status_custom_key: { + label: t`Stock Status` + }, expiry_date: { // TODO: icon }, @@ -295,47 +301,37 @@ type StockRow = { }; function StockOperationsRow({ - input, + props, transfer = false, add = false, setMax = false, merge = false, record }: { - input: StockRow; + props: TableFieldRowProps; transfer?: boolean; add?: boolean; setMax?: boolean; merge?: boolean; record?: any; }) { - const item = input.item; - - const [value, setValue] = useState( - add ? 0 : item.quantity ?? 0 - ); - - const onChange = useCallback( - (value: any) => { - setValue(value); - input.changeFn(input.idx, 'quantity', value); - }, - [item] + const [quantity, setQuantity] = useState( + add ? 0 : props.item?.quantity ?? 0 ); const removeAndRefresh = () => { - input.removeFn(input.idx); + props.removeFn(props.idx); }; const [packagingOpen, packagingHandlers] = useDisclosure(false, { onOpen: () => { if (transfer) { - input.changeFn(input.idx, 'packaging', record?.packaging || undefined); + props.changeFn(props.idx, 'packaging', record?.packaging || undefined); } }, onClose: () => { if (transfer) { - input.changeFn(input.idx, 'packaging', undefined); + props.changeFn(props.idx, 'packaging', undefined); } } }); @@ -371,25 +367,24 @@ function StockOperationsRow({ {record.location ? record.location_detail?.pathstring : '-'} - - - {stockString} - - - + + {stockString} + + {!merge && ( - { + setQuantity(value); + props.changeFn(props.idx, 'quantity', value); + } + }} + error={props.rowErrors?.quantity?.message} /> )} @@ -397,7 +392,9 @@ function StockOperationsRow({ {transfer && ( moveToDefault(record, value, removeAndRefresh)} + onClick={() => + moveToDefault(record, props.item.quantity, removeAndRefresh) + } icon={} tooltip={t`Move to default location`} tooltipAlignment="top" @@ -416,7 +413,7 @@ function StockOperationsRow({ variant={packagingOpen ? 'filled' : 'transparent'} /> )} - input.removeFn(input.idx)} /> + props.removeFn(props.idx)} /> @@ -424,7 +421,7 @@ function StockOperationsRow({ { - input.changeFn(input.idx, 'packaging', value || undefined); + props.changeFn(props.idx, 'packaging', value || undefined); }} fieldDefinition={{ field_type: 'string', @@ -452,9 +449,9 @@ function mapAdjustmentItems(items: any[]) { return { pk: elem.pk, quantity: elem.quantity, - batch: elem.batch, - status: elem.status, - packaging: elem.packaging, + batch: elem.batch || undefined, + status: elem.status || undefined, + packaging: elem.packaging || undefined, obj: elem }; }); @@ -473,14 +470,16 @@ function stockTransferFields(items: any[]): ApiFormFieldSet { items: { field_type: 'table', value: mapAdjustmentItems(items), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { + const record = records[row.item.pk]; + return ( ); }, @@ -508,13 +507,16 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet { items: { field_type: 'table', value: mapAdjustmentItems(items), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { + const record = records[row.item.pk]; + return ( ); }, @@ -537,14 +539,11 @@ function stockAddFields(items: any[]): ApiFormFieldSet { items: { field_type: 'table', value: mapAdjustmentItems(items), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { + const record = records[row.item.pk]; + return ( - + ); }, headers: [t`Part`, t`Location`, t`In Stock`, t`Add`, t`Actions`] @@ -566,12 +565,12 @@ function stockCountFields(items: any[]): ApiFormFieldSet { items: { field_type: 'table', value: mapAdjustmentItems(items), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { return ( ); }, @@ -596,19 +595,19 @@ function stockChangeStatusFields(items: any[]): ApiFormFieldSet { value: items.map((elem) => { return elem.pk; }), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { return ( ); }, headers: [t`Part`, t`Location`, t`In Stock`, t`Actions`] }, - status_custom_key: {}, + status: {}, note: {} }; @@ -631,13 +630,13 @@ function stockMergeFields(items: any[]): ApiFormFieldSet { obj: elem }; }), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { return ( ); }, @@ -673,13 +672,13 @@ function stockAssignFields(items: any[]): ApiFormFieldSet { obj: elem }; }), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { return ( ); }, @@ -709,13 +708,15 @@ function stockDeleteFields(items: any[]): ApiFormFieldSet { value: items.map((elem) => { return elem.pk; }), - modelRenderer: (val) => { + modelRenderer: (row: TableFieldRowProps) => { + const record = records[row.item]; + return ( ); }, @@ -803,6 +804,7 @@ function stockOperationModal({ url: endpoint, fields: fields, title: title, + size: '80%', onFormSuccess: () => refresh() }); }