Add copy down functionality to validate table
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user