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