From 54f55b06a196b695f0d92b6fc520c1d4a41d2b47 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 6 Sep 2025 16:15:00 -0400 Subject: [PATCH] More validation fixes and enhancements --- .../components/ValidationContainer.tsx | 78 +++++++------------ .../components/ValidationTable.tsx | 8 +- .../hooks/useFilterManagement.tsx | 11 ++- .../hooks/useRowOperations.tsx | 4 +- 4 files changed, 46 insertions(+), 55 deletions(-) diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx index 9c7adf3..478437e 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -123,26 +123,40 @@ const ValidationContainer = ({ // Function to mark a row for revalidation const markRowForRevalidation = useCallback((rowIndex: number, fieldKey?: string) => { + // Map filtered rowIndex to original data index via __index + const originalIndex = (() => { + try { + const row = filteredData[rowIndex]; + if (!row) return rowIndex; + const id = row.__index; + if (!id) return rowIndex; + const idx = data.findIndex(r => r.__index === id); + return idx >= 0 ? idx : rowIndex; + } catch { + return rowIndex; + } + })(); + setFieldsToRevalidate(prev => { const newSet = new Set(prev); - newSet.add(rowIndex); + newSet.add(originalIndex); return newSet; }); - + // Also track which specific field needs to be revalidated if (fieldKey) { setFieldsToRevalidateMap(prev => { const newMap = { ...prev }; - if (!newMap[rowIndex]) { - newMap[rowIndex] = []; + if (!newMap[originalIndex]) { + newMap[originalIndex] = []; } - if (!newMap[rowIndex].includes(fieldKey)) { - newMap[rowIndex] = [...newMap[rowIndex], fieldKey]; + if (!newMap[originalIndex].includes(fieldKey)) { + newMap[originalIndex] = [...newMap[originalIndex], fieldKey]; } return newMap; }); } - }, []); + }, [data, filteredData]); // Add a ref to track the last validation time @@ -488,52 +502,16 @@ const ValidationContainer = ({ // Detect if this is a direct item_number edit const isItemNumberEdit = key === 'item_number' as T; - // For item_number edits, we need special handling to ensure they persist + // For item_number edits, use core updateRow to atomically update + validate if (isItemNumberEdit) { - console.log(`Manual edit to item_number: ${value}`); - - // First, update data immediately to ensure edit takes effect - setData(prevData => { - const newData = [...prevData]; - if (originalIndex >= 0 && originalIndex < newData.length) { - newData[originalIndex] = { - ...newData[originalIndex], - [key]: processedValue - }; - } - return newData; - }); - - // Mark for revalidation after a delay to ensure data update completes first - setTimeout(() => { - markRowForRevalidation(rowIndex, key as string); - }, 0); - - // Return early to prevent double-updating + const idx = originalIndex >= 0 ? originalIndex : rowIndex; + validationState.updateRow(idx, key as unknown as any, processedValue); return; } - // For all other fields, use standard approach - // Always use setData for updating - immediate update for better UX - const updatedRow = { ...rowData, [key]: processedValue }; - - // Mark this row for revalidation to clear any existing errors - markRowForRevalidation(rowIndex, key as string); - - // Update the data immediately to show the change - setData(prevData => { - const newData = [...prevData]; - if (originalIndex >= 0 && originalIndex < newData.length) { - // Create a new row object with the updated field - newData[originalIndex] = { - ...newData[originalIndex], - [key]: processedValue - }; - } else { - console.error(`Invalid originalIndex: ${originalIndex}, data length: ${newData.length}`); - } - return newData; - }); + // For all other fields, use core updateRow for atomic update + validation + const idx = originalIndex >= 0 ? originalIndex : rowIndex; + validationState.updateRow(idx, key as unknown as any, processedValue); // Secondary effects - using requestAnimationFrame for better performance requestAnimationFrame(() => { @@ -1225,4 +1203,4 @@ const ValidationContainer = ({ ) } -export default ValidationContainer \ No newline at end of file +export default ValidationContainer diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx index ee251ca..0eba56c 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/components/ValidationTable.tsx @@ -24,6 +24,9 @@ type ErrorType = { source?: string; } +// Stable empty errors array to prevent unnecessary re-renders +const EMPTY_ERRORS: ErrorType[] = Object.freeze([]); + interface ValidationTableProps { data: RowData[] fields: Fields @@ -421,7 +424,8 @@ const ValidationTable = ({ } // Get validation errors for this cell - const cellErrors = validationErrors.get(row.index)?.[fieldKey] || []; + // Use stable EMPTY_ERRORS to avoid new array creation on every render + const cellErrors = validationErrors.get(row.index)?.[fieldKey] || EMPTY_ERRORS; // Create a copy of the field with guaranteed field type for line and subline fields let fieldWithType = field; @@ -702,4 +706,4 @@ const areEqual = (prev: ValidationTableProps, next: ValidationTableProps( // Filter data based on current filter state const filteredData = useMemo(() => { + // Fast path: no filters active, return original data reference to avoid re-renders + const noSearch = !filters.searchText || filters.searchText.trim() === ''; + const noErrorsOnly = !filters.showErrorsOnly; + const noFieldFilter = !filters.filterField || !filters.filterValue || filters.filterValue.trim() === ''; + + if (noSearch && noErrorsOnly && noFieldFilter) { + return data; // preserve reference; prevents full table rerender on error map changes + } + return data.filter((row, index) => { // Filter by search text if (filters.searchText) { @@ -107,4 +116,4 @@ export const useFilterManagement = ( updateFilters, resetFilters }; -}; \ No newline at end of file +}; diff --git a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useRowOperations.tsx b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useRowOperations.tsx index 51ddbba..d1acce7 100644 --- a/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useRowOperations.tsx +++ b/inventory/src/components/product-import/steps/ValidationStepNew/hooks/useRowOperations.tsx @@ -301,8 +301,8 @@ export const useRowOperations = ( // Update with new validation results if (errors.length > 0) { newRowErrors[key as string] = errors; - } else if (!newRowErrors[key as string]) { - // If no errors found and no existing errors, ensure field is removed from errors + } else { + // Clear any existing errors for this field delete newRowErrors[key as string]; }