Don't use idx to identify rows
This commit is contained in:
parent
2b65220d0d
commit
9a8b8dadeb
|
|
@ -2,7 +2,7 @@ import { t } from '@lingui/core/macro';
|
|||
import { Trans } from '@lingui/react/macro';
|
||||
import { Alert, Container, Group, Stack, Table, Text } from '@mantine/core';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||
import { type ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { FieldValues, UseControllerReturn } from 'react-hook-form';
|
||||
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
|
|
@ -14,15 +14,25 @@ import { StandaloneField } from '../StandaloneField';
|
|||
export interface TableFieldRowProps {
|
||||
item: any;
|
||||
idx: number;
|
||||
rowId?: string | number;
|
||||
rowErrors: any;
|
||||
control: UseControllerReturn<FieldValues, any>;
|
||||
changeFn: (idx: number, key: string, value: any) => void;
|
||||
removeFn: (idx: number) => void;
|
||||
changeFn: (idx: number | string, key: string, value: any) => void;
|
||||
removeFn: (idx: number | string) => void;
|
||||
}
|
||||
|
||||
function getRowIdentifier(item: any, idx: number): string | number {
|
||||
if (item && typeof item === 'object') {
|
||||
return item.pk ?? item.item ?? item.id ?? item.uuid ?? idx;
|
||||
}
|
||||
|
||||
return item ?? idx;
|
||||
}
|
||||
|
||||
function TableFieldRow({
|
||||
item,
|
||||
idx,
|
||||
rowId,
|
||||
errors,
|
||||
definition,
|
||||
control,
|
||||
|
|
@ -31,11 +41,12 @@ function TableFieldRow({
|
|||
}: Readonly<{
|
||||
item: any;
|
||||
idx: number;
|
||||
rowId?: string | number;
|
||||
errors: any;
|
||||
definition: ApiFormFieldType;
|
||||
control: UseControllerReturn<FieldValues, any>;
|
||||
changeFn: (idx: number, key: string, value: any) => void;
|
||||
removeFn: (idx: number) => void;
|
||||
removeFn: (idx: number | string) => void;
|
||||
}>) {
|
||||
// Table fields require render function
|
||||
if (!definition.modelRenderer) {
|
||||
|
|
@ -53,6 +64,7 @@ function TableFieldRow({
|
|||
return definition.modelRenderer({
|
||||
item: item,
|
||||
idx: idx,
|
||||
rowId: rowId,
|
||||
rowErrors: errors,
|
||||
control: control,
|
||||
changeFn: changeFn,
|
||||
|
|
@ -98,23 +110,84 @@ export function TableField({
|
|||
} = control;
|
||||
const { value } = field;
|
||||
|
||||
const onRowFieldChange = useCallback(
|
||||
(idx: number, key: string, value: any) => {
|
||||
const val = field.value;
|
||||
val[idx][key] = value;
|
||||
const valueRef = useRef(value);
|
||||
const rowIndexByIdRef = useRef(new Map<string | number, number>());
|
||||
|
||||
field.onChange(val);
|
||||
useEffect(() => {
|
||||
valueRef.current = value;
|
||||
|
||||
const nextRowIndexById = new Map<string | number, number>();
|
||||
|
||||
value?.forEach((item: any, idx: number) => {
|
||||
nextRowIndexById.set(getRowIdentifier(item, idx), idx);
|
||||
});
|
||||
|
||||
rowIndexByIdRef.current = nextRowIndexById;
|
||||
}, [value]);
|
||||
|
||||
const resolveRowIndex = useCallback((identifier: number | string) => {
|
||||
const mappedIndex = rowIndexByIdRef.current.get(identifier);
|
||||
|
||||
if (mappedIndex !== undefined) {
|
||||
return mappedIndex;
|
||||
}
|
||||
|
||||
if (typeof identifier === 'number' && identifier >= 0) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
const onRowFieldChange = useCallback(
|
||||
(identifier: number | string, key: string, rowValue: any) => {
|
||||
const idx = resolveRowIndex(identifier);
|
||||
|
||||
if (idx === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = valueRef.current;
|
||||
|
||||
if (!Array.isArray(currentValue) || currentValue[idx] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = [...currentValue];
|
||||
const currentRow = nextValue[idx];
|
||||
|
||||
if (currentRow && typeof currentRow === 'object') {
|
||||
nextValue[idx] = {
|
||||
...currentRow,
|
||||
[key]: rowValue
|
||||
};
|
||||
}
|
||||
|
||||
field.onChange(nextValue);
|
||||
},
|
||||
[field.value, field.onChange]
|
||||
[field.onChange, resolveRowIndex]
|
||||
);
|
||||
|
||||
const removeRow = useCallback(
|
||||
(idx: number) => {
|
||||
const val = field.value;
|
||||
val.splice(idx, 1);
|
||||
field.onChange(val);
|
||||
(identifier: number | string) => {
|
||||
const idx = resolveRowIndex(identifier);
|
||||
|
||||
if (idx === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = valueRef.current;
|
||||
|
||||
if (!Array.isArray(currentValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = [...currentValue];
|
||||
nextValue.splice(idx, 1);
|
||||
|
||||
field.onChange(nextValue);
|
||||
},
|
||||
[field.value, field.onChange]
|
||||
[field.onChange, resolveRowIndex]
|
||||
);
|
||||
|
||||
// Extract errors associated with the current row
|
||||
|
|
@ -152,11 +225,14 @@ export function TableField({
|
|||
<Table.Tbody>
|
||||
{(value?.length ?? 0) > 0 ? (
|
||||
value.map((item: any, idx: number) => {
|
||||
const rowId = getRowIdentifier(item, idx);
|
||||
|
||||
return (
|
||||
<TableFieldRow
|
||||
key={`table-row-${idx}`}
|
||||
key={`table-row-${rowId}`}
|
||||
item={item}
|
||||
idx={idx}
|
||||
rowId={rowId}
|
||||
errors={rowErrors(idx)}
|
||||
control={control}
|
||||
definition={definition}
|
||||
|
|
@ -195,9 +271,7 @@ export function TableField({
|
|||
if (definition.addRow === undefined) return;
|
||||
const ret = definition.addRow();
|
||||
if (ret) {
|
||||
const val = field.value;
|
||||
val.push(ret);
|
||||
field.onChange(val);
|
||||
field.onChange([...(field.value ?? []), ret]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -230,10 +304,16 @@ export function TableFieldExtraRow({
|
|||
emptyValue?: any;
|
||||
onValueChange: (value: any) => void;
|
||||
}) {
|
||||
const hasMounted = useRef(false);
|
||||
|
||||
// Callback whenever the visibility of the sub-field changes
|
||||
// Skip the initial mount — the value was never set, nothing to reset
|
||||
useEffect(() => {
|
||||
if (!hasMounted.current) {
|
||||
hasMounted.current = true;
|
||||
return;
|
||||
}
|
||||
if (!visible) {
|
||||
// If the sub-field is hidden, reset the value to the "empty" value
|
||||
onValueChange(emptyValue);
|
||||
}
|
||||
}, [visible]);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import dayjs from 'dayjs';
|
|||
import {
|
||||
type JSX,
|
||||
Suspense,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
|
|
@ -567,6 +568,50 @@ type StockRow = {
|
|||
removeFn: TableFieldRefreshFn;
|
||||
};
|
||||
|
||||
function ReturnStockMoveButton({
|
||||
record,
|
||||
quantity,
|
||||
onRemove,
|
||||
returnStock
|
||||
}: {
|
||||
record: any;
|
||||
quantity: StockItemQuantity;
|
||||
onRemove: () => void;
|
||||
returnStock: boolean;
|
||||
}) {
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
onClick={() =>
|
||||
moveToDefault(
|
||||
record,
|
||||
quantity,
|
||||
onRemove,
|
||||
returnStock
|
||||
? {
|
||||
title: t`Confirm Stock Return`,
|
||||
onConfirm: (location: number) => {
|
||||
form.setValue('location', location, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true
|
||||
});
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
icon={<InvenTreeIcon icon='default_location' />}
|
||||
tooltip={t`Move to default location`}
|
||||
tooltipAlignment='top'
|
||||
disabled={
|
||||
!record.part_detail?.default_location &&
|
||||
!record.part_detail?.category_default_location
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function StockOperationsRow({
|
||||
props,
|
||||
transfer = false,
|
||||
|
|
@ -588,7 +633,7 @@ function StockOperationsRow({
|
|||
returnStock?: boolean;
|
||||
record?: any;
|
||||
}) {
|
||||
const form = useFormContext();
|
||||
const rowId = props.rowId ?? props.idx;
|
||||
|
||||
const statusOptions: ApiFormFieldChoice[] = useMemo(() => {
|
||||
return (
|
||||
|
|
@ -607,23 +652,26 @@ function StockOperationsRow({
|
|||
|
||||
const [status, setStatus] = useState<number | undefined>(undefined);
|
||||
|
||||
const removeAndRefresh = () => {
|
||||
props.removeFn(props.idx);
|
||||
};
|
||||
const removeAndRefresh = useCallback(() => {
|
||||
props.removeFn(rowId);
|
||||
}, [props.removeFn, rowId]);
|
||||
|
||||
const callChangeFn = (idx: number, key: string, value: any) => {
|
||||
setTimeout(() => props.changeFn(idx, key, value), 0);
|
||||
};
|
||||
const callChangeFn = useCallback(
|
||||
(identifier: number | string, key: string, value: any) => {
|
||||
props.changeFn(identifier, key, value);
|
||||
},
|
||||
[props.changeFn]
|
||||
);
|
||||
|
||||
const [packagingOpen, packagingHandlers] = useDisclosure(false, {
|
||||
onOpen: () => {
|
||||
if (transfer) {
|
||||
callChangeFn(props.idx, 'packaging', record?.packaging || undefined);
|
||||
callChangeFn(rowId, 'packaging', record?.packaging || undefined);
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
if (transfer) {
|
||||
callChangeFn(props.idx, 'packaging', undefined);
|
||||
callChangeFn(rowId, 'packaging', undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -631,11 +679,11 @@ function StockOperationsRow({
|
|||
const [statusOpen, statusHandlers] = useDisclosure(false, {
|
||||
onOpen: () => {
|
||||
setStatus(record?.status_custom_key || record?.status || undefined);
|
||||
props.changeFn(props.idx, 'status', record?.status || undefined);
|
||||
props.changeFn(rowId, 'status', record?.status || undefined);
|
||||
},
|
||||
onClose: () => {
|
||||
setStatus(undefined);
|
||||
callChangeFn(props.idx, 'status', undefined);
|
||||
callChangeFn(rowId, 'status', undefined);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -696,7 +744,7 @@ function StockOperationsRow({
|
|||
value: quantity,
|
||||
onValueChange: (value: any) => {
|
||||
setQuantity(value);
|
||||
props.changeFn(props.idx, 'quantity', value);
|
||||
props.changeFn(rowId, 'quantity', value);
|
||||
}
|
||||
}}
|
||||
error={props.rowErrors?.quantity?.message}
|
||||
|
|
@ -705,35 +753,28 @@ function StockOperationsRow({
|
|||
)}
|
||||
<Table.Td>
|
||||
<Flex gap='3px'>
|
||||
{transfer && (
|
||||
<ActionButton
|
||||
onClick={() =>
|
||||
moveToDefault(
|
||||
record,
|
||||
props.item.quantity,
|
||||
removeAndRefresh,
|
||||
returnStock
|
||||
? {
|
||||
title: t`Confirm Stock Return`,
|
||||
onConfirm: (location: number) => {
|
||||
form.setValue('location', location, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true
|
||||
});
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
icon={<InvenTreeIcon icon='default_location' />}
|
||||
tooltip={t`Move to default location`}
|
||||
tooltipAlignment='top'
|
||||
disabled={
|
||||
!record.part_detail?.default_location &&
|
||||
!record.part_detail?.category_default_location
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{transfer &&
|
||||
(returnStock ? (
|
||||
<ReturnStockMoveButton
|
||||
record={record}
|
||||
quantity={props.item.quantity}
|
||||
onRemove={removeAndRefresh}
|
||||
returnStock={returnStock}
|
||||
/>
|
||||
) : (
|
||||
<ActionButton
|
||||
onClick={() =>
|
||||
moveToDefault(record, props.item.quantity, removeAndRefresh)
|
||||
}
|
||||
icon={<InvenTreeIcon icon='default_location' />}
|
||||
tooltip={t`Move to default location`}
|
||||
tooltipAlignment='top'
|
||||
disabled={
|
||||
!record.part_detail?.default_location &&
|
||||
!record.part_detail?.category_default_location
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{changeStatus && (
|
||||
<ActionButton
|
||||
size='sm'
|
||||
|
|
@ -757,13 +798,11 @@ function StockOperationsRow({
|
|||
size='sm'
|
||||
icon={<InvenTreeIcon icon='merge' />}
|
||||
tooltip={t`Merge into existing stock`}
|
||||
onClick={() =>
|
||||
callChangeFn(props.idx, 'merge', !props.item?.merge)
|
||||
}
|
||||
onClick={() => callChangeFn(rowId, 'merge', !props.item?.merge)}
|
||||
variant={props.item?.merge ? 'filled' : 'transparent'}
|
||||
/>
|
||||
)}
|
||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
||||
<RemoveRowButton onClick={() => props.removeFn(rowId)} />
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
|
@ -772,7 +811,7 @@ function StockOperationsRow({
|
|||
visible={statusOpen}
|
||||
onValueChange={(value: any) => {
|
||||
setStatus(value);
|
||||
props.changeFn(props.idx, 'status', value || undefined);
|
||||
props.changeFn(rowId, 'status', value || undefined);
|
||||
}}
|
||||
fieldName='status'
|
||||
fieldDefinition={{
|
||||
|
|
@ -788,7 +827,7 @@ function StockOperationsRow({
|
|||
<TableFieldExtraRow
|
||||
visible={transfer && packagingOpen}
|
||||
onValueChange={(value: any) => {
|
||||
props.changeFn(props.idx, 'packaging', value || undefined);
|
||||
props.changeFn(rowId, 'packaging', value || undefined);
|
||||
}}
|
||||
fieldName='packaging'
|
||||
fieldDefinition={{
|
||||
|
|
@ -802,6 +841,27 @@ function StockOperationsRow({
|
|||
);
|
||||
}
|
||||
|
||||
const MemoizedStockOperationsRow = memo(
|
||||
StockOperationsRow,
|
||||
(previousProps, nextProps) => {
|
||||
return (
|
||||
previousProps.props.rowId === nextProps.props.rowId &&
|
||||
previousProps.props.item === nextProps.props.item &&
|
||||
previousProps.props.rowErrors === nextProps.props.rowErrors &&
|
||||
previousProps.props.changeFn === nextProps.props.changeFn &&
|
||||
previousProps.props.removeFn === nextProps.props.removeFn &&
|
||||
previousProps.record === nextProps.record &&
|
||||
previousProps.transfer === nextProps.transfer &&
|
||||
previousProps.changeStatus === nextProps.changeStatus &&
|
||||
previousProps.add === nextProps.add &&
|
||||
previousProps.setMax === nextProps.setMax &&
|
||||
previousProps.merge === nextProps.merge &&
|
||||
previousProps.transferMerge === nextProps.transferMerge &&
|
||||
previousProps.returnStock === nextProps.returnStock
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type StockItemQuantity = number | '' | undefined;
|
||||
|
||||
type StockAdjustmentItem = {
|
||||
|
|
@ -850,7 +910,7 @@ function stockTransferFields(
|
|||
const record = records[row.item.pk];
|
||||
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
transfer
|
||||
changeStatus
|
||||
|
|
@ -899,7 +959,7 @@ function stockReturnFields(items: any[]): ApiFormFieldSet {
|
|||
const record = records[row.item.pk];
|
||||
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
key={record.pk}
|
||||
record={record}
|
||||
|
|
@ -965,7 +1025,7 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
|
|||
const record = records[row.item.pk];
|
||||
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
setMax
|
||||
changeStatus
|
||||
|
|
@ -1012,7 +1072,7 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
|
|||
const record = records[row.item.pk];
|
||||
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
changeStatus
|
||||
props={row}
|
||||
add
|
||||
|
|
@ -1054,7 +1114,7 @@ function stockCountFields(items: any[]): ApiFormFieldSet {
|
|||
value: initialValue,
|
||||
modelRenderer: (row: TableFieldRowProps) => {
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
changeStatus
|
||||
key={row.item.pk}
|
||||
|
|
@ -1103,7 +1163,7 @@ function stockChangeStatusFields(items: any[]): ApiFormFieldSet {
|
|||
}),
|
||||
modelRenderer: (row: TableFieldRowProps) => {
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
key={row.item}
|
||||
merge
|
||||
|
|
@ -1170,7 +1230,7 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
|
|||
}),
|
||||
modelRenderer: (row: TableFieldRowProps) => {
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
key={row.item.item}
|
||||
merge
|
||||
|
|
@ -1219,7 +1279,7 @@ function stockAssignFields(items: any[]): ApiFormFieldSet {
|
|||
}),
|
||||
modelRenderer: (row: TableFieldRowProps) => {
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
key={row.item.item}
|
||||
merge
|
||||
|
|
@ -1263,7 +1323,7 @@ function stockDeleteFields(items: any[]): ApiFormFieldSet {
|
|||
const record = records[row.item];
|
||||
|
||||
return (
|
||||
<StockOperationsRow
|
||||
<MemoizedStockOperationsRow
|
||||
props={row}
|
||||
key={record.pk}
|
||||
merge
|
||||
|
|
@ -1447,9 +1507,14 @@ export function useReturnStockItem(props: StockOperationProps) {
|
|||
}
|
||||
|
||||
export function useCountStockItem(props: StockOperationProps) {
|
||||
const fieldGenerator = useCallback(
|
||||
(items: any[]) => stockCountFields(items),
|
||||
[]
|
||||
);
|
||||
|
||||
return useStockOperationModal({
|
||||
...props,
|
||||
fieldGenerator: stockCountFields,
|
||||
fieldGenerator: fieldGenerator,
|
||||
endpoint: ApiEndpoints.stock_count,
|
||||
title: t`Count Stock`,
|
||||
successMessage: t`Stock counted`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue