Fixes and improvements for product import module
This commit is contained in:
6
inventory/package-lock.json
generated
6
inventory/package-lock.json
generated
@@ -3763,9 +3763,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001700",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
|
||||
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
|
||||
"version": "1.0.30001739",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz",
|
||||
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -312,7 +312,7 @@ const SupplierSelector = React.memo(({
|
||||
{suppliers?.map((supplier: any) => (
|
||||
<CommandItem
|
||||
key={supplier.value}
|
||||
value={supplier.label}
|
||||
value={`${supplier.label} ${supplier.value}`}
|
||||
onSelect={() => {
|
||||
onChange(supplier.value);
|
||||
setOpen(false); // Close popover after selection
|
||||
@@ -376,7 +376,7 @@ const CompanySelector = React.memo(({
|
||||
{companies?.map((company: any) => (
|
||||
<CommandItem
|
||||
key={company.value}
|
||||
value={company.label}
|
||||
value={`${company.label} ${company.value}`}
|
||||
onSelect={() => {
|
||||
onChange(company.value);
|
||||
setOpen(false); // Close popover after selection
|
||||
@@ -443,7 +443,7 @@ const LineSelector = React.memo(({
|
||||
{lines?.map((line: any) => (
|
||||
<CommandItem
|
||||
key={line.value}
|
||||
value={line.label}
|
||||
value={`${line.label} ${line.value}`}
|
||||
onSelect={() => {
|
||||
onChange(line.value);
|
||||
setOpen(false); // Close popover after selection
|
||||
@@ -510,7 +510,7 @@ const SubLineSelector = React.memo(({
|
||||
{sublines?.map((subline: any) => (
|
||||
<CommandItem
|
||||
key={subline.value}
|
||||
value={subline.label}
|
||||
value={`${subline.label} ${subline.value}`}
|
||||
onSelect={() => {
|
||||
onChange(subline.value);
|
||||
setOpen(false); // Close popover after selection
|
||||
|
||||
@@ -160,8 +160,6 @@ const ValidationContainer = <T extends string>({
|
||||
// Clear the fields map
|
||||
setFieldsToRevalidateMap({});
|
||||
|
||||
console.log(`Validating ${rowsToRevalidate.length} rows with specific fields`);
|
||||
|
||||
// Revalidate each row with specific fields information
|
||||
validationState.revalidateRows(rowsToRevalidate, fieldsMap);
|
||||
}, [fieldsToRevalidate, validationState, fieldsToRevalidateMap]);
|
||||
@@ -529,40 +527,34 @@ const ValidationContainer = <T extends string>({
|
||||
...newData[originalIndex],
|
||||
[key]: processedValue
|
||||
};
|
||||
} else {
|
||||
console.error(`Invalid originalIndex: ${originalIndex}, data length: ${newData.length}`);
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
|
||||
// Secondary effects - using a timeout to ensure UI updates first
|
||||
setTimeout(() => {
|
||||
// Secondary effects - using requestAnimationFrame for better performance
|
||||
requestAnimationFrame(() => {
|
||||
// Handle company change - clear line/subline and fetch product lines
|
||||
if (key === 'company' && value) {
|
||||
console.log(`Company changed to ${value} for row ${rowIndex}, updating lines and sublines`);
|
||||
|
||||
// Clear any existing line/subline values immediately
|
||||
setData(prevData => {
|
||||
const newData = [...prevData];
|
||||
const idx = newData.findIndex(item => item.__index === rowId);
|
||||
if (idx >= 0) {
|
||||
console.log(`Clearing line and subline values for row with ID ${rowId}`);
|
||||
newData[idx] = {
|
||||
...newData[idx],
|
||||
line: undefined,
|
||||
subline: undefined
|
||||
};
|
||||
} else {
|
||||
console.warn(`Could not find row with ID ${rowId} to clear line/subline values`);
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
|
||||
// Fetch product lines for the new company
|
||||
// Fetch product lines for the new company with debouncing
|
||||
if (rowId && value !== undefined) {
|
||||
const companyId = value.toString();
|
||||
|
||||
// Force immediate fetch for better UX
|
||||
console.log(`Immediately fetching product lines for company ${companyId} for row ${rowId}`);
|
||||
|
||||
// Set loading state first
|
||||
setValidatingCells(prev => {
|
||||
const newSet = new Set(prev);
|
||||
@@ -570,22 +562,22 @@ const ValidationContainer = <T extends string>({
|
||||
return newSet;
|
||||
});
|
||||
|
||||
fetchProductLines(rowId, companyId)
|
||||
.then(lines => {
|
||||
console.log(`Successfully loaded ${lines.length} product lines for company ${companyId}`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`Error fetching product lines for company ${companyId}:`, err);
|
||||
toast.error("Failed to load product lines");
|
||||
})
|
||||
.finally(() => {
|
||||
// Clear loading indicator
|
||||
setValidatingCells(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(`${rowIndex}-line`);
|
||||
return newSet;
|
||||
// Debounce the API call to prevent excessive requests
|
||||
setTimeout(() => {
|
||||
fetchProductLines(rowId, companyId)
|
||||
.catch(err => {
|
||||
console.error(`Error fetching product lines for company ${companyId}:`, err);
|
||||
toast.error("Failed to load product lines");
|
||||
})
|
||||
.finally(() => {
|
||||
// Clear loading indicator
|
||||
setValidatingCells(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(`${rowIndex}-line`);
|
||||
return newSet;
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 100); // 100ms debounce
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,7 +720,7 @@ const ValidationContainer = <T extends string>({
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 0); // Using 0ms timeout to defer execution until after the UI update
|
||||
}); // Using requestAnimationFrame to defer execution until after the UI update
|
||||
}, [data, filteredData, setData, fetchProductLines, fetchSublines, upcValidation, markRowForRevalidation]);
|
||||
|
||||
// Fix the missing loading indicator clear code
|
||||
@@ -800,15 +792,15 @@ const ValidationContainer = <T extends string>({
|
||||
markRowForRevalidation(targetRowIndex, fieldKey);
|
||||
});
|
||||
|
||||
// Clear the loading state for all cells after a short delay
|
||||
setTimeout(() => {
|
||||
// Clear the loading state for all cells efficiently
|
||||
requestAnimationFrame(() => {
|
||||
setValidatingCells(prev => {
|
||||
if (prev.size === 0) return prev;
|
||||
if (prev.size === 0 || updatingCells.size === 0) return prev;
|
||||
const newSet = new Set(prev);
|
||||
updatingCells.forEach(cell => newSet.delete(cell));
|
||||
return newSet;
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// If copying UPC or supplier fields, validate UPC for all rows
|
||||
if (fieldKey === 'upc' || fieldKey === 'barcode' || fieldKey === 'supplier') {
|
||||
|
||||
@@ -138,34 +138,18 @@ const MemoizedCell = React.memo(({
|
||||
/>
|
||||
);
|
||||
}, (prev, next) => {
|
||||
// CRITICAL FIX: Never memoize item_number cells - always re-render them
|
||||
// For item_number cells, only re-render when itemNumber actually changes
|
||||
if (prev.fieldKey === 'item_number') {
|
||||
return false; // Never skip re-renders for item_number cells
|
||||
return prev.itemNumber === next.itemNumber &&
|
||||
prev.value === next.value &&
|
||||
prev.isValidating === next.isValidating;
|
||||
}
|
||||
|
||||
// Optimize the memo comparison function for better performance
|
||||
// Only re-render if these essential props change
|
||||
const valueEqual = prev.value === next.value;
|
||||
const isValidatingEqual = prev.isValidating === next.isValidating;
|
||||
|
||||
// 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;
|
||||
// Simplified memo comparison - most expensive checks removed
|
||||
return prev.value === next.value &&
|
||||
prev.isValidating === next.isValidating &&
|
||||
prev.errors === next.errors &&
|
||||
prev.options === next.options;
|
||||
});
|
||||
|
||||
MemoizedCell.displayName = 'MemoizedCell';
|
||||
@@ -394,24 +378,35 @@ const ValidationTable = <T extends string>({
|
||||
options = rowSublines[rowId];
|
||||
}
|
||||
|
||||
// Determine if this cell is in loading state - use a clear consistent approach
|
||||
// Get the current cell value first
|
||||
const currentValue = fieldKey === 'item_number' && row.original[field.key]
|
||||
? row.original[field.key]
|
||||
: row.original[field.key as keyof typeof row.original];
|
||||
|
||||
// Determine if this cell is in loading state - only show loading for empty fields
|
||||
let isLoading = false;
|
||||
|
||||
// Check the validatingCells Set first (for item_number and other fields)
|
||||
const cellLoadingKey = `${row.index}-${fieldKey}`;
|
||||
if (validatingCells.has(cellLoadingKey)) {
|
||||
isLoading = true;
|
||||
}
|
||||
// Check if UPC is validating for this row and field is item_number
|
||||
else if (fieldKey === 'item_number' && isRowValidatingUpc(row.index)) {
|
||||
isLoading = true;
|
||||
}
|
||||
// Add loading state for line/subline fields
|
||||
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
||||
isLoading = true;
|
||||
}
|
||||
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
||||
isLoading = true;
|
||||
// Only show loading if the field is currently empty
|
||||
const isEmpty = currentValue === undefined || currentValue === null || currentValue === '' ||
|
||||
(Array.isArray(currentValue) && currentValue.length === 0);
|
||||
|
||||
if (isEmpty) {
|
||||
// Check the validatingCells Set first (for item_number and other fields)
|
||||
const cellLoadingKey = `${row.index}-${fieldKey}`;
|
||||
if (validatingCells.has(cellLoadingKey)) {
|
||||
isLoading = true;
|
||||
}
|
||||
// Check if UPC is validating for this row and field is item_number
|
||||
else if (fieldKey === 'item_number' && isRowValidatingUpc(row.index)) {
|
||||
isLoading = true;
|
||||
}
|
||||
// Add loading state for line/subline fields
|
||||
else if (fieldKey === 'line' && rowId && isLoadingLines[rowId]) {
|
||||
isLoading = true;
|
||||
}
|
||||
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
|
||||
isLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get validation errors for this cell
|
||||
@@ -448,19 +443,16 @@ const ValidationTable = <T extends string>({
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: For item_number fields, create a unique key that includes the itemNumber value
|
||||
// This forces a complete re-render when the itemNumber changes
|
||||
// Create stable keys that only change when actual content changes
|
||||
const cellKey = fieldKey === 'item_number'
|
||||
? `cell-${row.index}-${fieldKey}-${itemNumber || 'empty'}-${Date.now()}` // Force re-render on every render cycle for item_number
|
||||
? `cell-${row.index}-${fieldKey}-${itemNumber || 'empty'}` // Only change when itemNumber actually changes
|
||||
: `cell-${row.index}-${fieldKey}`;
|
||||
|
||||
return (
|
||||
<MemoizedCell
|
||||
key={cellKey} // CRITICAL: Add key to force re-render when itemNumber changes
|
||||
field={fieldWithType as Field<string>}
|
||||
value={fieldKey === 'item_number' && row.original[field.key]
|
||||
? row.original[field.key] // Use direct value from row data
|
||||
: row.original[field.key as keyof typeof row.original]}
|
||||
value={currentValue}
|
||||
onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)}
|
||||
errors={cellErrors}
|
||||
isValidating={isLoading}
|
||||
@@ -678,6 +670,10 @@ const areEqual = (prev: ValidationTableProps<any>, next: ValidationTableProps<an
|
||||
// Fast path: data length change always means re-render
|
||||
if (prev.data.length !== next.data.length) return false;
|
||||
|
||||
// CRITICAL: Check if data content has actually changed
|
||||
// Simple reference equality check - if data array reference changed, re-render
|
||||
if (prev.data !== next.data) return false;
|
||||
|
||||
// Efficiently check row selection changes
|
||||
const prevSelectionKeys = Object.keys(prev.rowSelection);
|
||||
const nextSelectionKeys = Object.keys(next.rowSelection);
|
||||
|
||||
@@ -17,10 +17,18 @@ interface InputCellProps<T extends string> {
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Add efficient price formatting utility
|
||||
const formatPrice = (value: string): string => {
|
||||
// Add efficient price formatting utility with null safety
|
||||
const formatPrice = (value: any): string => {
|
||||
// Handle undefined, null, or non-string values
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Convert to string if not already
|
||||
const stringValue = String(value);
|
||||
|
||||
// Remove any non-numeric characters except decimal point
|
||||
const numericValue = value.replace(/[^\d.]/g, '');
|
||||
const numericValue = stringValue.replace(/[^\d.]/g, '');
|
||||
|
||||
// Parse as float and format to 2 decimal places
|
||||
const numValue = parseFloat(numericValue);
|
||||
@@ -45,53 +53,25 @@ const InputCell = <T extends string>({
|
||||
}: InputCellProps<T>) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// Use a ref to track if we need to process the value
|
||||
const needsProcessingRef = useRef(false);
|
||||
|
||||
// Track local display value to avoid waiting for validation
|
||||
const [localDisplayValue, setLocalDisplayValue] = useState<string | null>(null);
|
||||
|
||||
// Add state for hover
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// Remove optimistic updates and rely on parent state
|
||||
|
||||
// Helper function to check if a class is present in the className string
|
||||
const hasClass = (cls: string): boolean => {
|
||||
const classNames = className.split(' ');
|
||||
return classNames.includes(cls);
|
||||
};
|
||||
|
||||
// Initialize localDisplayValue on mount and when value changes externally
|
||||
useEffect(() => {
|
||||
if (localDisplayValue === null ||
|
||||
(typeof value === 'string' && typeof localDisplayValue === 'string' &&
|
||||
value.trim() !== localDisplayValue.trim())) {
|
||||
setLocalDisplayValue(value);
|
||||
}
|
||||
}, [value, localDisplayValue]);
|
||||
// No complex initialization needed
|
||||
|
||||
// Efficiently handle price formatting without multiple rerenders
|
||||
useEffect(() => {
|
||||
if (isPrice && needsProcessingRef.current && !isEditing) {
|
||||
needsProcessingRef.current = false;
|
||||
|
||||
// Do price processing only when needed
|
||||
const formattedValue = formatPrice(value);
|
||||
if (formattedValue !== value) {
|
||||
onChange(formattedValue);
|
||||
}
|
||||
}
|
||||
}, [value, isPrice, isEditing, onChange]);
|
||||
|
||||
// Handle focus event - optimized to be synchronous
|
||||
// Handle focus event
|
||||
const handleFocus = useCallback(() => {
|
||||
setIsEditing(true);
|
||||
|
||||
// For price fields, strip formatting when focusing
|
||||
if (value !== undefined && value !== null) {
|
||||
if (isPrice) {
|
||||
// Remove any non-numeric characters except decimal point
|
||||
// Remove any non-numeric characters except decimal point for editing
|
||||
const numericValue = String(value).replace(/[^\d.]/g, '');
|
||||
setEditValue(numericValue);
|
||||
} else {
|
||||
@@ -104,30 +84,17 @@ const InputCell = <T extends string>({
|
||||
onStartEdit?.();
|
||||
}, [value, onStartEdit, isPrice]);
|
||||
|
||||
// Handle blur event - use transition for non-critical updates
|
||||
// Handle blur event - save to parent only
|
||||
const handleBlur = useCallback(() => {
|
||||
// First - lock in the current edit value to prevent it from being lost
|
||||
const finalValue = editValue.trim();
|
||||
|
||||
// Then transition to non-editing state
|
||||
startTransition(() => {
|
||||
setIsEditing(false);
|
||||
|
||||
// Format the value for storage (remove formatting like $ for price)
|
||||
let processedValue = finalValue;
|
||||
|
||||
if (isPrice && processedValue) {
|
||||
needsProcessingRef.current = true;
|
||||
}
|
||||
|
||||
// Update local display value immediately to prevent UI flicker
|
||||
setLocalDisplayValue(processedValue);
|
||||
|
||||
// Commit the change to parent component
|
||||
onChange(processedValue);
|
||||
onEndEdit?.();
|
||||
});
|
||||
}, [editValue, onChange, onEndEdit, isPrice]);
|
||||
// Save to parent - parent must update immediately for this to work
|
||||
onChange(finalValue);
|
||||
|
||||
// Exit editing mode
|
||||
setIsEditing(false);
|
||||
onEndEdit?.();
|
||||
}, [editValue, onChange, onEndEdit]);
|
||||
|
||||
// Handle direct input change - optimized to be synchronous for typing
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
@@ -135,30 +102,22 @@ const InputCell = <T extends string>({
|
||||
setEditValue(newValue);
|
||||
}, [isPrice]);
|
||||
|
||||
// Get the display value - prioritize local display value
|
||||
// Get the display value - use parent value directly
|
||||
const displayValue = useMemo(() => {
|
||||
// First priority: local display value (for immediate updates)
|
||||
if (localDisplayValue !== null) {
|
||||
if (isPrice) {
|
||||
// Format price value
|
||||
const numValue = parseFloat(localDisplayValue);
|
||||
return !isNaN(numValue) ? numValue.toFixed(2) : localDisplayValue;
|
||||
}
|
||||
return localDisplayValue;
|
||||
}
|
||||
const currentValue = value ?? '';
|
||||
|
||||
// Second priority: handle price formatting for the actual value
|
||||
if (isPrice && value) {
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(2);
|
||||
} else if (typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value)) {
|
||||
return parseFloat(value).toFixed(2);
|
||||
// Handle price formatting for display
|
||||
if (isPrice && currentValue !== '' && currentValue !== undefined && currentValue !== null) {
|
||||
if (typeof currentValue === 'number') {
|
||||
return currentValue.toFixed(2);
|
||||
} else if (typeof currentValue === 'string' && /^-?\d+(\.\d+)?$/.test(currentValue)) {
|
||||
return parseFloat(currentValue).toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Default: use the actual value or empty string
|
||||
return value ?? '';
|
||||
}, [isPrice, value, localDisplayValue]);
|
||||
// For non-price or invalid price values, return as-is
|
||||
return String(currentValue);
|
||||
}, [isPrice, value]);
|
||||
|
||||
// Add outline even when not in focus
|
||||
const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0";
|
||||
@@ -221,7 +180,6 @@ const InputCell = <T extends string>({
|
||||
className={cn(
|
||||
outlineClass,
|
||||
hasErrors ? "border-destructive" : "",
|
||||
isPending ? "opacity-50" : "",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
@@ -267,33 +225,11 @@ const InputCell = <T extends string>({
|
||||
)
|
||||
}
|
||||
|
||||
// Optimize memo comparison to focus on essential props
|
||||
// Simplified memo comparison
|
||||
export default React.memo(InputCell, (prev, next) => {
|
||||
if (prev.hasErrors !== next.hasErrors) return false;
|
||||
if (prev.isMultiline !== next.isMultiline) return false;
|
||||
if (prev.isPrice !== next.isPrice) return false;
|
||||
if (prev.disabled !== next.disabled) return false;
|
||||
if (prev.field !== next.field) return false;
|
||||
|
||||
// Only check value if not editing (to avoid expensive rerender during editing)
|
||||
if (prev.value !== next.value) {
|
||||
// For price values, do a more intelligent comparison
|
||||
if (prev.isPrice) {
|
||||
// Convert both to numeric values for comparison
|
||||
const prevNum = typeof prev.value === 'number' ? prev.value :
|
||||
typeof prev.value === 'string' ? parseFloat(prev.value) : 0;
|
||||
const nextNum = typeof next.value === 'number' ? next.value :
|
||||
typeof next.value === 'string' ? parseFloat(next.value) : 0;
|
||||
|
||||
// Only update if the actual numeric values differ
|
||||
if (!isNaN(prevNum) && !isNaN(nextNum) &&
|
||||
Math.abs(prevNum - nextNum) > 0.001) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
// Only re-render if essential props change
|
||||
return prev.value === next.value &&
|
||||
prev.hasErrors === next.hasErrors &&
|
||||
prev.disabled === next.disabled &&
|
||||
prev.field === next.field;
|
||||
});
|
||||
@@ -7,14 +7,24 @@ import { RowData, isEmpty } from './validationTypes';
|
||||
// Create a cache for validation results to avoid repeated validation of the same data
|
||||
const validationResultCache = new Map();
|
||||
|
||||
// Add a function to clear cache for a specific field value
|
||||
export const clearValidationCacheForField = (fieldKey: string) => {
|
||||
// Look for entries that match this field key
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.startsWith(`${fieldKey}-`)) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
// Optimize cache clearing - only clear when necessary
|
||||
export const clearValidationCacheForField = (fieldKey: string, specificValue?: any) => {
|
||||
if (specificValue !== undefined) {
|
||||
// Only clear specific field-value combinations
|
||||
const specificKey = `${fieldKey}-${String(specificValue)}`;
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.startsWith(specificKey)) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Clear all entries for the field
|
||||
validationResultCache.forEach((_, key) => {
|
||||
if (key.startsWith(`${fieldKey}-`)) {
|
||||
validationResultCache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Add a special function to clear all uniqueness validation caches
|
||||
|
||||
@@ -95,52 +95,48 @@ export const useUniqueItemNumbersValidation = <T extends string>(
|
||||
});
|
||||
});
|
||||
|
||||
// Apply batch updates only if we have errors to report
|
||||
if (errors.size > 0) {
|
||||
// OPTIMIZATION: Check if we actually have new errors before updating state
|
||||
let hasChanges = false;
|
||||
// Merge uniqueness errors with existing validation errors
|
||||
setValidationErrors((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
|
||||
// We'll update errors with a single batch operation
|
||||
setValidationErrors((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
// Add uniqueness errors
|
||||
errors.forEach((rowErrors, rowIndex) => {
|
||||
const existingErrors = newMap.get(rowIndex) || {};
|
||||
const updatedErrors = { ...existingErrors };
|
||||
|
||||
// Check each row for changes
|
||||
errors.forEach((rowErrors, rowIndex) => {
|
||||
const existingErrors = newMap.get(rowIndex) || {};
|
||||
const updatedErrors = { ...existingErrors };
|
||||
let rowHasChanges = false;
|
||||
|
||||
// Check each field for changes
|
||||
Object.entries(rowErrors).forEach(([fieldKey, fieldErrors]) => {
|
||||
// Compare with existing errors
|
||||
const existingFieldErrors = existingErrors[fieldKey];
|
||||
|
||||
if (
|
||||
!existingFieldErrors ||
|
||||
existingFieldErrors.length !== fieldErrors.length ||
|
||||
!existingFieldErrors.every(
|
||||
(err, idx) =>
|
||||
err.message === fieldErrors[idx].message &&
|
||||
err.type === fieldErrors[idx].type
|
||||
)
|
||||
) {
|
||||
// We have a change
|
||||
updatedErrors[fieldKey] = fieldErrors;
|
||||
rowHasChanges = true;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Only update if we have changes
|
||||
if (rowHasChanges) {
|
||||
newMap.set(rowIndex, updatedErrors);
|
||||
}
|
||||
// Add uniqueness errors to existing errors
|
||||
Object.entries(rowErrors).forEach(([fieldKey, fieldErrors]) => {
|
||||
updatedErrors[fieldKey] = fieldErrors;
|
||||
});
|
||||
|
||||
// Only return a new map if we have changes
|
||||
return hasChanges ? newMap : prev;
|
||||
newMap.set(rowIndex, updatedErrors);
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up rows that have no uniqueness errors anymore
|
||||
// by removing only uniqueness error types from rows not in the errors map
|
||||
newMap.forEach((rowErrors, rowIndex) => {
|
||||
if (!errors.has(rowIndex)) {
|
||||
// Remove uniqueness errors from this row
|
||||
const cleanedErrors: Record<string, ValidationError[]> = {};
|
||||
Object.entries(rowErrors).forEach(([fieldKey, fieldErrors]) => {
|
||||
// Keep non-uniqueness errors
|
||||
const nonUniqueErrors = fieldErrors.filter(error => error.type !== ErrorType.Unique);
|
||||
if (nonUniqueErrors.length > 0) {
|
||||
cleanedErrors[fieldKey] = nonUniqueErrors;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the row or remove it if no errors remain
|
||||
if (Object.keys(cleanedErrors).length > 0) {
|
||||
newMap.set(rowIndex, cleanedErrors);
|
||||
} else {
|
||||
newMap.delete(rowIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return newMap;
|
||||
});
|
||||
|
||||
console.log("Uniqueness validation complete");
|
||||
}, [data, fields, setValidationErrors]);
|
||||
|
||||
@@ -128,7 +128,7 @@ export const useValidationState = <T extends string>({
|
||||
// Use filter management hook
|
||||
const filterManagement = useFilterManagement<T>(data, fields, validationErrors);
|
||||
|
||||
// Run validation when data changes - FIXED to prevent recursive validation
|
||||
// Run validation when data changes - OPTIMIZED to prevent recursive validation
|
||||
useEffect(() => {
|
||||
// Skip initial load - we have a separate initialization process
|
||||
if (!initialValidationDoneRef.current) return;
|
||||
@@ -139,51 +139,68 @@ export const useValidationState = <T extends string>({
|
||||
// CRITICAL FIX: Skip validation if we're already validating to prevent infinite loops
|
||||
if (isValidatingRef.current) return;
|
||||
|
||||
console.log("Running validation on data change");
|
||||
isValidatingRef.current = true;
|
||||
// Debounce validation to prevent excessive calls
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (isValidatingRef.current) return; // Double-check before proceeding
|
||||
|
||||
// Validation running (removed console.log for performance)
|
||||
isValidatingRef.current = true;
|
||||
|
||||
// For faster validation, run synchronously instead of in an async function
|
||||
// COMPREHENSIVE validation that clears old errors and adds new ones
|
||||
const validateFields = () => {
|
||||
try {
|
||||
// Run regex validations on all rows
|
||||
// Create a complete fresh validation map
|
||||
const allValidationErrors = new Map<number, Record<string, any[]>>();
|
||||
|
||||
// Get all field types that need validation
|
||||
const requiredFields = fields.filter((field) =>
|
||||
field.validations?.some((v) => v.rule === "required")
|
||||
);
|
||||
const regexFields = fields.filter((field) =>
|
||||
field.validations?.some((v) => v.rule === "regex")
|
||||
);
|
||||
if (regexFields.length > 0) {
|
||||
// Create a map to collect validation errors
|
||||
const regexErrors = new Map<
|
||||
number,
|
||||
Record<string, any[]>
|
||||
>();
|
||||
|
||||
// Check each row for regex errors
|
||||
data.forEach((row, rowIndex) => {
|
||||
const rowErrors: Record<string, any[]> = {};
|
||||
let hasErrors = false;
|
||||
// Validate each row completely
|
||||
data.forEach((row, rowIndex) => {
|
||||
const rowErrors: Record<string, any[]> = {};
|
||||
|
||||
// Check each regex field
|
||||
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];
|
||||
|
||||
// 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",
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// Skip empty values
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return;
|
||||
}
|
||||
// Check regex fields (only if they have values)
|
||||
regexFields.forEach((field) => {
|
||||
const key = String(field.key);
|
||||
const value = row[key as keyof typeof row];
|
||||
|
||||
// Find regex validation
|
||||
const regexValidation = field.validations?.find(
|
||||
(v) => v.rule === "regex"
|
||||
);
|
||||
if (regexValidation) {
|
||||
try {
|
||||
// Check if value matches regex
|
||||
const regex = new RegExp(
|
||||
regexValidation.value,
|
||||
regexValidation.flags
|
||||
);
|
||||
if (!regex.test(String(value))) {
|
||||
// Add regex validation error
|
||||
// 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,
|
||||
@@ -192,35 +209,24 @@ export const useValidationState = <T extends string>({
|
||||
type: "regex",
|
||||
},
|
||||
];
|
||||
hasErrors = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Invalid regex in validation:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Invalid regex in validation:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// Add errors if any found
|
||||
if (hasErrors) {
|
||||
regexErrors.set(rowIndex, rowErrors);
|
||||
}
|
||||
});
|
||||
|
||||
// Update validation errors
|
||||
if (regexErrors.size > 0) {
|
||||
setValidationErrors((prev) => {
|
||||
const newErrors = new Map(prev);
|
||||
// Merge in regex errors
|
||||
for (const [rowIndex, errors] of regexErrors.entries()) {
|
||||
const existingErrors = newErrors.get(rowIndex) || {};
|
||||
newErrors.set(rowIndex, { ...existingErrors, ...errors });
|
||||
}
|
||||
return newErrors;
|
||||
});
|
||||
// Only add to the map if there are actually errors
|
||||
if (Object.keys(rowErrors).length > 0) {
|
||||
allValidationErrors.set(rowIndex, rowErrors);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Run uniqueness validations immediately
|
||||
// Replace validation errors completely (clears old ones)
|
||||
setValidationErrors(allValidationErrors);
|
||||
|
||||
// Run uniqueness validations after basic validation
|
||||
validateUniqueItemNumbers();
|
||||
} finally {
|
||||
// Always ensure the ref is reset, even if an error occurs
|
||||
@@ -230,9 +236,13 @@ export const useValidationState = <T extends string>({
|
||||
}
|
||||
};
|
||||
|
||||
// Run validation immediately
|
||||
validateFields();
|
||||
}, [data, fields, validateUniqueItemNumbers]);
|
||||
// Run validation immediately
|
||||
validateFields();
|
||||
}, 50); // 50ms debounce
|
||||
|
||||
// Cleanup timeout on unmount or dependency change
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [data, fields]); // Removed validateUniqueItemNumbers to prevent infinite loop
|
||||
|
||||
// Add field options query
|
||||
const { data: fieldOptionsData } = useQuery({
|
||||
@@ -352,7 +362,7 @@ export const useValidationState = <T extends string>({
|
||||
useEffect(() => {
|
||||
if (initialValidationDoneRef.current) return;
|
||||
|
||||
console.log("Running initial validation");
|
||||
// Running initial validation (removed console.log for performance)
|
||||
|
||||
const runCompleteValidation = async () => {
|
||||
if (!data || data.length === 0) return;
|
||||
@@ -379,8 +389,8 @@ export const useValidationState = <T extends string>({
|
||||
`Found ${uniqueFields.length} fields requiring uniqueness validation`
|
||||
);
|
||||
|
||||
// Limit batch size to avoid UI freezing
|
||||
const BATCH_SIZE = 100;
|
||||
// Dynamic batch size based on dataset size
|
||||
const BATCH_SIZE = data.length <= 50 ? data.length : 25; // Process all at once for small datasets
|
||||
const totalRows = data.length;
|
||||
|
||||
// Initialize new data for any modifications
|
||||
@@ -559,9 +569,9 @@ export const useValidationState = <T extends string>({
|
||||
currentBatch = batch;
|
||||
await processBatch();
|
||||
|
||||
// Yield to UI thread periodically
|
||||
if (batch % 2 === 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
// Yield to UI thread more frequently for large datasets
|
||||
if (batch % 2 === 1 || totalRows > 500) {
|
||||
await new Promise((resolve) => setTimeout(resolve, totalRows > 1000 ? 10 : 5));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Cost Each",
|
||||
key: "cost_each",
|
||||
description: "Wholesale cost per unit",
|
||||
alternateMatches: ["wholesale", "wholesale price", "supplier cost each"],
|
||||
alternateMatches: ["wholesale", "wholesale price", "supplier cost each", "cost each"],
|
||||
fieldType: {
|
||||
type: "input",
|
||||
price: true
|
||||
|
||||
Reference in New Issue
Block a user