+
)}
@@ -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 && (
setIsCopyDownHovered(true)}
+ onMouseLeave={() => setIsCopyDownHovered(false)}
+ className="p-1 rounded-full hover:bg-blue-100 text-blue-500/70 hover:text-blue-600 transition-colors"
+ aria-label="Copy value to rows below"
>
-
- Copy value to all cells below
+
+
+
Copy value to rows below
+
Click to select target rows
+
+
+
+
+
+ )}
+ {isSourceCell && (
+
+
+
+
+ copyDownContext.setIsInCopyDownMode(false)}
+ className="p-1 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 transition-colors"
+ aria-label="Cancel copy down"
+ >
+
+
+
+
+ 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
+
+
setIsInCopyDownMode(false)}
+ className="text-xs h-7 border-blue-200 text-blue-700 hover:bg-blue-100"
+ >
+ Cancel
+
+
+
+ )}
+
+ {/* 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);
+ }
+ }}>
({
"w-full justify-between font-normal",
"border",
!internalValue.length && "text-muted-foreground",
- hasErrors ? "border-destructive" : ""
+ hasErrors ? "border-destructive" : "",
+ className
)}
+ 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,
+ borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined
+ }}
+ onClick={(e) => {
+ // Don't open the dropdown if we're in copy down mode
+ if (hasClass('!bg-blue-100') || hasClass('!bg-blue-200') || hasClass('hover:!bg-blue-100')) {
+ // Let the parent cell handle the click by NOT preventing default or stopping propagation
+ return;
+ }
+
+ // Only prevent default and stop propagation if not in copy down mode
+ e.preventDefault();
+ e.stopPropagation();
+ setOpen(!open);
+ }}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
>
@@ -411,46 +464,37 @@ const MultiSelectCell =
({
MultiSelectCell.displayName = 'MultiSelectCell';
export default React.memo(MultiSelectCell, (prev, next) => {
- // Quick check for reference equality of simple props
- if (prev.hasErrors !== next.hasErrors ||
- prev.disabled !== next.disabled) {
- return false;
+ // Check primitive props first (cheap comparisons)
+ if (prev.hasErrors !== next.hasErrors) return false;
+ if (prev.disabled !== next.disabled) return false;
+ if (prev.className !== next.className) return false;
+
+ // Check field reference
+ if (prev.field !== next.field) return false;
+
+ // Check value arrays (potentially expensive for large arrays)
+ // Handle undefined or null values safely
+ const prevValue = prev.value || [];
+ const nextValue = next.value || [];
+
+ if (prevValue.length !== nextValue.length) return false;
+ for (let i = 0; i < prevValue.length; i++) {
+ if (prevValue[i] !== nextValue[i]) return false;
}
- // Array comparison for value
- if (Array.isArray(prev.value) && Array.isArray(next.value)) {
- if (prev.value.length !== next.value.length) return false;
-
- // Check each item in the array - optimize for large arrays
- if (prev.value.length > 50) {
- // For large arrays, JSON stringify is actually faster than iterating
- return JSON.stringify(prev.value) === JSON.stringify(next.value);
- }
-
- // For smaller arrays, iterative comparison is more efficient
- for (let i = 0; i < prev.value.length; i++) {
- if (prev.value[i] !== next.value[i]) return false;
- }
- } else if (prev.value !== next.value) {
- return false;
+ // Check options (potentially expensive for large option lists)
+ const prevOptions = prev.options || [];
+ const nextOptions = next.options || [];
+ if (prevOptions.length !== nextOptions.length) return false;
+
+ // For large option lists, just compare references
+ if (prevOptions.length > 100) {
+ return prevOptions === nextOptions;
}
- // Only do a full options comparison if they are different references and small arrays
- if (prev.options !== next.options) {
- if (!prev.options || !next.options) return false;
- if (prev.options.length !== next.options.length) return false;
-
- // For large option lists, just check reference equality
- if (prev.options.length > 100) return false;
-
- // For smaller lists, check if any values differ
- for (let i = 0; i < prev.options.length; i++) {
- const prevOpt = prev.options[i];
- const nextOpt = next.options[i];
- if (prevOpt.value !== nextOpt.value || prevOpt.label !== nextOpt.label) {
- return false;
- }
- }
+ // For smaller lists, do a shallow comparison
+ for (let i = 0; i < prevOptions.length; i++) {
+ if (prevOptions[i] !== nextOptions[i]) return false;
}
return true;
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx
index adb10b7..3521872 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultilineInput.tsx
@@ -12,6 +12,7 @@ interface MultilineInputProps {
onChange: (value: any) => void
hasErrors?: boolean
disabled?: boolean
+ className?: string
}
const MultilineInput = ({
@@ -19,7 +20,8 @@ const MultilineInput = ({
value,
onChange,
hasErrors = false,
- disabled = false
+ disabled = false,
+ className = ''
}: MultilineInputProps) => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [editValue, setEditValue] = useState('');
@@ -27,7 +29,16 @@ const MultilineInput = ({
const cellRef = useRef(null);
const preventReopenRef = useRef(false);
const pendingChangeRef = useRef(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 ||
@@ -123,11 +134,26 @@ const MultilineInput = ({
// If disabled, just render the value without any interactivity
if (disabled) {
return (
-
+
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
{displayValue}
);
@@ -143,8 +169,22 @@ const MultilineInput =
({
"px-3 py-2 min-h-[80px] max-h-[80px] rounded-md text-sm w-full cursor-pointer",
"overflow-hidden whitespace-pre-wrap",
outlineClass,
- hasErrors ? "border-destructive" : "border-input"
+ hasErrors ? "border-destructive" : "border-input",
+ className
)}
+ 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}
@@ -189,5 +229,6 @@ export default React.memo(MultilineInput, (prev, next) => {
if (prev.disabled !== next.disabled) return false;
if (prev.field !== next.field) return false;
if (prev.value !== next.value) return false;
+ if (prev.className !== next.className) return false;
return true;
});
\ No newline at end of file
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx
index 04f6a29..75b4f7f 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/SelectCell.tsx
@@ -21,6 +21,7 @@ interface SelectCellProps {
hasErrors?: boolean
options: readonly any[]
disabled?: boolean
+ className?: string
}
// Lightweight version of the select cell with minimal dependencies
@@ -32,7 +33,8 @@ const SelectCell = ({
onEndEdit,
hasErrors,
options = [],
- disabled = false
+ disabled = false,
+ className = ''
}: SelectCellProps) => {
// State for the open/closed state of the dropdown
const [open, setOpen] = useState(false);
@@ -46,6 +48,15 @@ const SelectCell = ({
// State to track if the value is being processed/validated
const [isProcessing, setIsProcessing] = useState(false);
+ // 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);
+ };
+
// Update internal value when prop value changes
useEffect(() => {
setInternalValue(value);
@@ -140,8 +151,23 @@ const SelectCell = ({
"w-full px-3 py-2 h-10 rounded-md text-sm flex items-center",
"border",
hasErrors ? "border-destructive" : "border-input",
- isProcessing ? "text-muted-foreground" : ""
- )}>
+ isProcessing ? "text-muted-foreground" : "",
+ className
+ )}
+ 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)}
+ >
{displayText || ""}
);
@@ -151,8 +177,11 @@ const SelectCell =
({
{
- setOpen(isOpen);
- if (isOpen && onStartEdit) onStartEdit();
+ // 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(isOpen);
+ if (isOpen && onStartEdit) onStartEdit();
+ }
}}
>
@@ -165,14 +194,36 @@ const SelectCell = ({
"border",
!internalValue && "text-muted-foreground",
isProcessing && "text-muted-foreground",
- hasErrors ? "border-destructive" : ""
+ hasErrors ? "border-destructive" : "",
+ className
)}
+ 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,
+ borderWidth: hasClass('!border-blue-500') || hasClass('!border-blue-200') ? '0px' : undefined
+ }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
+
+ // Don't open the dropdown if we're in copy down mode
+ if (hasClass('!bg-blue-100') || hasClass('!bg-blue-200') || hasClass('hover:!bg-blue-100')) {
+ // Let the parent cell handle the click
+ return;
+ }
+
setOpen(!open);
if (!open && onStartEdit) onStartEdit();
}}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
>
{displayValue}
@@ -224,6 +275,7 @@ export default React.memo(SelectCell, (prev, next) => {
if (prev.value !== next.value) return false;
if (prev.hasErrors !== next.hasErrors) return false;
if (prev.disabled !== next.disabled) return false;
+ if (prev.className !== next.className) return false;
// Only check options array for reference equality - we're handling deep comparison internally
if (prev.options !== next.options &&