Add copy down functionality to validate table

This commit is contained in:
2025-03-09 16:30:11 -04:00
parent 7cc723ce83
commit c295c330ff
4 changed files with 99 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react'
import { Field } from '../../../types'
import { Loader2, AlertCircle } from 'lucide-react'
import { Loader2, AlertCircle, CopyDown, ArrowDown } from 'lucide-react'
import {
Tooltip,
TooltipContent,
@@ -122,6 +122,7 @@ export interface ValidationCellProps {
itemNumber?: string
width: number
rowIndex: number
copyDown?: () => void
}
const ItemNumberCell = React.memo(({
@@ -131,7 +132,8 @@ const ItemNumberCell = React.memo(({
width,
errors = [],
field,
onChange
onChange,
copyDown
}: {
value: any,
itemNumber?: string,
@@ -139,7 +141,8 @@ const ItemNumberCell = React.memo(({
width: number,
errors?: ErrorObject[],
field: Field<string>,
onChange: (value: any) => void
onChange: (value: any) => void,
copyDown?: () => void
}) => {
// Helper function to check if a value is empty
const isEmpty = (val: any): boolean =>
@@ -171,7 +174,7 @@ const ItemNumberCell = React.memo(({
: '';
return (
<TableCell className="p-1" style={{ width: `${width}px`, minWidth: `${width}px` }}>
<TableCell className="p-1 group relative" style={{ width: `${width}px`, minWidth: `${width}px` }}>
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
{isValidating ? (
<div className="flex items-center justify-center gap-2">
@@ -197,6 +200,25 @@ const ItemNumberCell = React.memo(({
}} />
</div>
)}
{copyDown && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-20 opacity-0 group-hover:opacity-100 transition-opacity">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={copyDown}
className="ml-1 p-1 rounded-sm hover:bg-gray-100 text-gray-500 hover:text-gray-700"
>
<ArrowDown className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Copy value to all cells below</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
</TableCell>
);
@@ -218,7 +240,9 @@ const ValidationCell = ({
fieldKey,
options = [],
itemNumber,
width}: ValidationCellProps) => {
width,
rowIndex,
copyDown}: ValidationCellProps) => {
// For item_number fields, use the specialized component
if (fieldKey === 'item_number') {
return (
@@ -230,6 +254,7 @@ const ValidationCell = ({
errors={errors}
field={field}
onChange={onChange}
copyDown={copyDown}
/>
);
}
@@ -273,7 +298,7 @@ const ValidationCell = ({
field.fieldType.price === true;
return (
<TableCell className="p-1" style={{ width: `${width}px`, minWidth: `${width}px` }}>
<TableCell className="p-1 group relative" style={{ width: `${width}px`, minWidth: `${width}px` }}>
<div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
{isValidating ? (
<div className="flex items-center justify-center gap-2">
@@ -299,6 +324,25 @@ const ValidationCell = ({
}} />
</div>
)}
{copyDown && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-20 opacity-0 group-hover:opacity-100 transition-opacity">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={copyDown}
className="ml-1 p-1 rounded-sm hover:bg-gray-100 text-gray-500 hover:text-gray-700"
>
<ArrowDown className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Copy value to all cells below</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
</TableCell>
);

View File

@@ -56,7 +56,8 @@ const ValidationContainer = <T extends string>({
loadTemplates,
setData,
fields,
isLoadingTemplates } = validationState
isLoadingTemplates,
copyDown } = validationState
// Add state for tracking product lines and sublines per row
const [rowProductLines, setRowProductLines] = useState<Record<string, any[]>>({});
@@ -832,22 +833,20 @@ const ValidationContainer = <T extends string>({
validatingCells={validatingCells}
itemNumbers={itemNumbersMap}
isLoadingTemplates={isLoadingTemplates}
copyDown={copyDown}
/>
);
}, [validatingUpcRows, itemNumbers, isLoadingTemplates]);
}, [validatingUpcRows, itemNumbers, isLoadingTemplates, copyDown]);
// Memoize the ValidationTable to prevent unnecessary re-renders
const renderValidationTable = useMemo(() => {
return (
<EnhancedValidationTable
data={filteredData}
// @ts-ignore - The fields are compatible at runtime but TypeScript has issues with the exact type
fields={validationState.fields}
updateRow={(rowIndex: number, key: string, value: any) =>
enhancedUpdateRow(rowIndex, key as T, value)
}
fields={fields}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
updateRow={updateRow}
validationErrors={validationErrors}
isValidatingUpc={isRowValidatingUpc}
validatingUpcRows={Array.from(validatingUpcRows)}
@@ -855,22 +854,19 @@ const ValidationContainer = <T extends string>({
templates={templates}
applyTemplate={applyTemplate}
getTemplateDisplayText={getTemplateDisplayText}
rowProductLines={rowProductLines}
rowSublines={rowSublines}
isLoadingLines={isLoadingLines}
isLoadingSublines={isLoadingSublines}
upcValidationResults={new Map(Object.entries(itemNumbers).map(([key, value]) => [parseInt(key), { itemNumber: value }]))}
validatingCells={new Set()}
itemNumbers={new Map()}
isLoadingTemplates={isLoadingTemplates}
copyDown={copyDown}
/>
);
}, [
EnhancedValidationTable,
filteredData,
validationState.fields,
enhancedUpdateRow,
fields,
rowSelection,
setRowSelection,
updateRow,
validationErrors,
isRowValidatingUpc,
validatingUpcRows,
@@ -878,11 +874,8 @@ const ValidationContainer = <T extends string>({
templates,
applyTemplate,
getTemplateDisplayText,
rowProductLines,
rowSublines,
isLoadingLines,
isLoadingSublines,
itemNumbers
isLoadingTemplates,
copyDown
]);
return (

View File

@@ -45,6 +45,7 @@ interface ValidationTableProps<T extends string> {
validatingCells: Set<string>
itemNumbers: Map<number, string>
isLoadingTemplates?: boolean
copyDown: (rowIndex: number, key: string) => void
[key: string]: any
}
@@ -62,6 +63,7 @@ interface MemoizedCellProps<T extends string = string> {
validatingCells: Set<string>
itemNumbers: Map<number, string>
width: number
copyDown: (rowIndex: number, key: string) => void
}
// Memoized cell component that only updates when its specific data changes
@@ -73,7 +75,8 @@ const MemoizedCell = React.memo(({
validationErrors,
validatingCells,
itemNumbers,
width
width,
copyDown
}: MemoizedCellProps) => {
const rowErrors = validationErrors.get(rowIndex) || {};
const fieldErrors = rowErrors[String(field.key)] || [];
@@ -92,6 +95,11 @@ const MemoizedCell = React.memo(({
updateRow(rowIndex, field.key, newValue);
}, [updateRow, rowIndex, field.key]);
// Memoize the copyDown handler
const handleCopyDown = useCallback(() => {
copyDown(rowIndex, field.key);
}, [copyDown, rowIndex, field.key]);
return (
<ValidationCell
field={field}
@@ -104,6 +112,7 @@ const MemoizedCell = React.memo(({
itemNumber={itemNumbers.get(rowIndex)}
width={width}
rowIndex={rowIndex}
copyDown={handleCopyDown}
/>
);
}, (prev, next) => {
@@ -177,6 +186,7 @@ interface MemoizedRowProps {
options?: { [key: string]: any[] };
rowIndex: number;
isSelected: boolean;
copyDown: (rowIndex: number, key: string) => void;
}
const MemoizedRow = React.memo<MemoizedRowProps>(({
@@ -188,7 +198,8 @@ const MemoizedRow = React.memo<MemoizedRowProps>(({
itemNumbers,
options = {},
rowIndex,
isSelected
isSelected,
copyDown
}) => {
return (
<TableRow
@@ -210,6 +221,11 @@ const MemoizedRow = React.memo<MemoizedRowProps>(({
const isValidating = validatingCells.has(`${rowIndex}-${field.key}`);
// Memoize the copyDown handler
const handleCopyDown = () => {
copyDown(rowIndex, field.key);
};
return (
<ValidationCell
key={String(field.key)}
@@ -223,6 +239,7 @@ const MemoizedRow = React.memo<MemoizedRowProps>(({
width={fieldWidth}
rowIndex={rowIndex}
itemNumber={itemNumbers.get(rowIndex)}
copyDown={handleCopyDown}
/>
);
})}
@@ -272,7 +289,8 @@ const ValidationTable = <T extends string>({
getTemplateDisplayText,
validatingCells,
itemNumbers,
isLoadingTemplates = false
isLoadingTemplates = false,
copyDown
}: ValidationTableProps<T>) => {
const { translations } = useRsi<T>();
@@ -430,11 +448,12 @@ const ValidationTable = <T extends string>({
validatingCells={validatingCells}
itemNumbers={itemNumbers}
width={fieldWidth}
copyDown={(rowIndex, key) => copyDown(rowIndex, key as T)}
/>
);
}
};
}).filter((col): col is ColumnDef<RowData<T>, any> => col !== null), [fields, validationErrors, validatingCells, itemNumbers, updateRow]);
}).filter((col): col is ColumnDef<RowData<T>, any> => col !== null), [fields, validationErrors, validatingCells, itemNumbers, updateRow, copyDown]);
// Combine columns
const columns = useMemo(() => [selectionColumn, templateColumn, ...fieldColumns], [selectionColumn, templateColumn, fieldColumns]);

View File

@@ -901,9 +901,21 @@ useEffect(() => {
newSet.delete(`${rowIndex}-${key}`);
return newSet;
});
}, 300); // Increase debounce time to reduce validation frequency
}, 300);
}, [data, validateRow, validateUpc, setData, setRowValidationStatus, cleanPriceFields, fields]);
// Copy a cell value to all cells below it in the same column
const copyDown = useCallback((rowIndex: number, key: T) => {
// Get the source value to copy
const sourceValue = data[rowIndex][key];
// Update all rows below with the same value using the existing updateRow function
// This ensures all validation logic runs consistently
for (let i = rowIndex + 1; i < data.length; i++) {
updateRow(i, key, sourceValue);
}
}, [data, updateRow]);
// Add this at the top of the component, after other useRef declarations
const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({});
@@ -1616,6 +1628,7 @@ useEffect(() => {
// Row manipulation
updateRow,
copyDown,
// Templates
templates,