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 React from 'react'
import { Field } from '../../../types' import { Field } from '../../../types'
import { Loader2, AlertCircle } from 'lucide-react' import { Loader2, AlertCircle, CopyDown, ArrowDown } from 'lucide-react'
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -122,6 +122,7 @@ export interface ValidationCellProps {
itemNumber?: string itemNumber?: string
width: number width: number
rowIndex: number rowIndex: number
copyDown?: () => void
} }
const ItemNumberCell = React.memo(({ const ItemNumberCell = React.memo(({
@@ -131,7 +132,8 @@ const ItemNumberCell = React.memo(({
width, width,
errors = [], errors = [],
field, field,
onChange onChange,
copyDown
}: { }: {
value: any, value: any,
itemNumber?: string, itemNumber?: string,
@@ -139,7 +141,8 @@ const ItemNumberCell = React.memo(({
width: number, width: number,
errors?: ErrorObject[], errors?: ErrorObject[],
field: Field<string>, field: Field<string>,
onChange: (value: any) => void onChange: (value: any) => void,
copyDown?: () => void
}) => { }) => {
// Helper function to check if a value is empty // Helper function to check if a value is empty
const isEmpty = (val: any): boolean => const isEmpty = (val: any): boolean =>
@@ -171,7 +174,7 @@ const ItemNumberCell = React.memo(({
: ''; : '';
return ( 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' : ''}`}> <div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
{isValidating ? ( {isValidating ? (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@@ -197,6 +200,25 @@ const ItemNumberCell = React.memo(({
}} /> }} />
</div> </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> </div>
</TableCell> </TableCell>
); );
@@ -218,7 +240,9 @@ const ValidationCell = ({
fieldKey, fieldKey,
options = [], options = [],
itemNumber, itemNumber,
width}: ValidationCellProps) => { width,
rowIndex,
copyDown}: ValidationCellProps) => {
// For item_number fields, use the specialized component // For item_number fields, use the specialized component
if (fieldKey === 'item_number') { if (fieldKey === 'item_number') {
return ( return (
@@ -230,6 +254,7 @@ const ValidationCell = ({
errors={errors} errors={errors}
field={field} field={field}
onChange={onChange} onChange={onChange}
copyDown={copyDown}
/> />
); );
} }
@@ -273,7 +298,7 @@ const ValidationCell = ({
field.fieldType.price === true; field.fieldType.price === true;
return ( 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' : ''}`}> <div className={`relative ${hasError || isRequiredButEmpty ? 'border-red-500' : ''}`}>
{isValidating ? ( {isValidating ? (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@@ -299,6 +324,25 @@ const ValidationCell = ({
}} /> }} />
</div> </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> </div>
</TableCell> </TableCell>
); );

View File

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

View File

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

View File

@@ -901,9 +901,21 @@ useEffect(() => {
newSet.delete(`${rowIndex}-${key}`); newSet.delete(`${rowIndex}-${key}`);
return newSet; return newSet;
}); });
}, 300); // Increase debounce time to reduce validation frequency }, 300);
}, [data, validateRow, validateUpc, setData, setRowValidationStatus, cleanPriceFields, fields]); }, [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 // Add this at the top of the component, after other useRef declarations
const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({}); const validationTimeoutsRef = useRef<Record<number, NodeJS.Timeout>>({});
@@ -1616,6 +1628,7 @@ useEffect(() => {
// Row manipulation // Row manipulation
updateRow, updateRow,
copyDown,
// Templates // Templates
templates, templates,