Product import speed/responsiveness fixes, particularly around validation
This commit is contained in:
@@ -20,6 +20,8 @@ interface UpcValidationTableAdapterProps<T extends string> {
|
|||||||
copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void
|
copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void
|
||||||
validatingCells: Set<string>
|
validatingCells: Set<string>
|
||||||
isLoadingTemplates: boolean
|
isLoadingTemplates: boolean
|
||||||
|
editingCells: Set<string>
|
||||||
|
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||||
rowProductLines: Record<string, any[]>
|
rowProductLines: Record<string, any[]>
|
||||||
rowSublines: Record<string, any[]>
|
rowSublines: Record<string, any[]>
|
||||||
isLoadingLines: Record<string, boolean>
|
isLoadingLines: Record<string, boolean>
|
||||||
@@ -53,6 +55,8 @@ function UpcValidationTableAdapter<T extends string>({
|
|||||||
copyDown,
|
copyDown,
|
||||||
validatingCells: externalValidatingCells,
|
validatingCells: externalValidatingCells,
|
||||||
isLoadingTemplates,
|
isLoadingTemplates,
|
||||||
|
editingCells,
|
||||||
|
setEditingCells,
|
||||||
rowProductLines,
|
rowProductLines,
|
||||||
rowSublines,
|
rowSublines,
|
||||||
isLoadingLines,
|
isLoadingLines,
|
||||||
@@ -86,11 +90,7 @@ function UpcValidationTableAdapter<T extends string>({
|
|||||||
|
|
||||||
// First add from itemNumbers directly - this is the source of truth for template applications
|
// First add from itemNumbers directly - this is the source of truth for template applications
|
||||||
if (itemNumbers) {
|
if (itemNumbers) {
|
||||||
// Log all numbers for debugging
|
|
||||||
console.log(`[ADAPTER-DEBUG] Received itemNumbers map with ${itemNumbers.size} entries`);
|
|
||||||
|
|
||||||
itemNumbers.forEach((itemNumber, rowIndex) => {
|
itemNumbers.forEach((itemNumber, rowIndex) => {
|
||||||
console.log(`[ADAPTER-DEBUG] Adding item number for row ${rowIndex} from itemNumbers: ${itemNumber}`);
|
|
||||||
result.set(rowIndex, itemNumber);
|
result.set(rowIndex, itemNumber);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -100,14 +100,12 @@ function UpcValidationTableAdapter<T extends string>({
|
|||||||
// Check if upcValidation has an item number for this row
|
// Check if upcValidation has an item number for this row
|
||||||
const itemNumber = upcValidation.getItemNumber(index);
|
const itemNumber = upcValidation.getItemNumber(index);
|
||||||
if (itemNumber) {
|
if (itemNumber) {
|
||||||
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from upcValidation: ${itemNumber}`);
|
|
||||||
result.set(index, itemNumber);
|
result.set(index, itemNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check if it's directly in the data
|
// Also check if it's directly in the data
|
||||||
const dataItemNumber = data[index].item_number;
|
const dataItemNumber = data[index].item_number;
|
||||||
if (dataItemNumber && !result.has(index)) {
|
if (dataItemNumber && !result.has(index)) {
|
||||||
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from data: ${dataItemNumber}`);
|
|
||||||
result.set(index, dataItemNumber);
|
result.set(index, dataItemNumber);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -151,6 +149,8 @@ function UpcValidationTableAdapter<T extends string>({
|
|||||||
rowSublines={rowSublines}
|
rowSublines={rowSublines}
|
||||||
isLoadingLines={isLoadingLines}
|
isLoadingLines={isLoadingLines}
|
||||||
isLoadingSublines={isLoadingSublines}
|
isLoadingSublines={isLoadingSublines}
|
||||||
|
editingCells={editingCells}
|
||||||
|
setEditingCells={setEditingCells}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,9 @@ const BaseCellContent = React.memo(({
|
|||||||
hasErrors,
|
hasErrors,
|
||||||
options = [],
|
options = [],
|
||||||
className = '',
|
className = '',
|
||||||
fieldKey = ''
|
fieldKey = '',
|
||||||
|
onStartEdit,
|
||||||
|
onEndEdit
|
||||||
}: {
|
}: {
|
||||||
field: Field<string>;
|
field: Field<string>;
|
||||||
value: any;
|
value: any;
|
||||||
@@ -87,6 +89,8 @@ const BaseCellContent = React.memo(({
|
|||||||
options?: readonly any[];
|
options?: readonly any[];
|
||||||
className?: string;
|
className?: string;
|
||||||
fieldKey?: string;
|
fieldKey?: string;
|
||||||
|
onStartEdit?: () => void;
|
||||||
|
onEndEdit?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
// Get field type information
|
// Get field type information
|
||||||
const fieldType = fieldKey === 'line' || fieldKey === 'subline'
|
const fieldType = fieldKey === 'line' || fieldKey === 'subline'
|
||||||
@@ -113,6 +117,8 @@ const BaseCellContent = React.memo(({
|
|||||||
field={{...field, fieldType: { type: 'select', options }}}
|
field={{...field, fieldType: { type: 'select', options }}}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onEndEdit={onEndEdit}
|
||||||
options={options}
|
options={options}
|
||||||
hasErrors={hasErrors}
|
hasErrors={hasErrors}
|
||||||
className={className}
|
className={className}
|
||||||
@@ -127,6 +133,8 @@ const BaseCellContent = React.memo(({
|
|||||||
field={field}
|
field={field}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onEndEdit={onEndEdit}
|
||||||
options={options}
|
options={options}
|
||||||
hasErrors={hasErrors}
|
hasErrors={hasErrors}
|
||||||
className={className}
|
className={className}
|
||||||
@@ -141,6 +149,8 @@ const BaseCellContent = React.memo(({
|
|||||||
field={field}
|
field={field}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onEndEdit={onEndEdit}
|
||||||
options={options}
|
options={options}
|
||||||
hasErrors={hasErrors}
|
hasErrors={hasErrors}
|
||||||
className={className}
|
className={className}
|
||||||
@@ -154,6 +164,8 @@ const BaseCellContent = React.memo(({
|
|||||||
field={field}
|
field={field}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onStartEdit={onStartEdit}
|
||||||
|
onEndEdit={onEndEdit}
|
||||||
hasErrors={hasErrors}
|
hasErrors={hasErrors}
|
||||||
isMultiline={isMultiline}
|
isMultiline={isMultiline}
|
||||||
isPrice={isPrice}
|
isPrice={isPrice}
|
||||||
@@ -191,6 +203,8 @@ export interface ValidationCellProps {
|
|||||||
rowIndex: number
|
rowIndex: number
|
||||||
copyDown?: (endRowIndex?: number) => void
|
copyDown?: (endRowIndex?: number) => void
|
||||||
totalRows?: number
|
totalRows?: number
|
||||||
|
editingCells: Set<string>
|
||||||
|
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add efficient error message extraction function
|
// Add efficient error message extraction function
|
||||||
@@ -288,7 +302,9 @@ const ValidationCell = React.memo(({
|
|||||||
width,
|
width,
|
||||||
copyDown,
|
copyDown,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
totalRows = 0
|
totalRows = 0,
|
||||||
|
editingCells,
|
||||||
|
setEditingCells
|
||||||
}: ValidationCellProps) => {
|
}: ValidationCellProps) => {
|
||||||
// Use the CopyDown context
|
// Use the CopyDown context
|
||||||
const copyDownContext = React.useContext(CopyDownContext);
|
const copyDownContext = React.useContext(CopyDownContext);
|
||||||
@@ -297,9 +313,6 @@ const ValidationCell = React.memo(({
|
|||||||
// This ensures that when the itemNumber changes, the display value changes
|
// This ensures that when the itemNumber changes, the display value changes
|
||||||
let displayValue;
|
let displayValue;
|
||||||
if (fieldKey === 'item_number' && itemNumber) {
|
if (fieldKey === 'item_number' && itemNumber) {
|
||||||
// Always log when an item_number field is rendered to help debug
|
|
||||||
console.log(`[VC-DEBUG] ValidationCell rendering item_number for row=${rowIndex} with itemNumber=${itemNumber}, value=${value}`);
|
|
||||||
|
|
||||||
// Prioritize itemNumber prop for item_number fields
|
// Prioritize itemNumber prop for item_number fields
|
||||||
displayValue = itemNumber;
|
displayValue = itemNumber;
|
||||||
} else {
|
} else {
|
||||||
@@ -324,6 +337,22 @@ const ValidationCell = React.memo(({
|
|||||||
// Add state for hover on target row
|
// Add state for hover on target row
|
||||||
const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false);
|
const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false);
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Create cell key for editing state management
|
||||||
|
const cellKey = `${rowIndex}-${fieldKey}`;
|
||||||
|
|
||||||
|
// SINGLE-CLICK EDITING FIX: Create editing state management functions
|
||||||
|
const handleStartEdit = React.useCallback(() => {
|
||||||
|
setEditingCells(prev => new Set([...prev, cellKey]));
|
||||||
|
}, [setEditingCells, cellKey]);
|
||||||
|
|
||||||
|
const handleEndEdit = React.useCallback(() => {
|
||||||
|
setEditingCells(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(cellKey);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, [setEditingCells, cellKey]);
|
||||||
|
|
||||||
// Force isValidating to be a boolean
|
// Force isValidating to be a boolean
|
||||||
const isLoading = isValidating === true;
|
const isLoading = isValidating === true;
|
||||||
|
|
||||||
@@ -461,6 +490,8 @@ const ValidationCell = React.memo(({
|
|||||||
options={options}
|
options={options}
|
||||||
className={cellClassName}
|
className={cellClassName}
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
|
onStartEdit={handleStartEdit}
|
||||||
|
onEndEdit={handleEndEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ const ValidationContainer = <T extends string>({
|
|||||||
fields,
|
fields,
|
||||||
isLoadingTemplates,
|
isLoadingTemplates,
|
||||||
validatingCells,
|
validatingCells,
|
||||||
setValidatingCells
|
setValidatingCells,
|
||||||
|
editingCells,
|
||||||
|
setEditingCells
|
||||||
} = validationState
|
} = validationState
|
||||||
|
|
||||||
// Use product lines fetching hook
|
// Use product lines fetching hook
|
||||||
@@ -941,6 +943,8 @@ const ValidationContainer = <T extends string>({
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
templates={templates}
|
templates={templates}
|
||||||
applyTemplate={applyTemplateWrapper}
|
applyTemplate={applyTemplateWrapper}
|
||||||
|
editingCells={editingCells}
|
||||||
|
setEditingCells={setEditingCells}
|
||||||
getTemplateDisplayText={getTemplateDisplayText}
|
getTemplateDisplayText={getTemplateDisplayText}
|
||||||
isValidatingUpc={isRowValidatingUpc}
|
isValidatingUpc={isRowValidatingUpc}
|
||||||
validatingUpcRows={Array.from(upcValidation.validatingRows)}
|
validatingUpcRows={Array.from(upcValidation.validatingRows)}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface ValidationTableProps<T extends string> {
|
|||||||
itemNumbers: Map<number, string>
|
itemNumbers: Map<number, string>
|
||||||
isLoadingTemplates?: boolean
|
isLoadingTemplates?: boolean
|
||||||
copyDown: (rowIndex: number, key: string, endRowIndex?: number) => void
|
copyDown: (rowIndex: number, key: string, endRowIndex?: number) => void
|
||||||
|
editingCells: Set<string>
|
||||||
|
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +108,9 @@ const MemoizedCell = React.memo(({
|
|||||||
width,
|
width,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
copyDown,
|
copyDown,
|
||||||
totalRows
|
totalRows,
|
||||||
|
editingCells,
|
||||||
|
setEditingCells
|
||||||
}: {
|
}: {
|
||||||
field: Field<string>,
|
field: Field<string>,
|
||||||
value: any,
|
value: any,
|
||||||
@@ -119,7 +123,9 @@ const MemoizedCell = React.memo(({
|
|||||||
width: number,
|
width: number,
|
||||||
rowIndex: number,
|
rowIndex: number,
|
||||||
copyDown?: (endRowIndex?: number) => void,
|
copyDown?: (endRowIndex?: number) => void,
|
||||||
totalRows: number
|
totalRows: number,
|
||||||
|
editingCells: Set<string>,
|
||||||
|
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ValidationCell
|
<ValidationCell
|
||||||
@@ -135,6 +141,8 @@ const MemoizedCell = React.memo(({
|
|||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
copyDown={copyDown}
|
copyDown={copyDown}
|
||||||
totalRows={totalRows}
|
totalRows={totalRows}
|
||||||
|
editingCells={editingCells}
|
||||||
|
setEditingCells={setEditingCells}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, (prev, next) => {
|
}, (prev, next) => {
|
||||||
@@ -146,6 +154,7 @@ const MemoizedCell = React.memo(({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Simplified memo comparison - most expensive checks removed
|
// Simplified memo comparison - most expensive checks removed
|
||||||
|
// Note: editingCells changes are not checked here as they need immediate re-renders
|
||||||
return prev.value === next.value &&
|
return prev.value === next.value &&
|
||||||
prev.isValidating === next.isValidating &&
|
prev.isValidating === next.isValidating &&
|
||||||
prev.errors === next.errors &&
|
prev.errors === next.errors &&
|
||||||
@@ -169,6 +178,8 @@ const ValidationTable = <T extends string>({
|
|||||||
itemNumbers,
|
itemNumbers,
|
||||||
isLoadingTemplates = false,
|
isLoadingTemplates = false,
|
||||||
copyDown,
|
copyDown,
|
||||||
|
editingCells,
|
||||||
|
setEditingCells,
|
||||||
rowProductLines = {},
|
rowProductLines = {},
|
||||||
rowSublines = {},
|
rowSublines = {},
|
||||||
isLoadingLines = {},
|
isLoadingLines = {},
|
||||||
@@ -463,6 +474,8 @@ const ValidationTable = <T extends string>({
|
|||||||
rowIndex={row.index}
|
rowIndex={row.index}
|
||||||
copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)}
|
copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)}
|
||||||
totalRows={data.length}
|
totalRows={data.length}
|
||||||
|
editingCells={editingCells}
|
||||||
|
setEditingCells={setEditingCells}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,10 +142,10 @@ const SelectCell = <T extends string>({
|
|||||||
// 5. Call onChange synchronously to avoid race conditions with other cells
|
// 5. Call onChange synchronously to avoid race conditions with other cells
|
||||||
onChange(valueToCommit);
|
onChange(valueToCommit);
|
||||||
|
|
||||||
// 6. Clear processing state after a short delay
|
// 6. Clear processing state after a short delay - reduced for responsiveness
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}, 200);
|
}, 50);
|
||||||
}, [onChange, onEndEdit]);
|
}, [onChange, onEndEdit]);
|
||||||
|
|
||||||
// If disabled, render a static view
|
// If disabled, render a static view
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const useRowOperations = <T extends string>(
|
|||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 5); // Reduced delay for faster secondary effects
|
||||||
},
|
},
|
||||||
[data, fields, validateFieldFromHook, setData, setValidationErrors]
|
[data, fields, validateFieldFromHook, setData, setValidationErrors]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ export const useUniqueItemNumbersValidation = <T extends string>(
|
|||||||
) => {
|
) => {
|
||||||
// Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode
|
// Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode
|
||||||
const validateUniqueItemNumbers = useCallback(async () => {
|
const validateUniqueItemNumbers = useCallback(async () => {
|
||||||
console.log("Validating unique fields");
|
|
||||||
|
|
||||||
// Skip if no data
|
// Skip if no data
|
||||||
if (!data.length) return;
|
if (!data.length) return;
|
||||||
|
|
||||||
@@ -23,11 +21,6 @@ export const useUniqueItemNumbersValidation = <T extends string>(
|
|||||||
.filter((field) => field.validations?.some((v) => v.rule === "unique"))
|
.filter((field) => field.validations?.some((v) => v.rule === "unique"))
|
||||||
.map((field) => String(field.key));
|
.map((field) => String(field.key));
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Found ${uniqueFields.length} fields requiring uniqueness validation:`,
|
|
||||||
uniqueFields
|
|
||||||
);
|
|
||||||
|
|
||||||
// Always check item_number uniqueness even if not explicitly defined
|
// Always check item_number uniqueness even if not explicitly defined
|
||||||
if (!uniqueFields.includes("item_number")) {
|
if (!uniqueFields.includes("item_number")) {
|
||||||
uniqueFields.push("item_number");
|
uniqueFields.push("item_number");
|
||||||
@@ -41,32 +34,44 @@ export const useUniqueItemNumbersValidation = <T extends string>(
|
|||||||
// Initialize batch updates
|
// Initialize batch updates
|
||||||
const errors = new Map<number, Record<string, ValidationError[]>>();
|
const errors = new Map<number, Record<string, ValidationError[]>>();
|
||||||
|
|
||||||
// Single pass through data to identify all unique values
|
// ASYNC: Single pass through data to identify all unique values in batches
|
||||||
data.forEach((row, index) => {
|
const BATCH_SIZE = 20;
|
||||||
uniqueFields.forEach((fieldKey) => {
|
for (let batchStart = 0; batchStart < data.length; batchStart += BATCH_SIZE) {
|
||||||
const value = row[fieldKey as keyof typeof row];
|
const batchEnd = Math.min(batchStart + BATCH_SIZE, data.length);
|
||||||
|
|
||||||
|
for (let index = batchStart; index < batchEnd; index++) {
|
||||||
|
const row = data[index];
|
||||||
|
uniqueFields.forEach((fieldKey) => {
|
||||||
|
const value = row[fieldKey as keyof typeof row];
|
||||||
|
|
||||||
// Skip empty values
|
// Skip empty values
|
||||||
if (value === undefined || value === null || value === "") {
|
if (value === undefined || value === null || value === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueStr = String(value);
|
const valueStr = String(value);
|
||||||
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
||||||
|
|
||||||
if (fieldMap) {
|
if (fieldMap) {
|
||||||
// Get or initialize the array of indices for this value
|
// Get or initialize the array of indices for this value
|
||||||
const indices = fieldMap.get(valueStr) || [];
|
const indices = fieldMap.get(valueStr) || [];
|
||||||
indices.push(index);
|
indices.push(index);
|
||||||
fieldMap.set(valueStr, indices);
|
fieldMap.set(valueStr, indices);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Yield control back to UI thread after each batch
|
||||||
|
if (batchEnd < data.length) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process duplicates
|
// ASYNC: Process duplicates in batches to prevent UI blocking
|
||||||
uniqueFields.forEach((fieldKey) => {
|
let processedFields = 0;
|
||||||
|
for (const fieldKey of uniqueFields) {
|
||||||
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
const fieldMap = uniqueFieldsMap.get(fieldKey);
|
||||||
if (!fieldMap) return;
|
if (!fieldMap) continue;
|
||||||
|
|
||||||
fieldMap.forEach((indices, value) => {
|
fieldMap.forEach((indices, value) => {
|
||||||
// Only process if there are duplicates
|
// Only process if there are duplicates
|
||||||
@@ -93,7 +98,13 @@ export const useUniqueItemNumbersValidation = <T extends string>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
processedFields++;
|
||||||
|
// Yield control after every few fields to prevent UI blocking
|
||||||
|
if (processedFields % 2 === 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Merge uniqueness errors with existing validation errors
|
// Merge uniqueness errors with existing validation errors
|
||||||
setValidationErrors((prev) => {
|
setValidationErrors((prev) => {
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ export const useValidationState = <T extends string>({
|
|||||||
|
|
||||||
// Add state for tracking cells in loading state
|
// Add state for tracking cells in loading state
|
||||||
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set());
|
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Add global editing state to prevent validation during editing
|
||||||
|
const [editingCells, setEditingCells] = useState<Set<string>>(new Set());
|
||||||
|
const hasEditingCells = editingCells.size > 0;
|
||||||
|
|
||||||
const initialValidationDoneRef = useRef(false);
|
const initialValidationDoneRef = useRef(false);
|
||||||
const isValidatingRef = useRef(false);
|
const isValidatingRef = useRef(false);
|
||||||
@@ -138,16 +142,20 @@ export const useValidationState = <T extends string>({
|
|||||||
|
|
||||||
// CRITICAL FIX: Skip validation if we're already validating to prevent infinite loops
|
// CRITICAL FIX: Skip validation if we're already validating to prevent infinite loops
|
||||||
if (isValidatingRef.current) return;
|
if (isValidatingRef.current) return;
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Skip validation while cells are being edited
|
||||||
|
if (hasEditingCells) return;
|
||||||
|
|
||||||
// Debounce validation to prevent excessive calls
|
// Debounce validation to prevent excessive calls - reduced for better responsiveness
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (isValidatingRef.current) return; // Double-check before proceeding
|
if (isValidatingRef.current) return; // Double-check before proceeding
|
||||||
|
if (hasEditingCells) return; // Double-check editing state
|
||||||
|
|
||||||
// Validation running (removed console.log for performance)
|
// Validation running (removed console.log for performance)
|
||||||
isValidatingRef.current = true;
|
isValidatingRef.current = true;
|
||||||
|
|
||||||
// COMPREHENSIVE validation that clears old errors and adds new ones
|
// ASYNC validation that clears old errors and adds new ones
|
||||||
const validateFields = () => {
|
const validateFields = async () => {
|
||||||
try {
|
try {
|
||||||
// Create a complete fresh validation map
|
// Create a complete fresh validation map
|
||||||
const allValidationErrors = new Map<number, Record<string, any[]>>();
|
const allValidationErrors = new Map<number, Record<string, any[]>>();
|
||||||
@@ -160,89 +168,103 @@ export const useValidationState = <T extends string>({
|
|||||||
field.validations?.some((v) => v.rule === "regex")
|
field.validations?.some((v) => v.rule === "regex")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validate each row completely
|
// ASYNC PROCESSING: Process rows in small batches to prevent UI blocking
|
||||||
data.forEach((row, rowIndex) => {
|
const BATCH_SIZE = 10; // Small batch size for responsiveness
|
||||||
const rowErrors: Record<string, any[]> = {};
|
const totalRows = data.length;
|
||||||
|
|
||||||
|
for (let batchStart = 0; batchStart < totalRows; batchStart += BATCH_SIZE) {
|
||||||
|
const batchEnd = Math.min(batchStart + BATCH_SIZE, totalRows);
|
||||||
|
|
||||||
|
// Process this batch synchronously (fast)
|
||||||
|
for (let rowIndex = batchStart; rowIndex < batchEnd; rowIndex++) {
|
||||||
|
const row = data[rowIndex];
|
||||||
|
const rowErrors: Record<string, any[]> = {};
|
||||||
|
|
||||||
// Check required fields
|
// Check required fields
|
||||||
requiredFields.forEach((field) => {
|
requiredFields.forEach((field) => {
|
||||||
const key = String(field.key);
|
const key = String(field.key);
|
||||||
const value = row[key as keyof typeof row];
|
const value = row[key as keyof typeof row];
|
||||||
|
|
||||||
// Check if field is empty
|
// Check if field is empty
|
||||||
if (value === undefined || value === null || value === "" ||
|
if (value === undefined || value === null || value === "" ||
|
||||||
(Array.isArray(value) && value.length === 0)) {
|
(Array.isArray(value) && value.length === 0)) {
|
||||||
const requiredValidation = field.validations?.find((v) => v.rule === "required");
|
const requiredValidation = field.validations?.find((v) => v.rule === "required");
|
||||||
rowErrors[key] = [
|
rowErrors[key] = [
|
||||||
{
|
{
|
||||||
message: requiredValidation?.errorMessage || "This field is required",
|
message: requiredValidation?.errorMessage || "This field is required",
|
||||||
level: requiredValidation?.level || "error",
|
level: requiredValidation?.level || "error",
|
||||||
source: "row",
|
source: "row",
|
||||||
type: "required",
|
type: "required",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check regex fields (only if they have values)
|
|
||||||
regexFields.forEach((field) => {
|
|
||||||
const key = String(field.key);
|
|
||||||
const value = row[key as keyof typeof row];
|
|
||||||
|
|
||||||
// Skip empty values for regex validation
|
|
||||||
if (value === undefined || value === null || value === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regexValidation = field.validations?.find((v) => v.rule === "regex");
|
|
||||||
if (regexValidation) {
|
|
||||||
try {
|
|
||||||
const regex = new RegExp(regexValidation.value, regexValidation.flags);
|
|
||||||
if (!regex.test(String(value))) {
|
|
||||||
// Only add regex error if no required error exists
|
|
||||||
if (!rowErrors[key]) {
|
|
||||||
rowErrors[key] = [
|
|
||||||
{
|
|
||||||
message: regexValidation.errorMessage,
|
|
||||||
level: regexValidation.level || "error",
|
|
||||||
source: "row",
|
|
||||||
type: "regex",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Invalid regex in validation:", error);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Only add to the map if there are actually errors
|
// Check regex fields (only if they have values)
|
||||||
if (Object.keys(rowErrors).length > 0) {
|
regexFields.forEach((field) => {
|
||||||
allValidationErrors.set(rowIndex, rowErrors);
|
const key = String(field.key);
|
||||||
|
const value = row[key as keyof typeof row];
|
||||||
|
|
||||||
|
// Skip empty values for regex validation
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexValidation = field.validations?.find((v) => v.rule === "regex");
|
||||||
|
if (regexValidation) {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(regexValidation.value, regexValidation.flags);
|
||||||
|
if (!regex.test(String(value))) {
|
||||||
|
// Only add regex error if no required error exists
|
||||||
|
if (!rowErrors[key]) {
|
||||||
|
rowErrors[key] = [
|
||||||
|
{
|
||||||
|
message: regexValidation.errorMessage,
|
||||||
|
level: regexValidation.level || "error",
|
||||||
|
source: "row",
|
||||||
|
type: "regex",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid regex in validation:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only add to the map if there are actually errors
|
||||||
|
if (Object.keys(rowErrors).length > 0) {
|
||||||
|
allValidationErrors.set(rowIndex, rowErrors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// CRITICAL: Yield control back to the UI thread after each batch
|
||||||
|
if (batchEnd < totalRows) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Replace validation errors completely (clears old ones)
|
// Replace validation errors completely (clears old ones)
|
||||||
setValidationErrors(allValidationErrors);
|
setValidationErrors(allValidationErrors);
|
||||||
|
|
||||||
// Run uniqueness validations after basic validation
|
// Run uniqueness validations after basic validation (also async)
|
||||||
validateUniqueItemNumbers();
|
setTimeout(() => validateUniqueItemNumbers(), 0);
|
||||||
} finally {
|
} finally {
|
||||||
// Always ensure the ref is reset, even if an error occurs
|
// Always ensure the ref is reset, even if an error occurs - reduced delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isValidatingRef.current = false;
|
isValidatingRef.current = false;
|
||||||
}, 100);
|
}, 10);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run validation immediately
|
// Run validation immediately (async)
|
||||||
validateFields();
|
validateFields();
|
||||||
}, 50); // 50ms debounce
|
}, 10); // Reduced debounce for better responsiveness
|
||||||
|
|
||||||
// Cleanup timeout on unmount or dependency change
|
// Cleanup timeout on unmount or dependency change
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [data, fields]); // Removed validateUniqueItemNumbers to prevent infinite loop
|
}, [data, fields, hasEditingCells]); // Added hasEditingCells to dependencies
|
||||||
|
|
||||||
// Add field options query
|
// Add field options query
|
||||||
const { data: fieldOptionsData } = useQuery({
|
const { data: fieldOptionsData } = useQuery({
|
||||||
@@ -689,6 +711,10 @@ export const useValidationState = <T extends string>({
|
|||||||
// CRITICAL: Export validatingCells to make it available to ValidationContainer
|
// CRITICAL: Export validatingCells to make it available to ValidationContainer
|
||||||
validatingCells,
|
validatingCells,
|
||||||
setValidatingCells,
|
setValidatingCells,
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Export editing state management
|
||||||
|
editingCells,
|
||||||
|
setEditingCells,
|
||||||
|
|
||||||
// Row selection
|
// Row selection
|
||||||
rowSelection,
|
rowSelection,
|
||||||
|
|||||||
Reference in New Issue
Block a user