Optimize error processing and re-rendering in ValidationCell component. Implemented a centralized processErrors function, improved memoization, and enhanced batch updates to reduce redundancy and improve performance.
This commit is contained in:
@@ -206,19 +206,15 @@ function processErrors(value: any, errors: ErrorObject[]): {
|
||||
|
||||
if (valueIsEmpty) {
|
||||
// For empty values, check if there are required errors
|
||||
hasRequiredError = errors.some(error =>
|
||||
error.type === ErrorType.Required
|
||||
);
|
||||
hasRequiredError = errors.some(error => error.type === ErrorType.Required);
|
||||
filteredErrors = errors;
|
||||
} else {
|
||||
// For non-empty values, filter out required errors
|
||||
filteredErrors = errors.filter(error =>
|
||||
error.type !== ErrorType.Required
|
||||
);
|
||||
filteredErrors = errors.filter(error => error.type !== ErrorType.Required);
|
||||
}
|
||||
|
||||
// Determine if any actual errors exist after filtering
|
||||
const hasError = filteredErrors.some(error =>
|
||||
const hasError = filteredErrors.length > 0 && filteredErrors.some(error =>
|
||||
error.level === 'error' || error.level === 'warning'
|
||||
);
|
||||
|
||||
@@ -226,7 +222,7 @@ function processErrors(value: any, errors: ErrorObject[]): {
|
||||
const isRequiredButEmpty = valueIsEmpty && hasRequiredError;
|
||||
|
||||
// Only show error icons for non-empty fields with actual errors
|
||||
const shouldShowErrorIcon = hasError && !valueIsEmpty;
|
||||
const shouldShowErrorIcon = hasError && (!valueIsEmpty || !hasRequiredError);
|
||||
|
||||
// Get error messages for the tooltip - only if we need to show icon
|
||||
let errorMessages = '';
|
||||
@@ -246,6 +242,26 @@ function processErrors(value: any, errors: ErrorObject[]): {
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to compare error arrays efficiently
|
||||
function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]): boolean {
|
||||
// Fast path for referential equality
|
||||
if (prevErrors === nextErrors) return true;
|
||||
|
||||
// Fast path for length check
|
||||
if (!prevErrors || !nextErrors) return prevErrors === nextErrors;
|
||||
if (prevErrors.length !== nextErrors.length) return false;
|
||||
|
||||
// Check if errors are equivalent
|
||||
return prevErrors.every((error, index) => {
|
||||
const nextError = nextErrors[index];
|
||||
return (
|
||||
error.message === nextError.message &&
|
||||
error.level === nextError.level &&
|
||||
error.type === nextError.type
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const ItemNumberCell = React.memo(({
|
||||
value,
|
||||
itemNumber,
|
||||
@@ -430,7 +446,7 @@ const ItemNumberCell = React.memo(({
|
||||
|
||||
ItemNumberCell.displayName = 'ItemNumberCell';
|
||||
|
||||
const ValidationCell = ({
|
||||
const ValidationCell = React.memo(({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
@@ -443,59 +459,28 @@ const ValidationCell = ({
|
||||
copyDown,
|
||||
rowIndex,
|
||||
totalRows = 0}: ValidationCellProps) => {
|
||||
// Use the CopyDown context
|
||||
const copyDownContext = React.useContext(CopyDownContext);
|
||||
// Only destructure what we need to avoid unused variables warning
|
||||
const { isInCopyDownMode, sourceRowIndex, sourceFieldKey } = copyDownContext;
|
||||
|
||||
// Use the optimized processErrors function to avoid redundant filtering
|
||||
const {
|
||||
filteredErrors,
|
||||
hasError,
|
||||
isRequiredButEmpty,
|
||||
shouldShowErrorIcon,
|
||||
errorMessages
|
||||
} = React.useMemo(() => processErrors(value, errors), [value, errors]);
|
||||
|
||||
// Track whether this cell is the source of a copy-down operation
|
||||
const isSourceCell = isInCopyDownMode && rowIndex === sourceRowIndex && fieldKey === sourceFieldKey;
|
||||
|
||||
// Add state for hover on copy down button
|
||||
const [isCopyDownHovered, setIsCopyDownHovered] = React.useState(false);
|
||||
// Add state for hover on target row
|
||||
const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false);
|
||||
|
||||
// Get copy down context
|
||||
const copyDownContext = React.useContext(CopyDownContext);
|
||||
|
||||
// For item_number fields, use the specialized component
|
||||
if (fieldKey === 'item_number') {
|
||||
return (
|
||||
<ItemNumberCell
|
||||
value={value}
|
||||
itemNumber={itemNumber}
|
||||
isValidating={isValidating}
|
||||
width={width}
|
||||
errors={errors}
|
||||
field={field}
|
||||
onChange={onChange}
|
||||
copyDown={copyDown}
|
||||
rowIndex={rowIndex}
|
||||
totalRows={totalRows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Memoize filtered errors to avoid recalculation on every render
|
||||
const filteredErrors = React.useMemo(() => {
|
||||
return !isEmpty(value)
|
||||
? errors.filter(error => error.type !== ErrorType.Required)
|
||||
: errors;
|
||||
}, [value, errors]);
|
||||
|
||||
// Memoize error state derivations
|
||||
const { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages } = React.useMemo(() => {
|
||||
// Determine if the field has an error after filtering
|
||||
const hasError = filteredErrors.some(error => error.level === 'error' || error.level === 'warning');
|
||||
|
||||
// Determine if the field is required but empty
|
||||
const isRequiredButEmpty = isEmpty(value) &&
|
||||
errors.some(error => error.type === ErrorType.Required);
|
||||
|
||||
// Only show error icons for non-empty fields with actual errors (not just required errors)
|
||||
const shouldShowErrorIcon = hasError && !isEmpty(value);
|
||||
|
||||
// Get error messages for the tooltip
|
||||
const errorMessages = shouldShowErrorIcon
|
||||
? filteredErrors.filter(e => e.level === 'error' || e.level === 'warning').map(e => e.message).join('\n')
|
||||
: '';
|
||||
|
||||
return { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages };
|
||||
}, [filteredErrors, value, errors]);
|
||||
|
||||
// Handle copy down button click
|
||||
const handleCopyDownClick = () => {
|
||||
if (copyDown && totalRows > rowIndex + 1) {
|
||||
@@ -506,11 +491,6 @@ const ValidationCell = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Check if this cell is the source of the current copy down operation
|
||||
const isSourceCell = copyDownContext.isInCopyDownMode &&
|
||||
copyDownContext.sourceRowIndex === rowIndex &&
|
||||
copyDownContext.sourceFieldKey === fieldKey;
|
||||
|
||||
// Check if this cell is in a row that can be a target for copy down
|
||||
const isInTargetRow = copyDownContext.isInCopyDownMode &&
|
||||
copyDownContext.sourceFieldKey === fieldKey &&
|
||||
@@ -628,64 +608,35 @@ const ValidationCell = ({
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ValidationCell, (prev, next) => {
|
||||
// For validating cells, always re-render
|
||||
if (prev.isValidating !== next.isValidating) {
|
||||
return false;
|
||||
}
|
||||
}, (prevProps, nextProps) => {
|
||||
// Deep compare the most important props to avoid unnecessary re-renders
|
||||
const valueEqual = prevProps.value === nextProps.value;
|
||||
const isValidatingEqual = prevProps.isValidating === nextProps.isValidating;
|
||||
const fieldEqual = prevProps.field === nextProps.field;
|
||||
const itemNumberEqual = prevProps.itemNumber === nextProps.itemNumber;
|
||||
|
||||
// Quick reference equality checks first for better performance
|
||||
if (prev.value !== next.value || prev.width !== next.width) {
|
||||
return false;
|
||||
}
|
||||
// Use enhanced error comparison
|
||||
const errorsEqual = compareErrorArrays(prevProps.errors, nextProps.errors);
|
||||
|
||||
// Check for error arrays equality - avoid JSON.stringify
|
||||
const errorsEqual = compareErrorArrays(prev.errors || [], next.errors || []);
|
||||
if (!errorsEqual) return false;
|
||||
// Shallow options comparison with length check
|
||||
const optionsEqual =
|
||||
prevProps.options === nextProps.options ||
|
||||
(Array.isArray(prevProps.options) &&
|
||||
Array.isArray(nextProps.options) &&
|
||||
prevProps.options.length === nextProps.options.length &&
|
||||
prevProps.options.every((opt, idx) => {
|
||||
// Handle safely when options might be undefined
|
||||
const nextOptions = nextProps.options || [];
|
||||
return opt === nextOptions[idx];
|
||||
}));
|
||||
|
||||
// Check options only when needed
|
||||
if (prev.field.fieldType.type === 'select' || prev.field.fieldType.type === 'multi-select') {
|
||||
if (prev.options !== next.options) {
|
||||
// Use safe defaults for options to handle undefined
|
||||
const prevOpts = prev.options || [];
|
||||
const nextOpts = next.options || [];
|
||||
|
||||
// Only do shallow comparison if references are different
|
||||
if (prevOpts.length !== nextOpts.length) return false;
|
||||
|
||||
// Quick length check before detailed comparison
|
||||
for (let i = 0; i < prevOpts.length; i++) {
|
||||
if (prevOpts[i] !== nextOpts[i]) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip comparison of props that rarely change
|
||||
// (rowIndex, width, copyDown, totalRows)
|
||||
|
||||
// For item numbers, check itemNumber equality
|
||||
if (prev.fieldKey === 'item_number' && prev.itemNumber !== next.itemNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we got this far, the props are equal
|
||||
return true;
|
||||
return valueEqual && isValidatingEqual && fieldEqual && errorsEqual &&
|
||||
optionsEqual && itemNumberEqual;
|
||||
});
|
||||
|
||||
// Helper function to compare error arrays efficiently
|
||||
function compareErrorArrays(prevErrors: ErrorObject[], nextErrors: ErrorObject[]): boolean {
|
||||
if (prevErrors === nextErrors) return true;
|
||||
if (prevErrors.length !== nextErrors.length) return false;
|
||||
|
||||
for (let i = 0; i < prevErrors.length; i++) {
|
||||
const prevError = prevErrors[i];
|
||||
const nextError = nextErrors[i];
|
||||
|
||||
if (prevError.message !== nextError.message ||
|
||||
prevError.level !== nextError.level ||
|
||||
prevError.source !== nextError.source) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
ValidationCell.displayName = 'ValidationCell';
|
||||
|
||||
export default ValidationCell;
|
||||
@@ -139,15 +139,30 @@ const MemoizedCell = React.memo(({
|
||||
/>
|
||||
);
|
||||
}, (prev, next) => {
|
||||
// Optimize the memo comparison function for better performance
|
||||
// Only re-render if these essential props change
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prev.isValidating === next.isValidating &&
|
||||
prev.itemNumber === next.itemNumber &&
|
||||
// Deep compare errors
|
||||
prev.errors === next.errors &&
|
||||
prev.options === next.options
|
||||
const valueEqual = prev.value === next.value;
|
||||
const isValidatingEqual = prev.isValidating === next.isValidating;
|
||||
const itemNumberEqual = prev.itemNumber === next.itemNumber;
|
||||
|
||||
// Shallow equality check for errors array
|
||||
const errorsEqual = prev.errors === next.errors || (
|
||||
Array.isArray(prev.errors) &&
|
||||
Array.isArray(next.errors) &&
|
||||
prev.errors.length === next.errors.length &&
|
||||
prev.errors.every((err, idx) => err === next.errors[idx])
|
||||
);
|
||||
|
||||
// Shallow equality check for options array
|
||||
const optionsEqual = prev.options === next.options || (
|
||||
Array.isArray(prev.options) &&
|
||||
Array.isArray(next.options) &&
|
||||
prev.options.length === next.options.length &&
|
||||
prev.options.every((opt, idx) => opt === next.options?.[idx])
|
||||
);
|
||||
|
||||
// Skip checking for props that rarely change
|
||||
return valueEqual && isValidatingEqual && errorsEqual && optionsEqual && itemNumberEqual;
|
||||
});
|
||||
|
||||
MemoizedCell.displayName = 'MemoizedCell';
|
||||
@@ -387,15 +402,13 @@ const ValidationTable = <T extends string>({
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: { rowSelection },
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: (row) => {
|
||||
if (row.__index) return row.__index;
|
||||
const index = data.indexOf(row);
|
||||
return String(index);
|
||||
}
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: useCallback((row: RowData<T>, index: number) => String(index), []),
|
||||
});
|
||||
|
||||
// Calculate total table width for stable horizontal scrolling
|
||||
@@ -508,7 +521,7 @@ const ValidationTable = <T extends string>({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Body */}
|
||||
{/* Table Body - Restore the original structure */}
|
||||
<Table style={{ width: `${totalWidth}px`, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, marginTop: '-1px' }}>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
@@ -517,19 +530,19 @@ const ValidationTable = <T extends string>({
|
||||
className={cn(
|
||||
"hover:bg-muted/50",
|
||||
row.getIsSelected() ? "bg-muted/50" : "",
|
||||
validationErrors.get(data.indexOf(row.original)) &&
|
||||
Object.keys(validationErrors.get(data.indexOf(row.original)) || {}).length > 0 ? "bg-red-50/40" : "",
|
||||
validationErrors.get(parseInt(row.id)) &&
|
||||
Object.keys(validationErrors.get(parseInt(row.id)) || {}).length > 0 ? "bg-red-50/40" : "",
|
||||
// Add cursor-pointer class when in copy down mode for target rows
|
||||
isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "cursor-pointer copy-down-target-row" : ""
|
||||
isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? "cursor-pointer copy-down-target-row" : ""
|
||||
)}
|
||||
style={{
|
||||
// Force cursor pointer on all target rows
|
||||
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined,
|
||||
cursor: isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? 'pointer' : undefined,
|
||||
position: 'relative' // Ensure we can position the overlay
|
||||
}}
|
||||
onMouseEnter={() => handleRowMouseEnter(row.index)}
|
||||
onMouseEnter={() => handleRowMouseEnter(parseInt(row.id))}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, cellIndex) => {
|
||||
{row.getVisibleCells().map((cell: any) => {
|
||||
const width = cell.column.getSize();
|
||||
return (
|
||||
<TableCell
|
||||
@@ -541,9 +554,9 @@ const ValidationTable = <T extends string>({
|
||||
boxSizing: 'border-box',
|
||||
padding: '0',
|
||||
// Force cursor pointer on all cells in target rows
|
||||
cursor: isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? 'pointer' : undefined
|
||||
cursor: isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? 'pointer' : undefined
|
||||
}}
|
||||
className={isInCopyDownMode && sourceRowIndex !== null && row.index > sourceRowIndex ? "target-row-cell" : ""}
|
||||
className={isInCopyDownMode && sourceRowIndex !== null && parseInt(row.id) > sourceRowIndex ? "target-row-cell" : ""}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
|
||||
@@ -228,164 +228,144 @@ export const useValidationState = <T extends string>({
|
||||
const pendingUpdatesRef = useRef<{
|
||||
errors: Map<number, Record<string, ValidationError[]>>,
|
||||
statuses: Map<number, 'pending' | 'validating' | 'validated' | 'error'>,
|
||||
data: Array<RowData<T>>
|
||||
data: Array<RowData<T>>,
|
||||
cells: Set<string>
|
||||
}>({
|
||||
errors: new Map(),
|
||||
statuses: new Map(),
|
||||
data: []
|
||||
data: [],
|
||||
cells: new Set()
|
||||
});
|
||||
|
||||
// Optimized batch update function
|
||||
// Optimized batch update function with single state update
|
||||
const flushPendingUpdates = useCallback(() => {
|
||||
const updates = pendingUpdatesRef.current;
|
||||
const hasDataUpdates = updates.data.length > 0;
|
||||
const hasErrorsUpdates = updates.errors.size > 0;
|
||||
const hasStatusUpdates = updates.statuses.size > 0;
|
||||
const hasCellUpdates = updates.cells.size > 0;
|
||||
|
||||
// Use a single setState call for validation errors if possible
|
||||
if (updates.errors.size > 0) {
|
||||
setValidationErrors(prev => {
|
||||
// Create a new Map only if we're modifying it
|
||||
const needsUpdate = Array.from(updates.errors.entries()).some(([rowIndex, errors]) => {
|
||||
const prevErrors = prev.get(rowIndex);
|
||||
const hasErrors = Object.keys(errors).length > 0;
|
||||
if (!hasDataUpdates && !hasErrorsUpdates && !hasStatusUpdates && !hasCellUpdates) {
|
||||
return; // No updates to process
|
||||
}
|
||||
|
||||
// Batch all state updates in a single callback
|
||||
// This minimizes React re-renders
|
||||
const batchedUpdates = () => {
|
||||
if (hasDataUpdates) {
|
||||
setData(prevData => {
|
||||
if (updates.data.length === 0) return prevData;
|
||||
|
||||
// Check if we need to update this row's errors
|
||||
if (!prevErrors && hasErrors) return true;
|
||||
if (prevErrors && !hasErrors) return true;
|
||||
if (!prevErrors && !hasErrors) return false;
|
||||
|
||||
// Check if the error objects are different
|
||||
return Object.keys(errors).some(key => {
|
||||
const prevError = prevErrors?.[key];
|
||||
const nextError = errors[key];
|
||||
|
||||
if (!prevError && nextError) return true;
|
||||
if (prevError && !nextError) return true;
|
||||
if (!prevError && !nextError) return false;
|
||||
|
||||
// Compare the arrays if both exist
|
||||
if (Array.isArray(prevError) && Array.isArray(nextError)) {
|
||||
if (prevError.length !== nextError.length) return true;
|
||||
|
||||
// Deep comparison of error objects
|
||||
return prevError.some((err, i) => {
|
||||
const nextErr = nextError[i];
|
||||
return err.message !== nextErr.message ||
|
||||
err.level !== nextErr.level ||
|
||||
err.source !== nextErr.source;
|
||||
});
|
||||
// Create a new array only if we need to change something
|
||||
const newData = [...prevData];
|
||||
for (const rowData of updates.data) {
|
||||
const index = rowData.__index ? parseInt(rowData.__index) : -1;
|
||||
if (index >= 0 && index < newData.length) {
|
||||
newData[index] = rowData;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// If no real changes, return the same state object
|
||||
if (!needsUpdate) return prev;
|
||||
|
||||
// Otherwise create a new Map with the updates
|
||||
const newErrors = new Map(prev);
|
||||
updates.errors.forEach((errors, rowIndex) => {
|
||||
if (Object.keys(errors).length === 0) {
|
||||
newErrors.delete(rowIndex);
|
||||
} else {
|
||||
newErrors.set(rowIndex, errors);
|
||||
}
|
||||
});
|
||||
|
||||
return newErrors;
|
||||
});
|
||||
|
||||
// Clear the updates
|
||||
updates.errors = new Map();
|
||||
}
|
||||
|
||||
// Use a single setState call for row validation statuses
|
||||
if (updates.statuses.size > 0) {
|
||||
setRowValidationStatus(prev => {
|
||||
// Check if we need to update
|
||||
const needsUpdate = Array.from(updates.statuses.entries()).some(([rowIndex, status]) => {
|
||||
return prev.get(rowIndex) !== status;
|
||||
});
|
||||
|
||||
// If no real changes, return the same state object
|
||||
if (!needsUpdate) return prev;
|
||||
|
||||
// Create a new Map with updates
|
||||
const newStatuses = new Map(prev);
|
||||
updates.statuses.forEach((status, rowIndex) => {
|
||||
newStatuses.set(rowIndex, status);
|
||||
});
|
||||
|
||||
return newStatuses;
|
||||
});
|
||||
|
||||
// Clear the updates
|
||||
updates.statuses = new Map();
|
||||
}
|
||||
|
||||
// Use a single setState call for data updates
|
||||
if (updates.data.length > 0) {
|
||||
// Find non-empty items
|
||||
const dataUpdates = updates.data.filter(item => item !== undefined);
|
||||
|
||||
if (dataUpdates.length > 0) {
|
||||
setData(prev => {
|
||||
// Check if we actually need to update
|
||||
const needsUpdate = dataUpdates.some((row, index) => {
|
||||
const oldRow = prev[index];
|
||||
if (!oldRow) return true;
|
||||
|
||||
// Compare the rows
|
||||
return Object.keys(row).some(key => {
|
||||
// Skip meta fields that don't affect rendering
|
||||
if (key.startsWith('__') && key !== '__template') return false;
|
||||
|
||||
return oldRow[key] !== row[key];
|
||||
});
|
||||
});
|
||||
|
||||
// If no actual changes, return the same array
|
||||
if (!needsUpdate) return prev;
|
||||
|
||||
// Create a new array with the updates
|
||||
const newData = [...prev];
|
||||
dataUpdates.forEach((row, index) => {
|
||||
if (index < newData.length) {
|
||||
newData[index] = row;
|
||||
}
|
||||
});
|
||||
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
// Clear the updates
|
||||
updates.data = [];
|
||||
}
|
||||
if (hasErrorsUpdates) {
|
||||
setValidationErrors(prev => {
|
||||
if (updates.errors.size === 0) return prev;
|
||||
|
||||
// Create a new map
|
||||
const newErrors = new Map(prev);
|
||||
for (const [rowIndex, errors] of updates.errors.entries()) {
|
||||
newErrors.set(rowIndex, errors);
|
||||
}
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
|
||||
if (hasStatusUpdates) {
|
||||
setRowValidationStatus(prev => {
|
||||
if (updates.statuses.size === 0) return prev;
|
||||
|
||||
// Create a new map
|
||||
const newStatuses = new Map(prev);
|
||||
for (const [rowIndex, status] of updates.statuses.entries()) {
|
||||
newStatuses.set(rowIndex, status);
|
||||
}
|
||||
return newStatuses;
|
||||
});
|
||||
}
|
||||
|
||||
if (hasCellUpdates) {
|
||||
setValidatingCells(prev => {
|
||||
if (updates.cells.size === 0) return prev;
|
||||
|
||||
// Create a new set
|
||||
return new Set([...prev, ...updates.cells]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Use React's batching mechanism
|
||||
batchedUpdates();
|
||||
|
||||
// Reset pending updates
|
||||
pendingUpdatesRef.current = {
|
||||
errors: new Map(),
|
||||
statuses: new Map(),
|
||||
data: [],
|
||||
cells: new Set()
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Debounced flush updates
|
||||
const debouncedFlushUpdates = useMemo(
|
||||
() => debounce(flushPendingUpdates, DEBOUNCE_DELAY),
|
||||
[flushPendingUpdates]
|
||||
);
|
||||
// Queue a row update to be processed in batch
|
||||
const queueRowUpdate = useCallback((rowIndex: number, key: T, value: any) => {
|
||||
const updates = pendingUpdatesRef.current;
|
||||
|
||||
// Find the row to update
|
||||
let rowToUpdate: RowData<T> | undefined = undefined;
|
||||
for (const row of updates.data) {
|
||||
if (row.__index === String(rowIndex)) {
|
||||
rowToUpdate = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, look in the current data state
|
||||
if (!rowToUpdate) {
|
||||
const currentRow = data[rowIndex];
|
||||
if (!currentRow) return;
|
||||
|
||||
// Create a copy and add to pending updates
|
||||
rowToUpdate = { ...currentRow };
|
||||
updates.data.push(rowToUpdate);
|
||||
}
|
||||
|
||||
// Update the value
|
||||
rowToUpdate[key] = value;
|
||||
|
||||
// Mark as changed
|
||||
if (!rowToUpdate.__changes) {
|
||||
rowToUpdate.__changes = {};
|
||||
}
|
||||
rowToUpdate.__changes[key as string] = true;
|
||||
|
||||
// Queue cell for validation
|
||||
updates.cells.add(`${rowIndex}:${key}`);
|
||||
|
||||
// Schedule a flush
|
||||
if (DEBOUNCE_DELAY <= 0) {
|
||||
// No delay, flush immediately
|
||||
flushPendingUpdates();
|
||||
} else {
|
||||
// Use setTimeout instead of debounce utility for more flexibility
|
||||
setTimeout(flushPendingUpdates, DEBOUNCE_DELAY);
|
||||
}
|
||||
}, [data, flushPendingUpdates]);
|
||||
|
||||
// Queue updates instead of immediate setState calls
|
||||
const queueUpdate = useCallback((rowIndex: number, updates: {
|
||||
errors?: Record<string, ValidationError[]>,
|
||||
status?: 'pending' | 'validating' | 'validated' | 'error',
|
||||
data?: RowData<T>
|
||||
}) => {
|
||||
if (updates.errors) {
|
||||
pendingUpdatesRef.current.errors.set(rowIndex, updates.errors);
|
||||
}
|
||||
if (updates.status) {
|
||||
pendingUpdatesRef.current.statuses.set(rowIndex, updates.status);
|
||||
}
|
||||
if (updates.data) {
|
||||
pendingUpdatesRef.current.data[rowIndex] = updates.data;
|
||||
}
|
||||
debouncedFlushUpdates();
|
||||
}, [debouncedFlushUpdates]);
|
||||
// Replace the existing updateRow function
|
||||
const updateRow = useCallback((rowIndex: number, key: T, value: any) => {
|
||||
// Use the optimized queue mechanism
|
||||
queueRowUpdate(rowIndex, key, value);
|
||||
}, [queueRowUpdate]);
|
||||
|
||||
// Update validateUniqueItemNumbers to use batch updates
|
||||
const validateUniqueItemNumbers = useCallback(async () => {
|
||||
@@ -848,135 +828,6 @@ export const useValidationState = <T extends string>({
|
||||
}
|
||||
}, [data, fields, validateField, rowHook]);
|
||||
|
||||
// Update a row's field value
|
||||
const updateRow = useCallback((rowIndex: number, key: T, value: any) => {
|
||||
// Save current scroll position
|
||||
const scrollPosition = {
|
||||
left: window.scrollX,
|
||||
top: window.scrollY
|
||||
};
|
||||
|
||||
// Track the cell as validating
|
||||
setValidatingCells(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(`${rowIndex}-${key}`);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Process the value based on field type
|
||||
const field = fields.find(f => f.key === key);
|
||||
let processedValue = value;
|
||||
|
||||
// Special handling for price fields
|
||||
if (field &&
|
||||
typeof field.fieldType === 'object' &&
|
||||
(field.fieldType.type === 'input' || field.fieldType.type === 'multi-input') &&
|
||||
'price' in field.fieldType &&
|
||||
field.fieldType.price === true &&
|
||||
typeof value === 'string') {
|
||||
// Remove $ and commas
|
||||
processedValue = value.replace(/[$,\s]/g, '').trim();
|
||||
|
||||
// Convert to number if possible
|
||||
const numValue = parseFloat(processedValue);
|
||||
if (!isNaN(numValue)) {
|
||||
processedValue = numValue.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the data
|
||||
setData(prev => {
|
||||
const newData = [...prev];
|
||||
if (rowIndex >= 0 && rowIndex < newData.length) {
|
||||
// Create a new row object without modifying the original
|
||||
const updatedRow = { ...newData[rowIndex] };
|
||||
|
||||
// Update the field value
|
||||
updatedRow[key] = processedValue;
|
||||
|
||||
// Track changes from original
|
||||
if (!updatedRow.__original) {
|
||||
updatedRow.__original = { ...updatedRow };
|
||||
}
|
||||
|
||||
if (!updatedRow.__changes) {
|
||||
updatedRow.__changes = {};
|
||||
}
|
||||
|
||||
// Record this change
|
||||
updatedRow.__changes[key] = true;
|
||||
|
||||
// Replace the row in the data array
|
||||
newData[rowIndex] = updatedRow;
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
|
||||
// Restore scroll position
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo(scrollPosition.left, scrollPosition.top);
|
||||
});
|
||||
|
||||
// Validate the updated field
|
||||
if (field) {
|
||||
// Clear any existing validation errors for this field
|
||||
setValidationErrors(prev => {
|
||||
const rowErrors = prev.get(rowIndex) || {};
|
||||
const newRowErrors = { ...rowErrors };
|
||||
|
||||
// Remove errors for this field
|
||||
delete newRowErrors[String(key)];
|
||||
|
||||
// Update the map
|
||||
const newMap = new Map(prev);
|
||||
if (Object.keys(newRowErrors).length > 0) {
|
||||
newMap.set(rowIndex, newRowErrors);
|
||||
} else {
|
||||
newMap.delete(rowIndex);
|
||||
}
|
||||
|
||||
return newMap;
|
||||
});
|
||||
|
||||
// Validate the field
|
||||
const errors = validateField(processedValue, field as Field<T>);
|
||||
|
||||
// If there are errors, update the validation errors
|
||||
if (errors.length > 0) {
|
||||
setValidationErrors(prev => {
|
||||
const rowErrors = prev.get(rowIndex) || {};
|
||||
const newRowErrors = { ...rowErrors, [String(key)]: errors };
|
||||
|
||||
// Update the map
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(rowIndex, newRowErrors);
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the cell as no longer validating
|
||||
setValidatingCells(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(`${rowIndex}-${key}`);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Call field's onChange handler if it exists
|
||||
if (field && field.onChange) {
|
||||
field.onChange(processedValue, data[rowIndex]);
|
||||
}
|
||||
|
||||
// Schedule validation for the entire row
|
||||
const timeoutId = setTimeout(() => {
|
||||
validateRow(rowIndex);
|
||||
}, 300);
|
||||
|
||||
// Store the timeout ID for cleanup
|
||||
validationTimeoutsRef.current[rowIndex] = timeoutId;
|
||||
}, [data, fields, validateField, validateRow]);
|
||||
|
||||
// Copy a cell value to all cells below it in the same column
|
||||
const copyDown = useCallback((rowIndex: number, key: T) => {
|
||||
// Get the source value to copy
|
||||
|
||||
Reference in New Issue
Block a user