Fixes and improvements for product import module

This commit is contained in:
2025-09-06 14:38:47 -04:00
parent 9e7aac836e
commit 4dfe85231a
9 changed files with 248 additions and 308 deletions

View File

@@ -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": [
{

View File

@@ -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

View File

@@ -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,10 +562,9 @@ const ValidationContainer = <T extends string>({
return newSet;
});
// Debounce the API call to prevent excessive requests
setTimeout(() => {
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");
@@ -586,6 +577,7 @@ const ValidationContainer = <T extends string>({
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') {

View File

@@ -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,9 +378,19 @@ 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;
// 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)) {
@@ -413,6 +407,7 @@ const ValidationTable = <T extends string>({
else if (fieldKey === 'subline' && rowId && isLoadingSublines[rowId]) {
isLoading = true;
}
}
// Get validation errors for this cell
const cellErrors = validationErrors.get(row.index)?.[fieldKey] || [];
@@ -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);

View File

@@ -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(() => {
// Save to parent - parent must update immediately for this to work
onChange(finalValue);
// Exit editing mode
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]);
}, [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;
});

View File

@@ -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
// 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

View File

@@ -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;
// We'll update errors with a single batch operation
// Merge uniqueness errors with existing validation errors
setValidationErrors((prev) => {
const newMap = new Map(prev);
// Check each row for changes
// Add uniqueness errors
errors.forEach((rowErrors, rowIndex) => {
const existingErrors = newMap.get(rowIndex) || {};
const updatedErrors = { ...existingErrors };
let rowHasChanges = false;
// Check each field for changes
// Add uniqueness errors to existing errors
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);
});
// 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;
}
});
// Only return a new map if we have changes
return hasChanges ? newMap : prev;
});
// 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]);

View File

@@ -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");
// 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
// Validate each row completely
data.forEach((row, rowIndex) => {
const rowErrors: Record<string, any[]> = {};
let hasErrors = false;
// Check each regex field
// 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",
},
];
}
});
// Check regex fields (only if they have values)
regexFields.forEach((field) => {
const key = String(field.key);
const value = row[key as keyof typeof row];
// Skip empty values
// Skip empty values for regex validation
if (value === undefined || value === null || value === "") {
return;
}
// Find regex validation
const regexValidation = field.validations?.find(
(v) => v.rule === "regex"
);
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
);
const regex = new RegExp(regexValidation.value, regexValidation.flags);
if (!regex.test(String(value))) {
// Add regex validation error
// Only add regex error if no required error exists
if (!rowErrors[key]) {
rowErrors[key] = [
{
message: regexValidation.errorMessage,
@@ -192,7 +209,7 @@ export const useValidationState = <T extends string>({
type: "regex",
},
];
hasErrors = true;
}
}
} catch (error) {
console.error("Invalid regex in validation:", error);
@@ -200,27 +217,16 @@ export const useValidationState = <T extends string>({
}
});
// Add errors if any found
if (hasErrors) {
regexErrors.set(rowIndex, rowErrors);
// Only add to the map if there are actually errors
if (Object.keys(rowErrors).length > 0) {
allValidationErrors.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;
});
}
}
// Replace validation errors completely (clears old ones)
setValidationErrors(allValidationErrors);
// Run uniqueness validations immediately
// Run uniqueness validations after basic validation
validateUniqueItemNumbers();
} finally {
// Always ensure the ref is reset, even if an error occurs
@@ -232,7 +238,11 @@ export const useValidationState = <T extends string>({
// Run validation immediately
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
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));
}
}

View File

@@ -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