Product import speed/responsiveness fixes, particularly around validation

This commit is contained in:
2025-09-06 15:08:53 -04:00
parent 4dfe85231a
commit 5e2ee73e2d
8 changed files with 199 additions and 114 deletions

View File

@@ -20,6 +20,8 @@ interface UpcValidationTableAdapterProps<T extends string> {
copyDown: (rowIndex: number, fieldKey: string, endRowIndex?: number) => void
validatingCells: Set<string>
isLoadingTemplates: boolean
editingCells: Set<string>
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
rowProductLines: Record<string, any[]>
rowSublines: Record<string, any[]>
isLoadingLines: Record<string, boolean>
@@ -53,6 +55,8 @@ function UpcValidationTableAdapter<T extends string>({
copyDown,
validatingCells: externalValidatingCells,
isLoadingTemplates,
editingCells,
setEditingCells,
rowProductLines,
rowSublines,
isLoadingLines,
@@ -86,11 +90,7 @@ function UpcValidationTableAdapter<T extends string>({
// First add from itemNumbers directly - this is the source of truth for template applications
if (itemNumbers) {
// Log all numbers for debugging
console.log(`[ADAPTER-DEBUG] Received itemNumbers map with ${itemNumbers.size} entries`);
itemNumbers.forEach((itemNumber, rowIndex) => {
console.log(`[ADAPTER-DEBUG] Adding item number for row ${rowIndex} from itemNumbers: ${itemNumber}`);
result.set(rowIndex, itemNumber);
});
}
@@ -100,14 +100,12 @@ function UpcValidationTableAdapter<T extends string>({
// Check if upcValidation has an item number for this row
const itemNumber = upcValidation.getItemNumber(index);
if (itemNumber) {
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from upcValidation: ${itemNumber}`);
result.set(index, itemNumber);
}
// Also check if it's directly in the data
const dataItemNumber = data[index].item_number;
if (dataItemNumber && !result.has(index)) {
console.log(`[ADAPTER-DEBUG] Adding item number for row ${index} from data: ${dataItemNumber}`);
result.set(index, dataItemNumber);
}
});
@@ -151,6 +149,8 @@ function UpcValidationTableAdapter<T extends string>({
rowSublines={rowSublines}
isLoadingLines={isLoadingLines}
isLoadingSublines={isLoadingSublines}
editingCells={editingCells}
setEditingCells={setEditingCells}
/>
)
}

View File

@@ -78,7 +78,9 @@ const BaseCellContent = React.memo(({
hasErrors,
options = [],
className = '',
fieldKey = ''
fieldKey = '',
onStartEdit,
onEndEdit
}: {
field: Field<string>;
value: any;
@@ -87,6 +89,8 @@ const BaseCellContent = React.memo(({
options?: readonly any[];
className?: string;
fieldKey?: string;
onStartEdit?: () => void;
onEndEdit?: () => void;
}) => {
// Get field type information
const fieldType = fieldKey === 'line' || fieldKey === 'subline'
@@ -113,6 +117,8 @@ const BaseCellContent = React.memo(({
field={{...field, fieldType: { type: 'select', options }}}
value={value}
onChange={onChange}
onStartEdit={onStartEdit}
onEndEdit={onEndEdit}
options={options}
hasErrors={hasErrors}
className={className}
@@ -127,6 +133,8 @@ const BaseCellContent = React.memo(({
field={field}
value={value}
onChange={onChange}
onStartEdit={onStartEdit}
onEndEdit={onEndEdit}
options={options}
hasErrors={hasErrors}
className={className}
@@ -141,6 +149,8 @@ const BaseCellContent = React.memo(({
field={field}
value={value}
onChange={onChange}
onStartEdit={onStartEdit}
onEndEdit={onEndEdit}
options={options}
hasErrors={hasErrors}
className={className}
@@ -154,6 +164,8 @@ const BaseCellContent = React.memo(({
field={field}
value={value}
onChange={onChange}
onStartEdit={onStartEdit}
onEndEdit={onEndEdit}
hasErrors={hasErrors}
isMultiline={isMultiline}
isPrice={isPrice}
@@ -191,6 +203,8 @@ export interface ValidationCellProps {
rowIndex: number
copyDown?: (endRowIndex?: number) => void
totalRows?: number
editingCells: Set<string>
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
}
// Add efficient error message extraction function
@@ -288,7 +302,9 @@ const ValidationCell = React.memo(({
width,
copyDown,
rowIndex,
totalRows = 0
totalRows = 0,
editingCells,
setEditingCells
}: ValidationCellProps) => {
// Use the CopyDown context
const copyDownContext = React.useContext(CopyDownContext);
@@ -297,9 +313,6 @@ const ValidationCell = React.memo(({
// This ensures that when the itemNumber changes, the display value changes
let displayValue;
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
displayValue = itemNumber;
} else {
@@ -324,6 +337,22 @@ const ValidationCell = React.memo(({
// Add state for hover on target row
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
const isLoading = isValidating === true;
@@ -461,6 +490,8 @@ const ValidationCell = React.memo(({
options={options}
className={cellClassName}
fieldKey={fieldKey}
onStartEdit={handleStartEdit}
onEndEdit={handleEndEdit}
/>
</div>
)}

View File

@@ -61,7 +61,9 @@ const ValidationContainer = <T extends string>({
fields,
isLoadingTemplates,
validatingCells,
setValidatingCells
setValidatingCells,
editingCells,
setEditingCells
} = validationState
// Use product lines fetching hook
@@ -941,6 +943,8 @@ const ValidationContainer = <T extends string>({
filters={filters}
templates={templates}
applyTemplate={applyTemplateWrapper}
editingCells={editingCells}
setEditingCells={setEditingCells}
getTemplateDisplayText={getTemplateDisplayText}
isValidatingUpc={isRowValidatingUpc}
validatingUpcRows={Array.from(upcValidation.validatingRows)}

View File

@@ -46,6 +46,8 @@ interface ValidationTableProps<T extends string> {
itemNumbers: Map<number, string>
isLoadingTemplates?: boolean
copyDown: (rowIndex: number, key: string, endRowIndex?: number) => void
editingCells: Set<string>
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
[key: string]: any
}
@@ -106,7 +108,9 @@ const MemoizedCell = React.memo(({
width,
rowIndex,
copyDown,
totalRows
totalRows,
editingCells,
setEditingCells
}: {
field: Field<string>,
value: any,
@@ -119,7 +123,9 @@ const MemoizedCell = React.memo(({
width: number,
rowIndex: number,
copyDown?: (endRowIndex?: number) => void,
totalRows: number
totalRows: number,
editingCells: Set<string>,
setEditingCells: React.Dispatch<React.SetStateAction<Set<string>>>
}) => {
return (
<ValidationCell
@@ -135,6 +141,8 @@ const MemoizedCell = React.memo(({
rowIndex={rowIndex}
copyDown={copyDown}
totalRows={totalRows}
editingCells={editingCells}
setEditingCells={setEditingCells}
/>
);
}, (prev, next) => {
@@ -146,6 +154,7 @@ const MemoizedCell = React.memo(({
}
// 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 &&
prev.isValidating === next.isValidating &&
prev.errors === next.errors &&
@@ -169,6 +178,8 @@ const ValidationTable = <T extends string>({
itemNumbers,
isLoadingTemplates = false,
copyDown,
editingCells,
setEditingCells,
rowProductLines = {},
rowSublines = {},
isLoadingLines = {},
@@ -463,6 +474,8 @@ const ValidationTable = <T extends string>({
rowIndex={row.index}
copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)}
totalRows={data.length}
editingCells={editingCells}
setEditingCells={setEditingCells}
/>
);
}

View File

@@ -142,10 +142,10 @@ const SelectCell = <T extends string>({
// 5. Call onChange synchronously to avoid race conditions with other cells
onChange(valueToCommit);
// 6. Clear processing state after a short delay
// 6. Clear processing state after a short delay - reduced for responsiveness
setTimeout(() => {
setIsProcessing(false);
}, 200);
}, 50);
}, [onChange, onEndEdit]);
// If disabled, render a static view

View File

@@ -255,7 +255,7 @@ export const useRowOperations = <T extends string>(
return newData;
});
}
}, 50);
}, 5); // Reduced delay for faster secondary effects
},
[data, fields, validateFieldFromHook, setData, setValidationErrors]
);

View File

@@ -10,8 +10,6 @@ export const useUniqueItemNumbersValidation = <T extends string>(
) => {
// Update validateUniqueItemNumbers to also check for uniqueness of UPC/barcode
const validateUniqueItemNumbers = useCallback(async () => {
console.log("Validating unique fields");
// Skip if no data
if (!data.length) return;
@@ -23,11 +21,6 @@ export const useUniqueItemNumbersValidation = <T extends string>(
.filter((field) => field.validations?.some((v) => v.rule === "unique"))
.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
if (!uniqueFields.includes("item_number")) {
uniqueFields.push("item_number");
@@ -41,32 +34,44 @@ export const useUniqueItemNumbersValidation = <T extends string>(
// Initialize batch updates
const errors = new Map<number, Record<string, ValidationError[]>>();
// Single pass through data to identify all unique values
data.forEach((row, index) => {
uniqueFields.forEach((fieldKey) => {
const value = row[fieldKey as keyof typeof row];
// ASYNC: Single pass through data to identify all unique values in batches
const BATCH_SIZE = 20;
for (let batchStart = 0; batchStart < data.length; batchStart += BATCH_SIZE) {
const batchEnd = Math.min(batchStart + BATCH_SIZE, data.length);
// Skip empty values
if (value === undefined || value === null || value === "") {
return;
}
for (let index = batchStart; index < batchEnd; index++) {
const row = data[index];
uniqueFields.forEach((fieldKey) => {
const value = row[fieldKey as keyof typeof row];
const valueStr = String(value);
const fieldMap = uniqueFieldsMap.get(fieldKey);
// Skip empty values
if (value === undefined || value === null || value === "") {
return;
}
if (fieldMap) {
// Get or initialize the array of indices for this value
const indices = fieldMap.get(valueStr) || [];
indices.push(index);
fieldMap.set(valueStr, indices);
}
});
});
const valueStr = String(value);
const fieldMap = uniqueFieldsMap.get(fieldKey);
// Process duplicates
uniqueFields.forEach((fieldKey) => {
if (fieldMap) {
// Get or initialize the array of indices for this value
const indices = fieldMap.get(valueStr) || [];
indices.push(index);
fieldMap.set(valueStr, indices);
}
});
}
// Yield control back to UI thread after each batch
if (batchEnd < data.length) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// ASYNC: Process duplicates in batches to prevent UI blocking
let processedFields = 0;
for (const fieldKey of uniqueFields) {
const fieldMap = uniqueFieldsMap.get(fieldKey);
if (!fieldMap) return;
if (!fieldMap) continue;
fieldMap.forEach((indices, value) => {
// 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
setValidationErrors((prev) => {

View File

@@ -90,6 +90,10 @@ export const useValidationState = <T extends string>({
// Add state for tracking cells in loading state
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 isValidatingRef = useRef(false);
@@ -139,15 +143,19 @@ export const useValidationState = <T extends string>({
// CRITICAL FIX: Skip validation if we're already validating to prevent infinite loops
if (isValidatingRef.current) return;
// Debounce validation to prevent excessive calls
// PERFORMANCE FIX: Skip validation while cells are being edited
if (hasEditingCells) return;
// Debounce validation to prevent excessive calls - reduced for better responsiveness
const timeoutId = setTimeout(() => {
if (isValidatingRef.current) return; // Double-check before proceeding
if (hasEditingCells) return; // Double-check editing state
// Validation running (removed console.log for performance)
isValidatingRef.current = true;
// COMPREHENSIVE validation that clears old errors and adds new ones
const validateFields = () => {
// ASYNC validation that clears old errors and adds new ones
const validateFields = async () => {
try {
// Create a complete fresh validation map
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")
);
// Validate each row completely
data.forEach((row, rowIndex) => {
const rowErrors: Record<string, any[]> = {};
// ASYNC PROCESSING: Process rows in small batches to prevent UI blocking
const BATCH_SIZE = 10; // Small batch size for responsiveness
const totalRows = data.length;
// Check required fields
requiredFields.forEach((field) => {
const key = String(field.key);
const value = row[key as keyof typeof row];
for (let batchStart = 0; batchStart < totalRows; batchStart += BATCH_SIZE) {
const batchEnd = Math.min(batchStart + BATCH_SIZE, totalRows);
// Check if field is empty
if (value === undefined || value === null || value === "" ||
(Array.isArray(value) && value.length === 0)) {
const requiredValidation = field.validations?.find((v) => v.rule === "required");
rowErrors[key] = [
{
message: requiredValidation?.errorMessage || "This field is required",
level: requiredValidation?.level || "error",
source: "row",
type: "required",
},
];
}
});
// Process this batch synchronously (fast)
for (let rowIndex = batchStart; rowIndex < batchEnd; rowIndex++) {
const row = data[rowIndex];
const rowErrors: Record<string, any[]> = {};
// Check regex fields (only if they have values)
regexFields.forEach((field) => {
const key = String(field.key);
const value = row[key as keyof typeof row];
// Check required fields
requiredFields.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);
// Check if field is empty
if (value === undefined || value === null || value === "" ||
(Array.isArray(value) && value.length === 0)) {
const requiredValidation = field.validations?.find((v) => v.rule === "required");
rowErrors[key] = [
{
message: requiredValidation?.errorMessage || "This field is required",
level: requiredValidation?.level || "error",
source: "row",
type: "required",
},
];
}
}
});
});
// Only add to the map if there are actually errors
if (Object.keys(rowErrors).length > 0) {
allValidationErrors.set(rowIndex, rowErrors);
// 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
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)
setValidationErrors(allValidationErrors);
// Run uniqueness validations after basic validation
validateUniqueItemNumbers();
// Run uniqueness validations after basic validation (also async)
setTimeout(() => validateUniqueItemNumbers(), 0);
} 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(() => {
isValidatingRef.current = false;
}, 100);
}, 10);
}
};
// Run validation immediately
// Run validation immediately (async)
validateFields();
}, 50); // 50ms debounce
}, 10); // Reduced debounce for better responsiveness
// Cleanup timeout on unmount or dependency change
return () => clearTimeout(timeoutId);
}, [data, fields]); // Removed validateUniqueItemNumbers to prevent infinite loop
}, [data, fields, hasEditingCells]); // Added hasEditingCells to dependencies
// Add field options query
const { data: fieldOptionsData } = useQuery({
@@ -690,6 +712,10 @@ export const useValidationState = <T extends string>({
validatingCells,
setValidatingCells,
// PERFORMANCE FIX: Export editing state management
editingCells,
setEditingCells,
// Row selection
rowSelection,
setRowSelection,