Optimize validation table
This commit is contained in:
@@ -309,19 +309,37 @@ export default React.memo(ValidationCell, (prev, next) => {
|
||||
const prevErrorsStr = JSON.stringify(prev.errors);
|
||||
const nextErrorsStr = JSON.stringify(next.errors);
|
||||
|
||||
// For item number fields, compare itemNumber
|
||||
// Deep comparison of options
|
||||
const prevOptionsStr = JSON.stringify(prev.options);
|
||||
const nextOptionsStr = JSON.stringify(next.options);
|
||||
|
||||
// For validating cells, always re-render
|
||||
if (prev.isValidating !== next.isValidating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For item numbers, check if the item number changed
|
||||
if (prev.fieldKey === 'item_number') {
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prev.itemNumber === next.itemNumber &&
|
||||
prev.isValidating === next.isValidating
|
||||
prevErrorsStr === nextErrorsStr
|
||||
);
|
||||
}
|
||||
|
||||
// For all other fields, compare core props
|
||||
// For select and multi-select fields, check if options changed
|
||||
if (prev.field.fieldType.type === 'select' || prev.field.fieldType.type === 'multi-select') {
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prevErrorsStr === nextErrorsStr &&
|
||||
JSON.stringify(prev.options) === JSON.stringify(next.options)
|
||||
prevOptionsStr === nextOptionsStr
|
||||
);
|
||||
}
|
||||
|
||||
// For all other fields, check if value or errors changed
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prevErrorsStr === nextErrorsStr &&
|
||||
prev.width === next.width
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useRef, useEffect, useLayoutEffect } from 'react'
|
||||
import React, { useMemo, useRef, useEffect, useLayoutEffect, useCallback } from 'react'
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
@@ -78,15 +78,25 @@ const MemoizedCell = React.memo(({
|
||||
const rowErrors = validationErrors.get(rowIndex) || {};
|
||||
const fieldErrors = rowErrors[String(field.key)] || [];
|
||||
const isValidating = validatingCells.has(`${rowIndex}-${field.key}`);
|
||||
const options = field.fieldType.type === 'select' || field.fieldType.type === 'multi-select'
|
||||
? Array.from(field.fieldType.options || [])
|
||||
: [];
|
||||
|
||||
// Only compute options when needed for select/multi-select fields
|
||||
const options = useMemo(() => {
|
||||
if (field.fieldType.type === 'select' || field.fieldType.type === 'multi-select') {
|
||||
return Array.from((field.fieldType as any).options || []);
|
||||
}
|
||||
return [];
|
||||
}, [field.fieldType]);
|
||||
|
||||
// Memoize the onChange handler to prevent unnecessary re-renders
|
||||
const handleChange = useCallback((newValue: any) => {
|
||||
updateRow(rowIndex, field.key, newValue);
|
||||
}, [updateRow, rowIndex, field.key]);
|
||||
|
||||
return (
|
||||
<ValidationCell
|
||||
field={field}
|
||||
value={value}
|
||||
onChange={(newValue) => updateRow(rowIndex, field.key, newValue)}
|
||||
onChange={handleChange}
|
||||
errors={fieldErrors}
|
||||
isValidating={isValidating}
|
||||
fieldKey={String(field.key)}
|
||||
@@ -128,6 +138,17 @@ const MemoizedCell = React.memo(({
|
||||
const prevErrors = prev.validationErrors.get(prev.rowIndex)?.[fieldKey];
|
||||
const nextErrors = next.validationErrors.get(next.rowIndex)?.[fieldKey];
|
||||
|
||||
// For select/multi-select fields, also check if options changed
|
||||
if (prev.field.fieldType.type === 'select' || prev.field.fieldType.type === 'multi-select') {
|
||||
const prevOptions = (prev.field.fieldType as any).options;
|
||||
const nextOptions = (next.field.fieldType as any).options;
|
||||
|
||||
// If options length changed, we need to re-render
|
||||
if (prevOptions?.length !== nextOptions?.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
JSON.stringify(prevErrors) === JSON.stringify(nextErrors)
|
||||
|
||||
@@ -193,51 +193,30 @@ const MultiInputCell = <T extends string>({
|
||||
if (containerRef.current) {
|
||||
const container = containerRef.current;
|
||||
|
||||
// Force direct style properties using the DOM API
|
||||
container.style.setProperty('width', `${cellWidth}px`, 'important');
|
||||
container.style.setProperty('min-width', `${cellWidth}px`, 'important');
|
||||
container.style.setProperty('max-width', `${cellWidth}px`, 'important');
|
||||
container.style.setProperty('box-sizing', 'border-box', 'important');
|
||||
container.style.setProperty('display', 'inline-block', 'important');
|
||||
container.style.setProperty('flex', '0 0 auto', 'important');
|
||||
// Force direct style properties using the DOM API - simplified approach
|
||||
container.style.width = `${cellWidth}px`;
|
||||
container.style.minWidth = `${cellWidth}px`;
|
||||
container.style.maxWidth = `${cellWidth}px`;
|
||||
|
||||
// Apply to the button element as well
|
||||
const button = container.querySelector('button');
|
||||
if (button) {
|
||||
// Cast to HTMLElement to access style property
|
||||
const htmlButton = button as HTMLElement;
|
||||
htmlButton.style.setProperty('width', `${cellWidth}px`, 'important');
|
||||
htmlButton.style.setProperty('min-width', `${cellWidth}px`, 'important');
|
||||
htmlButton.style.setProperty('max-width', `${cellWidth}px`, 'important');
|
||||
|
||||
// Make sure flex layout is enforced
|
||||
const buttonContent = button.querySelector('div');
|
||||
if (buttonContent && buttonContent instanceof HTMLElement) {
|
||||
buttonContent.style.setProperty('display', 'flex', 'important');
|
||||
buttonContent.style.setProperty('align-items', 'center', 'important');
|
||||
buttonContent.style.setProperty('justify-content', 'space-between', 'important');
|
||||
buttonContent.style.setProperty('width', '100%', 'important');
|
||||
buttonContent.style.setProperty('overflow', 'hidden', 'important');
|
||||
}
|
||||
|
||||
// Find the chevron icon and ensure it's not wrapping
|
||||
const chevron = button.querySelector('svg');
|
||||
if (chevron && chevron instanceof SVGElement) {
|
||||
chevron.style.cssText = 'flex-shrink: 0 !important; margin-left: auto !important;';
|
||||
}
|
||||
htmlButton.style.width = `${cellWidth}px`;
|
||||
htmlButton.style.minWidth = `${cellWidth}px`;
|
||||
htmlButton.style.maxWidth = `${cellWidth}px`;
|
||||
}
|
||||
}
|
||||
}, [cellWidth]);
|
||||
|
||||
// Create a key-value map for inline styles with fixed width
|
||||
const fixedWidth = {
|
||||
// Create a key-value map for inline styles with fixed width - simplified
|
||||
const fixedWidth = useMemo(() => ({
|
||||
width: `${cellWidth}px`,
|
||||
minWidth: `${cellWidth}px`,
|
||||
maxWidth: `${cellWidth}px`,
|
||||
boxSizing: 'border-box' as const,
|
||||
display: 'inline-block',
|
||||
flex: '0 0 auto'
|
||||
};
|
||||
}), [cellWidth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -349,35 +328,30 @@ const MultiInputCell = <T extends string>({
|
||||
if (containerRef.current) {
|
||||
const container = containerRef.current;
|
||||
|
||||
// Force direct style properties using the DOM API
|
||||
container.style.setProperty('width', `${cellWidth}px`, 'important');
|
||||
container.style.setProperty('min-width', `${cellWidth}px`, 'important');
|
||||
container.style.setProperty('max-width', `${cellWidth}px`, 'important');
|
||||
container.style.setProperty('box-sizing', 'border-box', 'important');
|
||||
container.style.setProperty('display', 'inline-block', 'important');
|
||||
container.style.setProperty('flex', '0 0 auto', 'important');
|
||||
// Force direct style properties using the DOM API - simplified approach
|
||||
container.style.width = `${cellWidth}px`;
|
||||
container.style.minWidth = `${cellWidth}px`;
|
||||
container.style.maxWidth = `${cellWidth}px`;
|
||||
|
||||
// Apply to the input or div element as well
|
||||
const input = container.querySelector('textarea, div');
|
||||
if (input) {
|
||||
// Apply to the button element as well
|
||||
const button = container.querySelector('button');
|
||||
if (button) {
|
||||
// Cast to HTMLElement to access style property
|
||||
const htmlElement = input as HTMLElement;
|
||||
htmlElement.style.setProperty('width', `${cellWidth}px`, 'important');
|
||||
htmlElement.style.setProperty('min-width', `${cellWidth}px`, 'important');
|
||||
htmlElement.style.setProperty('max-width', `${cellWidth}px`, 'important');
|
||||
const htmlButton = button as HTMLElement;
|
||||
htmlButton.style.width = `${cellWidth}px`;
|
||||
htmlButton.style.minWidth = `${cellWidth}px`;
|
||||
htmlButton.style.maxWidth = `${cellWidth}px`;
|
||||
}
|
||||
}
|
||||
}, [cellWidth]);
|
||||
|
||||
// Create a key-value map for inline styles with fixed width
|
||||
const fixedWidth = {
|
||||
// Create a key-value map for inline styles with fixed width - simplified
|
||||
const fixedWidth = useMemo(() => ({
|
||||
width: `${cellWidth}px`,
|
||||
minWidth: `${cellWidth}px`,
|
||||
maxWidth: `${cellWidth}px`,
|
||||
boxSizing: 'border-box' as const,
|
||||
display: 'inline-block',
|
||||
flex: '0 0 auto'
|
||||
};
|
||||
}), [cellWidth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useCallback } from 'react'
|
||||
import { useState, useRef, useCallback, useMemo } from 'react'
|
||||
import { Field } from '../../../../types'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -45,29 +45,36 @@ const SelectCell = <T extends string>({
|
||||
// Ref for the command list to enable scrolling
|
||||
const commandListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Memoize options processing to avoid recalculation on every render
|
||||
const selectOptions = useMemo(() => {
|
||||
// Ensure we always have an array of options with the correct shape
|
||||
const fieldType = field.fieldType;
|
||||
const fieldOptions = fieldType &&
|
||||
(fieldType.type === 'select' || fieldType.type === 'multi-select') &&
|
||||
fieldType.options ?
|
||||
fieldType.options :
|
||||
(fieldType as any).options ?
|
||||
(fieldType as any).options :
|
||||
[];
|
||||
|
||||
// Always ensure selectOptions is a valid array with at least a default option
|
||||
const selectOptions = (options || fieldOptions || []).map(option => ({
|
||||
const processedOptions = (options || fieldOptions || []).map((option: any) => ({
|
||||
label: option.label || String(option.value),
|
||||
value: String(option.value)
|
||||
}));
|
||||
|
||||
if (selectOptions.length === 0) {
|
||||
if (processedOptions.length === 0) {
|
||||
// Add a default empty option if we have none
|
||||
selectOptions.push({ label: 'No options available', value: '' });
|
||||
processedOptions.push({ label: 'No options available', value: '' });
|
||||
}
|
||||
|
||||
// Get current display value
|
||||
const displayValue = value ?
|
||||
selectOptions.find(option => String(option.value) === String(value))?.label || String(value) :
|
||||
return processedOptions;
|
||||
}, [field.fieldType, options]);
|
||||
|
||||
// Memoize display value to avoid recalculation on every render
|
||||
const displayValue = useMemo(() => {
|
||||
return value ?
|
||||
selectOptions.find((option: SelectOption) => String(option.value) === String(value))?.label || String(value) :
|
||||
'Select...';
|
||||
}, [value, selectOptions]);
|
||||
|
||||
// Handle wheel scroll in dropdown
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
@@ -77,11 +84,27 @@ const SelectCell = <T extends string>({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelect = (selectedValue: string) => {
|
||||
const handleSelect = useCallback((selectedValue: string) => {
|
||||
onChange(selectedValue);
|
||||
setOpen(false);
|
||||
if (onEndEdit) onEndEdit();
|
||||
};
|
||||
}, [onChange, onEndEdit]);
|
||||
|
||||
// Memoize the command items to avoid recreating them on every render
|
||||
const commandItems = useMemo(() => {
|
||||
return selectOptions.map((option: SelectOption) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
{String(option.value) === String(value) && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
));
|
||||
}, [selectOptions, value, handleSelect]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -111,21 +134,11 @@ const SelectCell = <T extends string>({
|
||||
<CommandList
|
||||
ref={commandListRef}
|
||||
onWheel={handleWheel}
|
||||
className="max-h-[200px]"
|
||||
>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{selectOptions.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
{String(option.value) === String(value) && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
{commandItems}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
@@ -165,6 +165,7 @@ export const useValidationState = <T extends string>({
|
||||
const [isValidating] = useState(false)
|
||||
const [validationErrors, setValidationErrors] = useState<Map<number, Record<string, ErrorType[]>>>(new Map())
|
||||
const [rowValidationStatus, setRowValidationStatus] = useState<Map<number, 'pending' | 'validating' | 'validated' | 'error'>>(new Map())
|
||||
const [validatingCells, setValidatingCells] = useState<Set<string>>(new Set())
|
||||
|
||||
// Template state
|
||||
const [templates, setTemplates] = useState<Template[]>([])
|
||||
@@ -737,31 +738,36 @@ useEffect(() => {
|
||||
|
||||
// Validate a single row
|
||||
const validateRow = useCallback((rowIndex: number) => {
|
||||
// Skip if row doesn't exist or if we're in the middle of applying a template
|
||||
if (rowIndex < 0 || rowIndex >= data.length) return;
|
||||
// Skip if row doesn't exist
|
||||
if (!data[rowIndex]) return;
|
||||
|
||||
// Skip validation if we're applying a template
|
||||
if (isApplyingTemplateRef.current) {
|
||||
return true;
|
||||
}
|
||||
// Mark row as validating
|
||||
setRowValidationStatus(prev => {
|
||||
const updated = new Map(prev);
|
||||
updated.set(rowIndex, 'validating');
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Get the row data
|
||||
const row = data[rowIndex];
|
||||
const fieldErrors: Record<string, ErrorType[]> = {};
|
||||
let hasErrors = false;
|
||||
|
||||
// Ensure values are trimmed for proper validation
|
||||
const cleanedRow = { ...row };
|
||||
Object.entries(cleanedRow).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
(cleanedRow as any)[key] = value.trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Validate each field
|
||||
// Use a more efficient approach - only validate fields that need validation
|
||||
// This includes required fields and fields with values
|
||||
fields.forEach(field => {
|
||||
if (field.disabled) return;
|
||||
|
||||
const key = String(field.key);
|
||||
const value = cleanedRow[key as keyof typeof cleanedRow];
|
||||
const value = row[key as keyof typeof row];
|
||||
|
||||
// Skip validation for empty non-required fields
|
||||
const isRequired = field.validations?.some(v => v.rule === 'required');
|
||||
if (!isRequired && (value === undefined || value === null || value === '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the field
|
||||
const errors = validateField(value, field as Field<T>);
|
||||
if (errors.length > 0) {
|
||||
fieldErrors[key] = errors;
|
||||
@@ -769,8 +775,8 @@ useEffect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Special validation for supplier and company - check existence and non-emptiness
|
||||
if (!cleanedRow.supplier || (typeof cleanedRow.supplier === 'string' && cleanedRow.supplier.trim() === '')) {
|
||||
// Special validation for supplier and company
|
||||
if (!row.supplier) {
|
||||
fieldErrors['supplier'] = [{
|
||||
message: 'Supplier is required',
|
||||
level: 'error',
|
||||
@@ -778,8 +784,7 @@ useEffect(() => {
|
||||
}];
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (!cleanedRow.company || (typeof cleanedRow.company === 'string' && cleanedRow.company.trim() === '')) {
|
||||
if (!row.company) {
|
||||
fieldErrors['company'] = [{
|
||||
message: 'Company is required',
|
||||
level: 'error',
|
||||
@@ -788,64 +793,61 @@ useEffect(() => {
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Update validation state
|
||||
// Update validation errors for this row
|
||||
setValidationErrors(prev => {
|
||||
const newErrors = new Map(prev);
|
||||
if (hasErrors) {
|
||||
newErrors.set(rowIndex, fieldErrors);
|
||||
} else {
|
||||
newErrors.delete(rowIndex);
|
||||
}
|
||||
return newErrors;
|
||||
const updated = new Map(prev);
|
||||
updated.set(rowIndex, fieldErrors);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Update row validation status
|
||||
setRowValidationStatus(prev => {
|
||||
const newStatus = new Map(prev);
|
||||
newStatus.set(rowIndex, hasErrors ? 'error' : 'validated');
|
||||
return newStatus;
|
||||
const updated = new Map(prev);
|
||||
updated.set(rowIndex, hasErrors ? 'error' : 'validated');
|
||||
return updated;
|
||||
});
|
||||
}, [data, fields, validateField]);
|
||||
|
||||
// Update the row data with errors
|
||||
setData(prev => {
|
||||
const newData = [...prev];
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
__errors: hasErrors ? fieldErrors : undefined
|
||||
};
|
||||
return newData;
|
||||
});
|
||||
|
||||
return !hasErrors;
|
||||
}, [data, fields, validateField, setValidationErrors, setRowValidationStatus, setData]);
|
||||
|
||||
// Update a single row's field value
|
||||
// Update a row's field value
|
||||
const updateRow = useCallback((rowIndex: number, key: T, value: any) => {
|
||||
// Process value before updating data
|
||||
let processedValue = value;
|
||||
|
||||
// Strip dollar signs from price fields
|
||||
if ((key === 'msrp' || key === 'cost_each') && typeof value === 'string') {
|
||||
processedValue = value.replace(/[$,]/g, '');
|
||||
|
||||
// Also ensure it's a valid number
|
||||
const numValue = parseFloat(processedValue);
|
||||
if (!isNaN(numValue)) {
|
||||
processedValue = numValue.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Save current scroll position
|
||||
const scrollPosition = {
|
||||
left: window.scrollX,
|
||||
top: window.scrollY
|
||||
};
|
||||
|
||||
// Update the data immediately for UI responsiveness
|
||||
// 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 state
|
||||
setData(prevData => {
|
||||
const newData = [...prevData];
|
||||
|
||||
// Create a copy of the row to avoid reference issues
|
||||
const updatedRow = { ...newData[rowIndex] };
|
||||
const updatedRow = { ...newData[rowIndex] } as Record<string, any>;
|
||||
|
||||
// Update the field value
|
||||
updatedRow[key] = processedValue;
|
||||
@@ -869,12 +871,21 @@ useEffect(() => {
|
||||
window.scrollTo(scrollPosition.left, scrollPosition.top);
|
||||
});
|
||||
|
||||
// Validate the row after the update
|
||||
setTimeout(() => {
|
||||
// Use debounced validation to avoid excessive validation calls
|
||||
const shouldValidateUpc = (key === 'upc' || key === 'supplier');
|
||||
|
||||
// Clear any existing timeout for this row
|
||||
if (validationTimeoutsRef.current[rowIndex]) {
|
||||
clearTimeout(validationTimeoutsRef.current[rowIndex]);
|
||||
}
|
||||
|
||||
// Set a new timeout for validation
|
||||
validationTimeoutsRef.current[rowIndex] = setTimeout(() => {
|
||||
// Validate the row
|
||||
validateRow(rowIndex);
|
||||
|
||||
// Trigger UPC validation if applicable
|
||||
if ((key === 'upc' || key === 'supplier') && data[rowIndex]) {
|
||||
// Trigger UPC validation if applicable, but only if both fields are present
|
||||
if (shouldValidateUpc && data[rowIndex]) {
|
||||
const row = data[rowIndex];
|
||||
const upcValue = key === 'upc' ? processedValue : row.upc;
|
||||
const supplierValue = key === 'supplier' ? processedValue : row.supplier;
|
||||
@@ -883,8 +894,28 @@ useEffect(() => {
|
||||
validateUpc(rowIndex, String(supplierValue), String(upcValue));
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
}, [data, validateRow, validateUpc, setData, setRowValidationStatus, cleanPriceFields]);
|
||||
|
||||
// Remove the cell from validating state
|
||||
setValidatingCells(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(`${rowIndex}-${key}`);
|
||||
return newSet;
|
||||
});
|
||||
}, 300); // Increase debounce time to reduce validation frequency
|
||||
}, [data, validateRow, validateUpc, setData, setRowValidationStatus, cleanPriceFields, fields]);
|
||||
|
||||
// Add this at the top of the component, after other useRef declarations
|
||||
const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({});
|
||||
|
||||
// Clean up timeouts on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clear all validation timeouts
|
||||
Object.values(validationTimeoutsRef.current).forEach(timeout => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Save a new template
|
||||
const saveTemplate = useCallback(async (name: string, type: string) => {
|
||||
@@ -1286,14 +1317,20 @@ useEffect(() => {
|
||||
console.log(`Validating ${data.length} rows`);
|
||||
|
||||
// Process in batches to avoid blocking the UI
|
||||
const BATCH_SIZE = 50;
|
||||
const BATCH_SIZE = 100; // Increase batch size for better performance
|
||||
let currentBatch = 0;
|
||||
let totalBatches = Math.ceil(data.length / BATCH_SIZE);
|
||||
|
||||
const processBatch = () => {
|
||||
const startIdx = currentBatch * BATCH_SIZE;
|
||||
const endIdx = Math.min(startIdx + BATCH_SIZE, data.length);
|
||||
|
||||
// Create a batch of validation promises
|
||||
const batchPromises = [];
|
||||
|
||||
for (let rowIndex = startIdx; rowIndex < endIdx; rowIndex++) {
|
||||
batchPromises.push(
|
||||
new Promise<void>(resolve => {
|
||||
const row = data[rowIndex];
|
||||
const fieldErrors: Record<string, ErrorType[]> = {};
|
||||
let hasErrors = false;
|
||||
@@ -1322,10 +1359,18 @@ useEffect(() => {
|
||||
newData[rowIndex] = cleanedRow;
|
||||
}
|
||||
|
||||
// Only validate required fields and fields with values
|
||||
fields.forEach(field => {
|
||||
if (field.disabled) return;
|
||||
const key = String(field.key);
|
||||
const value = row[key as keyof typeof row];
|
||||
|
||||
// Skip validation for empty non-required fields
|
||||
const isRequired = field.validations?.some(v => v.rule === 'required');
|
||||
if (!isRequired && (value === undefined || value === null || value === '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = validateField(value, field as Field<T>);
|
||||
if (errors.length > 0) {
|
||||
fieldErrors[key] = errors;
|
||||
@@ -1351,39 +1396,54 @@ useEffect(() => {
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
// Update validation errors for this row
|
||||
initialErrors.set(rowIndex, fieldErrors);
|
||||
initialStatus.set(rowIndex, 'error');
|
||||
} else {
|
||||
initialStatus.set(rowIndex, 'validated');
|
||||
|
||||
// Update row validation status
|
||||
initialStatus.set(rowIndex, hasErrors ? 'error' : 'validated');
|
||||
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
__errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : undefined
|
||||
};
|
||||
}
|
||||
// Process all promises in the batch
|
||||
Promise.all(batchPromises).then(() => {
|
||||
// Update state for this batch
|
||||
setValidationErrors(prev => {
|
||||
const newMap = new Map(prev);
|
||||
initialErrors.forEach((errors, rowIndex) => {
|
||||
newMap.set(rowIndex, errors);
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
|
||||
setRowValidationStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
initialStatus.forEach((status, rowIndex) => {
|
||||
newMap.set(rowIndex, status);
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
|
||||
// Move to the next batch or finish
|
||||
currentBatch++;
|
||||
|
||||
// If there are more batches to process, schedule the next one
|
||||
if (endIdx < data.length) {
|
||||
setTimeout(processBatch, 0);
|
||||
if (currentBatch < totalBatches) {
|
||||
// Schedule the next batch with a small delay to allow UI updates
|
||||
setTimeout(processBatch, 10);
|
||||
} else {
|
||||
// All batches processed, update state
|
||||
// All batches processed, update the data
|
||||
setData(newData);
|
||||
setRowValidationStatus(initialStatus);
|
||||
setValidationErrors(initialErrors);
|
||||
console.log('Basic field validation complete');
|
||||
console.log('Basic validation complete');
|
||||
initialValidationDoneRef.current = true;
|
||||
|
||||
// Schedule UPC validations after basic validation is complete
|
||||
setTimeout(() => {
|
||||
runUPCValidation();
|
||||
}, 100);
|
||||
// Run item number uniqueness validation after basic validation
|
||||
validateItemNumberUniqueness();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Start processing batches
|
||||
// Start processing the first batch
|
||||
processBatch();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user