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