From bb455b3c37e4a82bba8c6f274c8147b3ebd0be60 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 27 Feb 2025 00:50:31 -0500 Subject: [PATCH] Match columns tweaks --- .../MatchColumnsStep/MatchColumnsStep.tsx | 549 ++++++++++++++---- .../src/translationsRSIProps.ts | 2 +- 2 files changed, 427 insertions(+), 124 deletions(-) diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index 085ba80..1f411b8 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -19,13 +19,21 @@ import { import { useQuery } from "@tanstack/react-query" import config from "@/config" import { Button } from "@/components/ui/button" -import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, FileIcon } from "lucide-react" +import { CheckCircle2, AlertCircle, EyeIcon, EyeOffIcon, ArrowRightIcon, XIcon, FileSpreadsheetIcon, LinkIcon, FileIcon, CheckIcon, ChevronsUpDown } from "lucide-react" import { Separator } from "@/components/ui/separator" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { useVirtualizer } from '@tanstack/react-virtual'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { cn } from "@/lib/utils" export type MatchColumnsProps = { data: RawData[] @@ -166,15 +174,12 @@ const MemoizedColumnSamplePreview = React.memo(({ samples }: { samples: any[] }) -
-

Sample Data

-
- +
{samples.map((sample, i) => (
- {i + 1}: {String(sample || '(empty)')} + {i < samples.length - 1 && }
))}
@@ -264,6 +269,353 @@ const ValueMappings = memo(({ ); }); +// Add these new components before the MatchColumnsStep component +const SupplierSelector = React.memo(({ + value, + onChange, + suppliers +}: { + value?: string; + onChange: (value: string) => void; + suppliers: any[] +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No suppliers found. + + {suppliers?.map((supplier: any) => ( + { + onChange(supplier.value); + setOpen(false); // Close popover after selection + }} + > + {supplier.label} + + + ))} + + + + + + ); +}); + +const CompanySelector = React.memo(({ + value, + onChange, + companies +}: { + value?: string; + onChange: (value: string) => void; + companies: any[] +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No companies found. + + {companies?.map((company: any) => ( + { + onChange(company.value); + setOpen(false); // Close popover after selection + }} + > + {company.label} + + + ))} + + + + + + ); +}); + +const LineSelector = React.memo(({ + value, + onChange, + lines, + disabled +}: { + value?: string; + onChange: (value: string) => void; + lines: any[]; + disabled: boolean; +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No lines found. + + {lines?.map((line: any) => ( + { + onChange(line.value); + setOpen(false); // Close popover after selection + }} + > + {line.label} + + + ))} + + + + + + ); +}); + +const SubLineSelector = React.memo(({ + value, + onChange, + sublines, + disabled +}: { + value?: string; + onChange: (value: string) => void; + sublines: any[]; + disabled: boolean; +}) => { + const [open, setOpen] = useState(false); + const handleCommandListWheel = (e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }; + + return ( + + + + + + + + + No sub lines found. + + {sublines?.map((subline: any) => ( + { + onChange(subline.value); + setOpen(false); // Close popover after selection + }} + > + {subline.label} + + + ))} + + + + + + ); +}); + +// Add this new component before the MatchColumnsStep component +const FieldSelector = React.memo(({ + column, + isUnmapped = false, + fieldCategories, + allFields, + onChange, + isFieldMappedToOtherColumn, + handleCommandListWheel +}: { + column: any; + isUnmapped?: boolean; + fieldCategories: any[]; + allFields: any[]; + onChange: (value: string) => void; + isFieldMappedToOtherColumn: (fieldKey: string, currentColumnIndex: number) => { isMapped: boolean, columnHeader?: string }; + handleCommandListWheel: (e: React.WheelEvent) => void; +}) => { + const [open, setOpen] = useState(false); + + // For ignored columns, show a badge + if (column.type === ColumnType.ignored) { + return Ignored; + } + + // Get the current value if this is a mapped column + const currentValue = "value" in column ? column.value as string : undefined; + + return ( + + + + + + + + + No fields found. + {fieldCategories.map(category => ( + + {category.fields.map((field: { key: string; label: string }) => { + const { isMapped, columnHeader } = isFieldMappedToOtherColumn(field.key as string, column.index); + return ( + { + onChange(value); + setOpen(false); // Close the popover after selection + }} + className={isMapped ? "opacity-70" : ""} + > +
+ {field.label} + {isMapped ? ( + + (mapped to "{columnHeader}") + + ) : null} +
+ {currentValue === field.key && ( + + )} +
+ ); + })} +
+ ))} +
+
+
+
+ ); +}); + export const MatchColumnsStep = React.memo(({ data, headerValues, @@ -278,7 +630,7 @@ export const MatchColumnsStep = React.memo(({ ([...headerValues] as string[]).map((value, index) => ({ type: ColumnType.empty, index, header: value ?? "" })), ) const [globalSelections, setGlobalSelections] = useState(initialGlobalSelections || {}) - const [showAllColumns, setShowAllColumns] = useState(false) + const [showAllColumns, setShowAllColumns] = useState(true) const [expandedValueMappings, setExpandedValueMappings] = useState([]) // Use debounce for expensive operations @@ -601,18 +953,14 @@ export const MatchColumnsStep = React.memo(({ return mappings; }, [columns, fields, isFieldCoveredByGlobalSelections]); - // Available fields for mapping (excluding already mapped fields) + // Available fields for mapping (including already mapped fields) const availableFields = useMemo(() => { const fieldsArray = Array.isArray(fields) ? fields : [fields]; - const mappedFieldKeys = matchedColumns - .filter(col => "value" in col) - .map(col => (col as any).value); - + // Don't filter out mapped fields, only filter global selections return fieldsArray.filter(field => - !mappedFieldKeys.includes(field.key) && !isFieldCoveredByGlobalSelections(field.key) ); - }, [fields, matchedColumns, isFieldCoveredByGlobalSelections]); + }, [fields, isFieldCoveredByGlobalSelections]); // All available fields including already mapped ones (for editing mapped columns) const allFields = useMemo(() => { @@ -963,48 +1311,48 @@ export const MatchColumnsStep = React.memo(({ return handlers; }, [columns, onChange]); - // Render the field selector for a column + // Add a function to check if a field is already mapped to another column + const isFieldMappedToOtherColumn = useCallback((fieldKey: string, currentColumnIndex: number) => { + const matchedColumnForField = columns.find(col => + col.type !== ColumnType.empty && + col.type !== ColumnType.ignored && + "value" in col && + col.value === fieldKey && + col.index !== currentColumnIndex + ); + return matchedColumnForField ? { isMapped: true, columnHeader: matchedColumnForField.header } : { isMapped: false }; + }, [columns]); + + // Add a wheel handler function for command lists + const handleCommandListWheel = useCallback((e: React.WheelEvent) => { + e.currentTarget.scrollTop += e.deltaY; + e.stopPropagation(); + }, []); + + // Replace the renderFieldSelector function with a more stable version const renderFieldSelector = useCallback((column: Column, isUnmapped: boolean = false) => { // For ignored columns, show a badge if (column.type === ColumnType.ignored) { return Ignored; } - // Get the current value if this is a mapped column - const currentValue = "value" in column ? column.value as string : undefined; - - // Use all fields for mapped columns, and only available fields for unmapped columns - const fieldCategoriesForSelector = isUnmapped ? availableFieldCategories : allFieldCategories; - // Get the pre-created onChange handler for this column const handleChange = columnChangeHandlers.get(column.index); return ( - + { + if (handleChange) handleChange(value); + }} + isFieldMappedToOtherColumn={isFieldMappedToOtherColumn} + handleCommandListWheel={handleCommandListWheel} + /> ); - }, [availableFieldCategories, allFieldCategories, columnChangeHandlers]); + }, [availableFieldCategories, allFields, columnChangeHandlers, isFieldMappedToOtherColumn, handleCommandListWheel]); // Replace the renderValueMappings function with a memoized version const renderValueMappings = useCallback((column: Column) => { @@ -1023,8 +1371,8 @@ export const MatchColumnsStep = React.memo(({ - Spreadsheet Column - Data + Imported Spreadsheet Column + Sample Data Map To Field Ignore @@ -1216,97 +1564,52 @@ export const MatchColumnsStep = React.memo(({
- + onChange={(value) => setGlobalSelections(prev => ({ ...prev, supplier: value }))} + suppliers={fieldOptions?.suppliers || []} + />
- + onChange={(value) => setGlobalSelections(prev => ({ + ...prev, + company: value, + line: undefined, + subline: undefined + }))} + companies={fieldOptions?.companies || []} + />
- + />
- + />
@@ -1378,8 +1681,8 @@ export const MatchColumnsStep = React.memo(({ size="sm" onClick={() => setShowAllColumns(!showAllColumns)} > - {showAllColumns ? : } - {showAllColumns ? "Hide mapped" : "Show all"} + {!showAllColumns ? : } + {!showAllColumns ? "Show all" : "Hide mapped"} diff --git a/inventory/src/lib/react-spreadsheet-import/src/translationsRSIProps.ts b/inventory/src/lib/react-spreadsheet-import/src/translationsRSIProps.ts index 284a3fc..7c97c6c 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/translationsRSIProps.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/translationsRSIProps.ts @@ -48,7 +48,7 @@ export const translations = { filterSwitchTitle: "Show only rows with errors", }, imageUploadStep: { - title: "Add Product Images", + title: "Add Images", nextButtonTitle: "Submit", backButtonTitle: "Back", },