From d0a83c04ca01fa66af5a0e7918f26124ec4cf663 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 14 Mar 2025 16:59:07 -0400 Subject: [PATCH] Improve copy down functionality with loading state and ability to select end cell instead of defaulting to the bottom --- .../components/BaseCellContent.tsx | 21 ++ .../components/ValidationCell.tsx | 263 ++++++++++++++++-- .../components/ValidationContainer.tsx | 77 ++++- .../components/ValidationTable.tsx | 239 +++++++++++----- .../components/cells/CheckboxCell.tsx | 60 +++- .../components/cells/InputCell.tsx | 64 ++++- .../components/cells/MultiSelectCell.tsx | 166 +++++++---- .../components/cells/MultilineInput.tsx | 57 +++- .../components/cells/SelectCell.tsx | 64 ++++- 9 files changed, 811 insertions(+), 200 deletions(-) create mode 100644 inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx new file mode 100644 index 0000000..116d190 --- /dev/null +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/BaseCellContent.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import MultiSelectCell from './MultiSelectCell'; + +const BaseCellContent = ({ fieldType, field, value, onChange, options, hasErrors, className }) => { + if (fieldType === 'multi-select' || fieldType === 'multi-input') { + return ( + + ); + } + + return null; +}; + +export default BaseCellContent; \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx index db4fa7e..104e4d2 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationCell.tsx @@ -1,6 +1,6 @@ import React from 'react' import { Field } from '../../../types' -import { Loader2, AlertCircle, ArrowDown } from 'lucide-react' +import { Loader2, AlertCircle, ArrowDown, Check, X } from 'lucide-react' import { Tooltip, TooltipContent, @@ -12,6 +12,29 @@ import SelectCell from './cells/SelectCell' import MultiSelectCell from './cells/MultiSelectCell' import { TableCell } from '@/components/ui/table' +// Context for copy down selection mode +export const CopyDownContext = React.createContext<{ + isInCopyDownMode: boolean; + sourceRowIndex: number | null; + sourceFieldKey: string | null; + targetRowIndex: number | null; + setIsInCopyDownMode: (value: boolean) => void; + setSourceRowIndex: (value: number | null) => void; + setSourceFieldKey: (value: string | null) => void; + setTargetRowIndex: (value: number | null) => void; + handleCopyDownComplete: (sourceRowIndex: number, fieldKey: string, targetRowIndex: number) => void; +}>({ + isInCopyDownMode: false, + sourceRowIndex: null, + sourceFieldKey: null, + targetRowIndex: null, + setIsInCopyDownMode: () => {}, + setSourceRowIndex: () => {}, + setSourceFieldKey: () => {}, + setTargetRowIndex: () => {}, + handleCopyDownComplete: () => {}, +}); + // Define error object type type ErrorObject = { message: string; @@ -51,13 +74,15 @@ const BaseCellContent = React.memo(({ value, onChange, hasErrors, - options = [] + options = [], + className = '' }: { field: Field; value: any; onChange: (value: any) => void; hasErrors: boolean; options?: readonly any[]; + className?: string; }) => { // Get field type information const fieldType = typeof field.fieldType === 'string' @@ -82,6 +107,7 @@ const BaseCellContent = React.memo(({ onChange={onChange} options={options} hasErrors={hasErrors} + className={className} /> ); } @@ -94,6 +120,7 @@ const BaseCellContent = React.memo(({ onChange={onChange} options={options} hasErrors={hasErrors} + className={className} /> ); } @@ -119,6 +146,7 @@ const BaseCellContent = React.memo(({ prev.value === next.value && prev.hasErrors === next.hasErrors && prev.field === next.field && + prev.className === next.className && optionsEqual ); }); @@ -136,7 +164,8 @@ export interface ValidationCellProps { itemNumber?: string width: number rowIndex: number - copyDown?: () => void + copyDown?: (endRowIndex?: number) => void + totalRows?: number } // Add efficient error message extraction function @@ -223,7 +252,9 @@ const ItemNumberCell = React.memo(({ errors = [], field, onChange, - copyDown + copyDown, + rowIndex, + totalRows = 0 }: { value: any, itemNumber?: string, @@ -232,7 +263,9 @@ const ItemNumberCell = React.memo(({ errors?: ErrorObject[], field: Field, onChange: (value: any) => void, - copyDown?: () => void + copyDown?: (endRowIndex?: number) => void, + rowIndex: number, + totalRows?: number }) => { // If we have a value or itemNumber, ignore "required" errors const displayValue = itemNumber || value; @@ -248,10 +281,67 @@ const ItemNumberCell = React.memo(({ [displayValue, errors] ); + // Add state for hover on copy down button + const [isCopyDownHovered, setIsCopyDownHovered] = React.useState(false); + // Add state for hover on target row + const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false); + + // Get copy down context + const copyDownContext = React.useContext(CopyDownContext); + + // Handle copy down button click + const handleCopyDownClick = () => { + if (copyDown && totalRows > rowIndex + 1) { + // Enter copy down mode + copyDownContext.setIsInCopyDownMode(true); + copyDownContext.setSourceRowIndex(rowIndex); + copyDownContext.setSourceFieldKey('item_number'); + } + }; + + // Check if this cell is the source of the current copy down operation + const isSourceCell = copyDownContext.isInCopyDownMode && + copyDownContext.sourceRowIndex === rowIndex && + copyDownContext.sourceFieldKey === 'item_number'; + + // Check if this cell is in a row that can be a target for copy down + const isInTargetRow = copyDownContext.isInCopyDownMode && + copyDownContext.sourceFieldKey === 'item_number' && + rowIndex > (copyDownContext.sourceRowIndex || 0); + + // Check if this row is the currently selected target row + const isSelectedTarget = isInTargetRow && rowIndex <= (copyDownContext.targetRowIndex || 0); + + // Handle click on a potential target cell + const handleTargetCellClick = () => { + if (isInTargetRow && copyDownContext.sourceRowIndex !== null) { + copyDownContext.handleCopyDownComplete( + copyDownContext.sourceRowIndex, + 'item_number', + rowIndex + ); + } + }; + //item_number fields return ( - + setIsTargetRowHovered(true) : undefined} + onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined} + >
- {shouldShowErrorIcon && ( + {shouldShowErrorIcon && !isInTargetRow && (
)} - {!shouldShowErrorIcon && copyDown && !isEmpty(displayValue) && ( + {!shouldShowErrorIcon && copyDown && !isEmpty(displayValue) && !copyDownContext.isInCopyDownMode && (
- -

Copy value to all cells below

+ +
+

Copy value to rows below

+

Click to select target rows

+
+
+
+
+
+ )} + {isSourceCell && ( +
+ + + + + + +

Cancel copy down

)} {isValidating ? ( -
- - {displayValue || ''} +
+ + Loading...
) : ( -
+
)} @@ -316,7 +437,17 @@ const ValidationCell = ({ options = [], itemNumber, width, - copyDown}: ValidationCellProps) => { + copyDown, + rowIndex, + totalRows = 0}: ValidationCellProps) => { + // Add state for hover on copy down button + const [isCopyDownHovered, setIsCopyDownHovered] = React.useState(false); + // Add state for hover on target row + const [isTargetRowHovered, setIsTargetRowHovered] = React.useState(false); + + // Get copy down context + const copyDownContext = React.useContext(CopyDownContext); + // For item_number fields, use the specialized component if (fieldKey === 'item_number') { return ( @@ -329,6 +460,8 @@ const ValidationCell = ({ field={field} onChange={onChange} copyDown={copyDown} + rowIndex={rowIndex} + totalRows={totalRows} /> ); } @@ -360,14 +493,59 @@ const ValidationCell = ({ return { hasError, isRequiredButEmpty, shouldShowErrorIcon, errorMessages }; }, [filteredErrors, value, errors]); - // Check if this is a multiline field + // Handle copy down button click + const handleCopyDownClick = () => { + if (copyDown && totalRows > rowIndex + 1) { + // Enter copy down mode + copyDownContext.setIsInCopyDownMode(true); + copyDownContext.setSourceRowIndex(rowIndex); + copyDownContext.setSourceFieldKey(fieldKey); + } + }; - // Check for price field + // Check if this cell is the source of the current copy down operation + const isSourceCell = copyDownContext.isInCopyDownMode && + copyDownContext.sourceRowIndex === rowIndex && + copyDownContext.sourceFieldKey === fieldKey; + // Check if this cell is in a row that can be a target for copy down + const isInTargetRow = copyDownContext.isInCopyDownMode && + copyDownContext.sourceFieldKey === fieldKey && + rowIndex > (copyDownContext.sourceRowIndex || 0); + + // Check if this row is the currently selected target row + const isSelectedTarget = isInTargetRow && rowIndex <= (copyDownContext.targetRowIndex || 0); + + // Handle click on a potential target cell + const handleTargetCellClick = () => { + if (isInTargetRow && copyDownContext.sourceRowIndex !== null && copyDownContext.sourceFieldKey !== null) { + copyDownContext.handleCopyDownComplete( + copyDownContext.sourceRowIndex, + copyDownContext.sourceFieldKey, + rowIndex + ); + } + }; + //normal selects, normal inputs, not item_number or multi-select return ( - + setIsTargetRowHovered(true) : undefined} + onMouseLeave={isInTargetRow ? () => setIsTargetRowHovered(false) : undefined} + >
- {shouldShowErrorIcon && ( + {shouldShowErrorIcon && !isInTargetRow && (
)} - {!shouldShowErrorIcon && copyDown && !isEmpty(value) && ( + {!shouldShowErrorIcon && copyDown && !isEmpty(value) && !copyDownContext.isInCopyDownMode && (
- -

Copy value to all cells below

+ +
+

Copy value to rows below

+

Click to select target rows

+
+
+
+
+
+ )} + {isSourceCell && ( +
+ + + + + + +

Cancel copy down

@@ -400,13 +604,18 @@ const ValidationCell = ({ Loading...
) : ( -
+
)} diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx index 16014cd..1a3eb8d 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx @@ -72,6 +72,9 @@ const ValidationContainer = ({ const [isValidatingUpc, setIsValidatingUpc] = useState(false); const [validatingUpcRows, setValidatingUpcRows] = useState>(new Set()); + // Add state for tracking cells in loading state + const [validatingCells, setValidatingCells] = useState>(new Set()); + // Store item numbers in a separate state to avoid updating the main data const [itemNumbers, setItemNumbers] = useState>({}); @@ -835,29 +838,79 @@ const ValidationContainer = ({ }, [enhancedUpdateRow]); // Enhanced copy down that uses enhancedUpdateRow instead of regular updateRow - const handleCopyDown = useCallback((rowIndex: number, fieldKey: string) => { + const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => { // Get the value to copy from the source row const sourceRow = data[rowIndex]; const valueToCopy = sourceRow[fieldKey]; + + // Create a proper copy of the value to avoid reference issues, especially for arrays (MultiSelectCell) + const valueCopy = Array.isArray(valueToCopy) ? [...valueToCopy] : valueToCopy; - // Get all rows below the source row - const rowsBelow = data.slice(rowIndex + 1); - - // Update each row below with the copied value - rowsBelow.forEach((_, index) => { + // Get all rows below the source row, up to endRowIndex if specified + const lastRowIndex = endRowIndex !== undefined ? Math.min(endRowIndex, data.length - 1) : data.length - 1; + const rowsToUpdate = data.slice(rowIndex + 1, lastRowIndex + 1); + + // Create a set of cells that will be in loading state + const loadingCells = new Set(); + + // Add all target cells to the loading state + rowsToUpdate.forEach((_, index) => { const targetRowIndex = rowIndex + 1 + index; - enhancedUpdateRow(targetRowIndex, fieldKey as T, valueToCopy); + loadingCells.add(`${targetRowIndex}-${fieldKey}`); }); - }, [data, enhancedUpdateRow]); + + // Update validatingCells to show loading state + setValidatingCells(prev => { + const newSet = new Set(prev); + loadingCells.forEach(cell => newSet.add(cell)); + return newSet; + }); + + // Use setTimeout to allow the UI to update with loading state before processing + setTimeout(() => { + // Update each row sequentially with a small delay for visual feedback + const updateSequentially = async () => { + for (let i = 0; i < rowsToUpdate.length; i++) { + const targetRowIndex = rowIndex + 1 + i; + + // Update the row with the copied value + enhancedUpdateRow(targetRowIndex, fieldKey as T, valueCopy); + + // Remove loading state after a short delay + setTimeout(() => { + setValidatingCells(prev => { + const newSet = new Set(prev); + newSet.delete(`${targetRowIndex}-${fieldKey}`); + return newSet; + }); + }, 100); // Short delay before removing loading state + + // Add a small delay between updates for visual effect + if (i < rowsToUpdate.length - 1) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + } + }; + + updateSequentially(); + }, 50); + }, [data, enhancedUpdateRow, setValidatingCells]); // Memoize the enhanced validation table component const EnhancedValidationTable = useMemo(() => React.memo((props: React.ComponentProps) => { // Create validatingCells set from validatingUpcRows, but only for item_number fields // This ensures only the item_number column shows loading state during UPC validation - const validatingCells = new Set(); + const combinedValidatingCells = new Set(); + + // Add UPC validation cells validatingUpcRows.forEach(rowIndex => { // Only mark the item_number cells as validating, NOT the UPC or supplier - validatingCells.add(`${rowIndex}-item_number`); + combinedValidatingCells.add(`${rowIndex}-item_number`); + }); + + // Add any other validating cells from state + validatingCells.forEach(cellKey => { + combinedValidatingCells.add(cellKey); }); // Convert itemNumbers to Map @@ -878,13 +931,13 @@ const ValidationContainer = ({ ); - }), [validatingUpcRows, itemNumbers, isLoadingTemplates, handleCopyDown]); + }), [validatingUpcRows, itemNumbers, isLoadingTemplates, handleCopyDown, validatingCells]); // Memoize the rendered validation table const renderValidationTable = useMemo(() => ( diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx index 935740c..42a161f 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationTable.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback } from 'react' +import React, { useMemo, useCallback, useState } from 'react' import { useReactTable, getCoreRowModel, @@ -8,7 +8,7 @@ import { } from '@tanstack/react-table' import { Fields, Field } from '../../../types' import { RowData, Template } from '../hooks/useValidationState' -import ValidationCell from './ValidationCell' +import ValidationCell, { CopyDownContext } from './ValidationCell' import { useRsi } from '../../../hooks/useRsi' import SearchableTemplateSelect from './SearchableTemplateSelect' import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table' @@ -45,7 +45,7 @@ interface ValidationTableProps { validatingCells: Set itemNumbers: Map isLoadingTemplates?: boolean - copyDown: (rowIndex: number, key: string) => void + copyDown: (rowIndex: number, key: string, endRowIndex?: number) => void [key: string]: any } @@ -106,7 +106,8 @@ const MemoizedCell = React.memo(({ itemNumber, width, rowIndex, - copyDown + copyDown, + totalRows }: { field: Field, value: any, @@ -118,7 +119,8 @@ const MemoizedCell = React.memo(({ itemNumber?: string, width: number, rowIndex: number, - copyDown?: () => void + copyDown?: (endRowIndex?: number) => void, + totalRows: number }) => { return ( ); }, (prev, next) => { @@ -167,6 +170,50 @@ const ValidationTable = ({ }: ValidationTableProps) => { const { translations } = useRsi(); + // Add state for copy down selection mode + const [isInCopyDownMode, setIsInCopyDownMode] = useState(false); + const [sourceRowIndex, setSourceRowIndex] = useState(null); + const [sourceFieldKey, setSourceFieldKey] = useState(null); + const [targetRowIndex, setTargetRowIndex] = useState(null); + + // Handle copy down completion + const handleCopyDownComplete = useCallback((sourceRowIndex: number, fieldKey: string, targetRowIndex: number) => { + // Call the copyDown function with the source row index, field key, and target row index + copyDown(sourceRowIndex, fieldKey, targetRowIndex); + + // Reset the copy down selection mode + setIsInCopyDownMode(false); + setSourceRowIndex(null); + setSourceFieldKey(null); + setTargetRowIndex(null); + }, [copyDown]); + + // Create copy down context value + const copyDownContextValue = useMemo(() => ({ + isInCopyDownMode, + sourceRowIndex, + sourceFieldKey, + targetRowIndex, + setIsInCopyDownMode, + setSourceRowIndex, + setSourceFieldKey, + setTargetRowIndex, + handleCopyDownComplete + }), [ + isInCopyDownMode, + sourceRowIndex, + sourceFieldKey, + targetRowIndex, + handleCopyDownComplete + ]); + + // Update targetRowIndex when hovering over rows in copy down mode + const handleRowMouseEnter = useCallback((rowIndex: number) => { + if (isInCopyDownMode && sourceRowIndex !== null && rowIndex > sourceRowIndex) { + setTargetRowIndex(rowIndex); + } + }, [isInCopyDownMode, sourceRowIndex]); + // Memoize the selection column with stable callback const handleSelectAll = useCallback((value: boolean, table: any) => { table.toggleAllPageRowsSelected(!!value); @@ -255,8 +302,8 @@ const ValidationTable = ({ }, [updateRow]); // Memoize the copyDown handler - const handleCopyDown = useCallback((rowIndex: number, fieldKey: string) => { - copyDown(rowIndex, fieldKey); + const handleCopyDown = useCallback((rowIndex: number, fieldKey: string, endRowIndex?: number) => { + copyDown(rowIndex, fieldKey, endRowIndex); }, [copyDown]); // Memoize field columns with stable handlers @@ -292,12 +339,13 @@ const ValidationTable = ({ itemNumber={itemNumbers.get(row.index)} width={fieldWidth} rowIndex={row.index} - copyDown={() => handleCopyDown(row.index, field.key as string)} + copyDown={(endRowIndex?: number) => handleCopyDown(row.index, field.key as string, endRowIndex)} + totalRows={data.length} /> ) }; }).filter((col): col is ColumnDef, any> => col !== null), - [fields, validationErrors, validatingCells, itemNumbers, handleFieldUpdate, handleCopyDown, optionsCache]); + [fields, validationErrors, validatingCells, itemNumbers, handleFieldUpdate, handleCopyDown, optionsCache, data.length]); // Combine columns const columns = useMemo(() => [selectionColumn, templateColumn, ...fieldColumns], [selectionColumn, templateColumn, fieldColumns]); @@ -335,71 +383,118 @@ const ValidationTable = ({ } return ( -
-
- {/* Custom Table Header - Always Visible */} -
-
- {table.getFlatHeaders().map((header, index) => { - const width = header.getSize(); - return ( -
- {flexRender(header.column.columnDef.header, header.getContext())} -
- ); - })} -
-
- - {/* Table Body */} - - - {table.getRowModel().rows.map((row) => ( - 0 ? "bg-red-50/40" : "" - )} - > - {row.getVisibleCells().map((cell, cellIndex) => { - const width = cell.column.getSize(); - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + +
+ {isInCopyDownMode && sourceRowIndex !== null && sourceFieldKey !== null && ( +
+
{ + // Find the column index + const colIndex = columns.findIndex(col => + 'accessorKey' in col && col.accessorKey === sourceFieldKey ); - })} - - ))} - -
+ + // If column not found, position at a default location + if (colIndex === -1) return '50px'; + + // Calculate position based on column widths + let position = 0; + for (let i = 0; i < colIndex; i++) { + position += columns[i].size || 0; + } + + // Add half of the current column width to center it + position += (columns[colIndex].size || 0) / 2; + + // Adjust to center the notification + position -= 120; // Half of the notification width + + return `${Math.max(50, position)}px`; + })() + }} + > +
+ Click on the last row you want to copy to +
+ +
+
+ )} +
+ {/* Custom Table Header - Always Visible */} +
+
+ {table.getFlatHeaders().map((header, index) => { + const width = header.getSize(); + return ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ ); + })} +
+
+ + {/* Table Body */} + + + {table.getRowModel().rows.map((row) => ( + 0 ? "bg-red-50/40" : "" + )} + onMouseEnter={() => handleRowMouseEnter(row.index)} + > + {row.getVisibleCells().map((cell, cellIndex) => { + const width = cell.column.getSize(); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + ))} + +
+
-
+ ); }; diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx index d3e04d9..db18398 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/CheckboxCell.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { Field } from '../../../../types' import { Checkbox } from '@/components/ui/checkbox' import { cn } from '@/lib/utils' +import React from 'react' interface CheckboxCellProps { field: Field @@ -9,6 +10,7 @@ interface CheckboxCellProps { onChange: (value: any) => void hasErrors?: boolean booleanMatches?: Record + className?: string } const CheckboxCell = ({ @@ -16,9 +18,12 @@ const CheckboxCell = ({ value, onChange, hasErrors, - booleanMatches = {} + booleanMatches = {}, + className = '' }: CheckboxCellProps) => { const [checked, setChecked] = useState(false) + // Add state for hover + const [isHovered, setIsHovered] = useState(false) // Initialize checkbox state useEffect(() => { @@ -70,6 +75,12 @@ const CheckboxCell = ({ setChecked(!!value) }, [value, field.fieldType, booleanMatches]) + // 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); + }; + // Handle checkbox change const handleChange = useCallback((checked: boolean) => { setChecked(checked) @@ -80,11 +91,27 @@ const CheckboxCell = ({ const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0" return ( -
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > ({ ) } -export default CheckboxCell \ No newline at end of file +export default React.memo(CheckboxCell, (prev, next) => { + if (prev.hasErrors !== next.hasErrors) return false; + if (prev.field !== next.field) return false; + if (prev.value !== next.value) return false; + if (prev.className !== next.className) return false; + + // Compare booleanMatches objects + const prevMatches = prev.booleanMatches || {}; + const nextMatches = next.booleanMatches || {}; + const prevKeys = Object.keys(prevMatches); + const nextKeys = Object.keys(nextMatches); + + if (prevKeys.length !== nextKeys.length) return false; + + for (const key of prevKeys) { + if (prevMatches[key] !== nextMatches[key]) return false; + } + + return true; +}); \ No newline at end of file diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx index 8cdc9f9..d62f9b4 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/InputCell.tsx @@ -14,6 +14,7 @@ interface InputCellProps { isMultiline?: boolean isPrice?: boolean disabled?: boolean + className?: string } // Add efficient price formatting utility @@ -39,7 +40,8 @@ const InputCell = ({ hasErrors, isMultiline = false, isPrice = false, - disabled = false + disabled = false, + className = '' }: InputCellProps) => { const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(''); @@ -52,6 +54,15 @@ const InputCell = ({ // Track local display value to avoid waiting for validation const [localDisplayValue, setLocalDisplayValue] = useState(null); + // Add state for hover + const [isHovered, setIsHovered] = useState(false); + + // 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 || @@ -151,11 +162,26 @@ const InputCell = ({ // If disabled, just render the value without any interactivity if (disabled) { return ( -
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > {displayValue}
); @@ -170,6 +196,7 @@ const InputCell = ({ onChange={onChange} hasErrors={hasErrors} disabled={disabled} + className={className} /> ); } @@ -188,8 +215,18 @@ const InputCell = ({ className={cn( outlineClass, hasErrors ? "border-destructive" : "", - isPending ? "opacity-50" : "" + isPending ? "opacity-50" : "", + className )} + style={{ + backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' : + hasClass('!bg-blue-200') ? '#bfdbfe' : + undefined, + borderColor: hasClass('!border-blue-500') ? '#3b82f6' : + hasClass('!border-blue-200') ? '#bfdbfe' : + undefined, + borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined + }} /> ) : (
({ outlineClass, hasErrors ? "border-destructive" : "border-input" )} + style={{ + backgroundColor: hasClass('!bg-blue-100') ? '#dbeafe' : + hasClass('!bg-blue-200') ? '#bfdbfe' : + hasClass('hover:!bg-blue-100') && isHovered ? '#dbeafe' : + undefined, + borderColor: hasClass('!border-blue-500') ? '#3b82f6' : + hasClass('!border-blue-200') ? '#bfdbfe' : + hasClass('!border-blue-200') && isHovered ? '#bfdbfe' : + undefined, + borderRadius: hasClass('!rounded-md') ? '0.375rem' : undefined + }} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} > {displayValue}
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx index cecd8a5..41058db 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiSelectCell.tsx @@ -22,6 +22,7 @@ interface MultiSelectCellProps { hasErrors?: boolean options?: readonly FieldOption[] disabled?: boolean + className?: string } // Memoized option item to prevent unnecessary renders for large option lists @@ -155,14 +156,17 @@ const MultiSelectCell = ({ onEndEdit, hasErrors, options: providedOptions, - disabled = false + disabled = false, + className = '' }: MultiSelectCellProps) => { const [open, setOpen] = useState(false) const [searchQuery, setSearchQuery] = useState("") - // Add internal state for tracking selections - const [internalValue, setInternalValue] = useState(value) + // Add internal state for tracking selections - ensure value is always an array + const [internalValue, setInternalValue] = useState(Array.isArray(value) ? value : []) // Ref for the command list to enable scrolling const commandListRef = useRef(null) + // Add state for hover + const [isHovered, setIsHovered] = useState(false) // Create a memoized Set for fast lookups of selected values const selectedValueSet = useMemo(() => new Set(internalValue), [internalValue]); @@ -170,7 +174,8 @@ const MultiSelectCell = ({ // Sync internalValue with external value when component mounts or value changes externally useEffect(() => { if (!open) { - setInternalValue(value) + // Ensure value is always an array + setInternalValue(Array.isArray(value) ? value : []) } }, [value, open]) @@ -306,35 +311,56 @@ const MultiSelectCell = ({ } }, []); - // If disabled, render a static view + // 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); + }; + + // If disabled, just render the value without any interactivity if (disabled) { - // Handle array values - const displayValue = Array.isArray(value) - ? value.map(v => { - const option = providedOptions?.find(o => o.value === v); - return option ? option.label : v; + const displayValue = internalValue.length > 0 + ? internalValue.map(val => { + const option = selectOptions.find(opt => opt.value === val); + return option ? option.label : val; }).join(', ') - : value; - + : ''; + return ( -
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > {displayValue || ""}
); } return ( - { - setOpen(isOpen); - handleOpenChange(isOpen); - }} - > + { + // Only open the popover if we're not in copy down mode + if (!hasClass('!bg-blue-100') && !hasClass('!bg-blue-200') && !hasClass('hover:!bg-blue-100')) { + setOpen(o); + handleOpenChange(o); + } + }}>