diff --git a/docs/validate-table-changes.md b/docs/validate-table-changes.md index 55890ab..9627514 100644 --- a/docs/validate-table-changes.md +++ b/docs/validate-table-changes.md @@ -1,6 +1,4 @@ # Current Issues to Address -1. The red row background should go away when all cells in the row are valid and all required cells are populated -2. Columns alignment with header is slightly off, gets worse the further right you go 3. The copy down button is in the way of the validation error icon and the select open trigger - all three need to be in unique locations 4. Validation isn't happening beyond checking if a cell is required or not - needs to respect rules in import.tsx * Red cell outline if cell is required and it's empty @@ -14,15 +12,19 @@ 10. UPC column doesn't need to show loading state when Item Number is being processed, only show on item number column 11. Copy down needs to show a loading state on the cells that it will copy to 12. Shipping restrictions/tax category should default to ID 0 if we didn't get it elsewhere -13. Header row should be sticky (both up/down and left/right) 14. Need a way to scroll around table if user doesn't have mouse wheel for left/right -15. Need to remove all artificial virtualization, batching, artificial delays, and caching. Adds too much complexity and data set is not ever large enough for this to be helpful. Keep actual performance optimizations. + ## Do NOT change or edit * Anything related to AI validation * Anything about how templates or UPC validation work (only focus on specific issues described above) * Anything outside of the ValidationStepNew folder +## Issues already fixed - do not work on these +✅FIXED 1. The red row background should go away when all cells in the row are valid and all required cells are populated +✅FIXED 2. Columns alignment with header is slightly off, gets worse the further right you go +✅FIXED 13. Header row should be sticky (both up/down and left/right) + --------- # Validation Step Components Overview 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 0dd7522..3caaebf 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 @@ -249,7 +249,7 @@ const ItemNumberCell = React.memo(({ ); return ( - +
{isValidating ? (
@@ -365,7 +365,7 @@ const ValidationCell = ({ // Check for price field return ( - +
{isValidating ? (
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 d2226a6..2badb5a 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 @@ -14,6 +14,7 @@ import { Fields } from '../../../types' import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog' import { TemplateForm } from '@/components/templates/TemplateForm' import axios from 'axios' +import { RowSelectionState } from '@tanstack/react-table' /** * ValidationContainer component - the main wrapper for the validation step @@ -56,13 +57,15 @@ const ValidationContainer = ({ loadTemplates, setData, fields, - isLoadingTemplates, - copyDown } = validationState + isLoadingTemplates } = validationState // Add state for tracking product lines and sublines per row const [rowProductLines, setRowProductLines] = useState>({}); const [rowSublines, setRowSublines] = useState>({}); + // These variables are used in the fetchProductLines and fetchSublines functions + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isLoadingLines, setIsLoadingLines] = useState>({}); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isLoadingSublines, setIsLoadingSublines] = useState>({}); // Add UPC validation state @@ -432,7 +435,10 @@ const ValidationContainer = ({ if (rowData && rowData.__index) { // Use setTimeout to make this non-blocking setTimeout(async () => { - await fetchProductLines(rowData.__index, value.toString()); + // Ensure value is not undefined before calling toString() + if (value !== undefined) { + await fetchProductLines(rowData.__index as string, value.toString()); + } }, 0); } } @@ -494,7 +500,10 @@ const ValidationContainer = ({ if (rowData && rowData.__index) { // Use setTimeout to make this non-blocking setTimeout(async () => { - await fetchSublines(rowData.__index, value.toString()); + // Ensure value is not undefined before calling toString() + if (value !== undefined) { + await fetchSublines(rowData.__index as string, value.toString()); + } }, 0); } } @@ -711,7 +720,7 @@ const ValidationContainer = ({ // Log if we can find a match for our supplier if (templateData.supplier !== undefined) { // Need to compare numeric values since supplier options have numeric values - const supplierMatch = options.suppliers.find(s => + const supplierMatch = options.suppliers.find((s: { value: string | number }) => s.value === templateData.supplier || Number(s.value) === Number(templateData.supplier) ); @@ -814,9 +823,8 @@ const ValidationContainer = ({ }, [data, rowSelection, setData, setRowSelection]); // Memoize handlers - const handleFiltersChange = useCallback((newFilters: any) => { - updateFilters(newFilters); - }, [updateFilters]); + // This function is defined for potential future use but not currently used + // eslint-disable-next-line @typescript-eslint/no-unused-vars const handleRowSelectionChange = useCallback((newSelection: RowSelectionState) => { setRowSelection(newSelection); @@ -883,10 +891,10 @@ const ValidationContainer = ({ const renderValidationTable = useMemo(() => ( } rowSelection={rowSelection} - setRowSelection={handleRowSelectionChange} - updateRow={handleUpdateRow} + setRowSelection={handleRowSelectionChange as React.Dispatch>} + updateRow={handleUpdateRow as (rowIndex: number, key: string, value: any) => void} validationErrors={validationErrors} isValidatingUpc={isRowValidatingUpc} validatingUpcRows={Array.from(validatingUpcRows)} @@ -898,6 +906,7 @@ const ValidationContainer = ({ itemNumbers={new Map()} isLoadingTemplates={isLoadingTemplates} copyDown={handleCopyDown} + upcValidationResults={new Map()} /> ), [ EnhancedValidationTable, @@ -923,10 +932,11 @@ const ValidationContainer = ({ const isScrolling = useRef(false); // Memoize scroll handlers - const handleScroll = useCallback((event: React.UIEvent) => { + const handleScroll = useCallback((event: React.UIEvent | Event) => { if (!isScrolling.current) { isScrolling.current = true; - const target = event.currentTarget; + // Use type assertion to handle both React.UIEvent and native Event + const target = event.currentTarget as HTMLDivElement; lastScrollPosition.current = { left: target.scrollLeft, top: target.scrollTop @@ -941,8 +951,13 @@ const ValidationContainer = ({ useEffect(() => { const container = scrollContainerRef.current; if (container) { - container.addEventListener('scroll', handleScroll, { passive: true }); - return () => container.removeEventListener('scroll', handleScroll); + // Convert React event handler to native event handler + const nativeHandler = ((evt: Event) => { + handleScroll(evt); + }) as EventListener; + + container.addEventListener('scroll', nativeHandler, { passive: true }); + return () => container.removeEventListener('scroll', nativeHandler); } }, [handleScroll]); @@ -1031,13 +1046,14 @@ const ValidationContainer = ({ style={{ willChange: 'transform', position: 'relative', - WebkitOverflowScrolling: 'touch' // Improve scroll performance on Safari + WebkitOverflowScrolling: 'touch', // Improve scroll performance on Safari + overscrollBehavior: 'contain', // Prevent scroll chaining + contain: 'paint', // Improve performance for sticky elements + scrollbarWidth: 'thin' // Thinner scrollbars in Firefox }} onScroll={handleScroll} > -
{/* Force container to be at least as wide as content */} - {renderValidationTable} -
+ {renderValidationTable}
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 24aace9..a3a524b 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 @@ -217,7 +217,7 @@ const ValidationTable = ({ const rowIndex = data.findIndex(r => r === row.original); return ( - + ({ size: fieldWidth, cell: ({ row }) => ( handleFieldUpdate(row.index, field.key, value)} + field={field as Field} + value={row.original[field.key as keyof typeof row.original]} + onChange={(value) => handleFieldUpdate(row.index, field.key as T, value)} errors={validationErrors.get(row.index)?.[fieldKey] || []} isValidating={validatingCells.has(`${row.index}-${field.key}`)} fieldKey={fieldKey} @@ -290,7 +290,7 @@ const ValidationTable = ({ itemNumber={itemNumbers.get(row.index)} width={fieldWidth} rowIndex={row.index} - copyDown={() => handleCopyDown(row.index, field.key)} + copyDown={() => handleCopyDown(row.index, field.key as string)} /> ) }; @@ -333,53 +333,71 @@ const ValidationTable = ({ } return ( - - - - {table.getFlatHeaders().map((header) => ( - - {flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - +
+ {/* 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" : "" + )} > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + {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/MultiInputCell.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx index fa25659..fa42117 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/cells/MultiInputCell.tsx @@ -30,7 +30,6 @@ interface MultiInputCellProps { } // Add global CSS to ensure fixed width constraints - use !important to override other styles -const fixedWidthClass = "!w-full !min-w-0 !max-w-full !flex-shrink-1 !flex-grow-0"; // Memoized option item to prevent unnecessary renders for large option lists const OptionItem = React.memo(({ @@ -372,133 +371,91 @@ const MultiInputCell = ({ // If we have a multi-select field with options, use command UI if (field.fieldType.type === 'multi-select' && selectOptions.length > 0) { - // Get width from field if available, or default to a reasonable value - const cellWidth = field.width || 200; - - // Create a reference to the container element - const containerRef = useRef(null); - - // Create a key-value map for inline styles with fixed width - simplified - const fixedWidth = useMemo(() => ({ - width: `${cellWidth}px`, - minWidth: `${cellWidth}px`, - maxWidth: `${cellWidth}px`, - boxSizing: 'border-box' as const, - }), [cellWidth]); - - // Use layout effect more efficiently - only for the button element - // since the container already uses inline styles - useLayoutEffect(() => { - // Skip if no width specified - if (!cellWidth) return; - - // Cache previous width to avoid unnecessary DOM updates - const prevWidth = containerRef.current?.getAttribute('data-prev-width'); - - // Only update if width changed - if (prevWidth !== String(cellWidth) && containerRef.current) { - // Store new width for next comparison - containerRef.current.setAttribute('data-prev-width', String(cellWidth)); - - // Only manipulate the button element directly since we can't - // reliably style it with CSS in all cases - const button = containerRef.current.querySelector('button'); - if (button) { - const htmlButton = button as HTMLElement; - htmlButton.style.width = `${cellWidth}px`; - htmlButton.style.minWidth = `${cellWidth}px`; - htmlButton.style.maxWidth = `${cellWidth}px`; - } - } - }, [cellWidth]); - return ( -
{ + setOpen(isOpen); + handleOpenChange(isOpen); + }} > - - - - - +
- ) +
+
+ {internalValue.length === 0 ? ( + Select... + ) : internalValue.length === 1 ? ( + {selectedValues[0].label} + ) : ( + <> + + {internalValue.length} selected + + + {selectedValues.map(v => v.label).join(', ')} + + + )} +
+ +
+ + + + + + + No results found. + + {sortedOptions.map((option) => ( + handleSelect(option.value)} + className="cursor-pointer" + > + {option.label} + {selectedValueSet.has(option.value) && ( + + )} + + ))} + + + + + + ); } // For standard multi-input without options, use text input @@ -510,12 +467,6 @@ const MultiInputCell = ({ const containerRef = useRef(null); // Create a key-value map for inline styles with fixed width - simplified - const fixedWidth = useMemo(() => ({ - width: `${cellWidth}px`, - minWidth: `${cellWidth}px`, - maxWidth: `${cellWidth}px`, - boxSizing: 'border-box' as const, - }), [cellWidth]); // Use layout effect more efficiently - only for the button element // since the container already uses inline styles @@ -539,6 +490,7 @@ const MultiInputCell = ({ htmlButton.style.width = `${cellWidth}px`; htmlButton.style.minWidth = `${cellWidth}px`; htmlButton.style.maxWidth = `${cellWidth}px`; + htmlButton.style.boxSizing = 'border-box'; } } }, [cellWidth]); @@ -547,7 +499,12 @@ const MultiInputCell = ({
{isMultiline ? ( @@ -562,7 +519,12 @@ const MultiInputCell = ({ outlineClass, hasErrors ? "border-destructive" : "" )} - style={fixedWidth} + style={{ + width: `${cellWidth}px`, + minWidth: `${cellWidth}px`, + maxWidth: `${cellWidth}px`, + boxSizing: 'border-box', + }} /> ) : (
({ "cursor-text truncate", outlineClass, hasErrors ? "border-destructive" : "", - "overflow-hidden items-center" + "overflow-hidden items-center", + "w-full" )} onClick={handleFocus} - style={fixedWidth} + style={{ + width: `${cellWidth}px`, + minWidth: `${cellWidth}px`, + maxWidth: `${cellWidth}px`, + boxSizing: 'border-box', + }} > {internalValue.length > 0 ? getDisplayValues().join(`, `) : ( @@ -589,7 +557,7 @@ const MultiInputCell = ({ // Fallback to default behavior if no width is specified return ( -
+
{isMultiline ? (