From 468f85c45d9b4d1a9c4b5a2de5058941dd37b425 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 19 Feb 2025 22:05:21 -0500 Subject: [PATCH] Clean up linter errors --- .../MatchColumnsStep/MatchColumnsStep.tsx | 24 +- .../components/TemplateColumn.tsx | 28 +- .../MatchColumnsStep/utils/setSubColumn.ts | 16 +- .../steps/ValidationStep/ValidationStep.tsx | 319 ++++++++++-------- inventory/src/pages/import/Import.tsx | 4 +- 5 files changed, 223 insertions(+), 168 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 b8c515e..f1b938e 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 @@ -7,7 +7,7 @@ import { setColumn } from "./utils/setColumn" import { setIgnoreColumn } from "./utils/setIgnoreColumn" import { setSubColumn } from "./utils/setSubColumn" import { normalizeTableData } from "./utils/normalizeTableData" -import type { Field, RawData } from "../../types" +import type { Field, RawData, Fields } from "../../types" import { getMatchedColumns } from "./utils/getMatchedColumns" import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields" import { toast } from "sonner" @@ -23,6 +23,7 @@ import { AlertDialogPortal, AlertDialogOverlay, } from "@/components/ui/alert-dialog" +import { DeepReadonly as TsDeepReadonly } from "ts-essentials" export type MatchColumnsProps = { data: RawData[] @@ -91,6 +92,8 @@ export type Column = export type Columns = Column[] +type ReadonlyField = TsDeepReadonly>; + export const MatchColumnsStep = ({ data, headerValues, @@ -108,7 +111,7 @@ export const MatchColumnsStep = ({ const onChange = useCallback( (value: T, columnIndex: number) => { - const field = fields.find((field: Field) => field.key === value) + const field = (fields as Fields).find((f) => f.key === value) if (!field) return const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key) @@ -117,7 +120,7 @@ export const MatchColumnsStep = ({ columns.map>((column, index) => { if (columnIndex === index) { // Set the new column value - return setColumn(column, field, data, autoMapSelectValues) + return setColumn(column, field as Field, data, autoMapSelectValues) } else if (index === existingFieldIndex) { // Clear the old column that had this field toast.warning(translations.matchColumnsStep.duplicateColumnWarningTitle, { @@ -159,13 +162,24 @@ export const MatchColumnsStep = ({ (value: string, columnIndex: number, entry: string) => { setColumns( columns.map((column, index) => - columnIndex === index && "matchedOptions" in column ? setSubColumn(column, entry, value) : column, + columnIndex === index && "matchedOptions" in column + ? setSubColumn(column as MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn, entry, value) + : column, ), ) }, [columns, setColumns], ) - const unmatchedRequiredFields = useMemo(() => findUnmatchedRequiredFields(fields, columns), [fields, columns]) + const unmatchedRequiredFields = useMemo(() => { + // Convert the fields to the expected type + const fieldsArray = Array.isArray(fields) ? fields : [fields] + const typedFields = fieldsArray.map(field => ({ + ...field, + key: field.key as TsDeepReadonly + })) as unknown as Fields + + return findUnmatchedRequiredFields(typedFields, columns) + }, [fields, columns]) const handleOnContinue = useCallback(async () => { if (unmatchedRequiredFields.length > 0) { diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/TemplateColumn.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/TemplateColumn.tsx index a9de2b2..002ddf0 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -1,7 +1,7 @@ import { useRsi } from "../../../hooks/useRsi" import type { Column } from "../MatchColumnsStep" import { ColumnType } from "../MatchColumnsStep" -import type { Fields, Field } from "../../../types" +import type { Fields } from "../../../types" import { Card, CardContent, @@ -27,10 +27,12 @@ type TemplateColumnProps = { onChange: (value: T, columnIndex: number) => void onSubChange: (value: string, columnIndex: number, entry: string) => void } + const getAccordionTitle = (fields: Fields, column: Column, translations: any) => { - const fieldLabel = fields.find((field: Field) => "value" in column && field.key === column.value)!.label - return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${ - "matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length + const field = fields.find((f) => "value" in column && f.key === column.value) + if (!field) return "" + return `${translations.matchColumnsStep.matchDropdownTitle} ${field.label} (${ + "matchedOptions" in column ? column.matchedOptions.filter((option) => !option.value).length : 0 } ${translations.matchColumnsStep.unmatched})` } @@ -42,9 +44,9 @@ export const TemplateColumn = ({ column, onChange, onSubChange column.type === ColumnType.matchedCheckbox || column.type === ColumnType.matchedSelectOptions const isSelect = "matchedOptions" in column - const selectOptions = fields.map(({ label, key }: { label: string; key: string }) => ({ value: key, label })) + const selectOptions = fields.map(({ label, key }) => ({ value: key, label })) const selectValue = column.type === ColumnType.empty ? undefined : - selectOptions.find(({ value }: { value: string }) => "value" in column && column.value === value)?.value + selectOptions.find(({ value }) => "value" in column && column.value === value)?.value if (isIgnored) { return null @@ -67,7 +69,7 @@ export const TemplateColumn = ({ column, onChange, onSubChange align="start" className="z-[1500]" > - {selectOptions.map((option: { value: string; label: string }) => ( + {selectOptions.map((option) => ( {option.label} @@ -86,7 +88,7 @@ export const TemplateColumn = ({ column, onChange, onSubChange - {getAccordionTitle(fields, column, translations)} + {getAccordionTitle(fields, column, translations)}
@@ -107,13 +109,15 @@ export const TemplateColumn = ({ column, onChange, onSubChange align="start" className="z-[1000]" > - {fields - .find((field: Field) => "value" in column && field.key === column.value) - ?.fieldType.options.map((option: { value: string; label: string }) => ( + {(() => { + const field = fields.find((f) => "value" in column && f.key === column.value) + if (!field || !("fieldType" in field) || !("options" in field.fieldType)) return null + return field.fieldType.options.map((option) => ( {option.label} - ))} + )) + })()}
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setSubColumn.ts b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setSubColumn.ts index fcde42c..2186524 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setSubColumn.ts +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/MatchColumnsStep/utils/setSubColumn.ts @@ -1,12 +1,18 @@ -import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn } from "../MatchColumnsStep" +import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn, MatchedMultiSelectColumn } from "../MatchColumnsStep" + export const setSubColumn = ( - oldColumn: MatchedSelectColumn | MatchedSelectOptionsColumn, + oldColumn: MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn, entry: string, value: string, -): MatchedSelectColumn | MatchedSelectOptionsColumn => { +): MatchedSelectColumn | MatchedSelectOptionsColumn | MatchedMultiSelectColumn => { const options = oldColumn.matchedOptions.map((option) => (option.entry === entry ? { ...option, value } : option)) - const allMathced = options.every(({ value }) => !!value) - if (allMathced) { + const allMatched = options.every(({ value }) => !!value) + + if (oldColumn.type === ColumnType.matchedMultiSelect) { + return { ...oldColumn, matchedOptions: options as MatchedOptions[] } + } + + if (allMatched) { return { ...oldColumn, matchedOptions: options as MatchedOptions[], type: ColumnType.matchedSelectOptions } } else { return { ...oldColumn, matchedOptions: options as MatchedOptions[], type: ColumnType.matchedSelect } diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx index 5d3dc2a..ca4a6ea 100644 --- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx +++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStep/ValidationStep.tsx @@ -3,7 +3,7 @@ import { useRsi } from "../../hooks/useRsi" import type { Meta } from "./types" import { addErrorsAndRunHooks } from "./utils/dataMutations" import type { Data, Field, SelectOption, MultiInput } from "../../types" -import { Check, ChevronsUpDown, ArrowDown } from "lucide-react" +import { Check, ChevronsUpDown, ArrowDown, AlertCircle } from "lucide-react" import { cn } from "@/lib/utils" import { Command, @@ -50,6 +50,12 @@ import { AlertDialogPortal, AlertDialogOverlay, } from "@/components/ui/alert-dialog" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" type Props = { initialData: (Data & Meta)[] @@ -68,6 +74,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { const [isEditing, setIsEditing] = useState(false) const [inputValue, setInputValue] = useState(value ?? "") const [validationError, setValidationError] = useState<{level: string, message: string} | undefined>(error) + const [searchQuery, setSearchQuery] = useState("") const validateRegex = (val: string) => { const regexValidation = field.validations?.find(v => v.rule === "regex") @@ -100,22 +107,19 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { return value } - const isRequired = field.validations?.some(v => v.rule === "required") // Determine the current validation state const getValidationState = () => { // Never show validation during editing if (isEditing) return undefined - // Only show validation errors if there's a value - if (value) { + // Only show validation errors if there's a value and we're not editing + if (value && !isEditing) { if (error) return error if (validationError) return validationError - } else if (isRequired && !isEditing) { - // Only show required validation when not editing and empty - return { level: "error", message: "Required" } } + // Never show required validation for empty cells return undefined } @@ -145,21 +149,32 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { // Handle blur for all input types const handleBlur = () => { validateAndCommit(inputValue) - setIsEditing(false) } // Show editing UI only when actually editing const shouldShowEditUI = isEditing + const ValidationIcon = ({ error }: { error: { level: string, message: string } }) => ( + + + +
+ +
+
+ +

{error.message}

+
+
+
+ ) + if (shouldShowEditUI) { switch (field.fieldType.type) { case "select": return ( -
- { - if (!open) handleBlur() - setIsEditing(open) - }}> +
+ - - - - + + + + No options found. - {field.fieldType.options.map((option) => ( - { - onChange(currentValue) - if (field.onChange) { - field.onChange(currentValue) - } - setIsEditing(false) - }} - > - {option.label} - - - ))} + {field.fieldType.options + .filter(option => + option.label.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map((option) => ( + { + onChange(currentValue) + if (field.onChange) { + field.onChange(currentValue) + } + setSearchQuery("") + setIsEditing(false) + }} + > + {option.label} + + + ))} - {currentError && ( -

{currentError.message}

- )} + {currentError && }
) case "multi-select": const selectedValues = Array.isArray(value) ? value : value ? [value] : [] return ( - { - if (!open) handleBlur() - setIsEditing(open) - }}> - - - - - - - - No options found. - - {field.fieldType.options.map((option) => ( - { - const valueIndex = selectedValues.indexOf(currentValue) - let newValues - if (valueIndex === -1) { - newValues = [...selectedValues, currentValue] - } else { - newValues = selectedValues.filter((_, i) => i !== valueIndex) - } - onChange(newValues) - // Don't close on selection for multi-select - }} - > -
- - {option.label} -
-
- ))} -
-
-
-
-
+
+ + + + + + + + + No options found. + + {field.fieldType.options + .filter(option => + option.label.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map((option) => ( + { + const valueIndex = selectedValues.indexOf(currentValue) + let newValues + if (valueIndex === -1) { + newValues = [...selectedValues, currentValue] + } else { + newValues = selectedValues.filter((_, i) => i !== valueIndex) + } + onChange(newValues) + }} + > +
+ + {option.label} +
+
+ ))} +
+
+
+
+
+ {currentError && } +
) case "checkbox": return ( -
+
{ onChange(checked) }} /> + {currentError && }
) case "multi-input": - return ( - { - setInputValue(e.target.value) - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleBlur() - } - }} - onBlur={handleBlur} - className={cn( - "w-full bg-transparent", - currentError ? "border-destructive text-destructive" : "" - )} - autoFocus={!error} - placeholder={`Enter values separated by ${(field.fieldType as MultiInput).separator || ","}`} - /> - ) default: return ( -
+
{ @@ -317,18 +332,23 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { onKeyDown={(e) => { if (e.key === "Enter") { handleBlur() + setIsEditing(false) } }} - onBlur={handleBlur} + onBlur={() => { + handleBlur() + setIsEditing(false) + }} className={cn( - "w-full bg-transparent", - currentError ? "border-destructive text-destructive" : "" + "w-full", + currentError ? "border-destructive" : "" )} - autoFocus={!error} + autoFocus + placeholder={field.fieldType.type === "multi-input" + ? `Enter values separated by ${(field.fieldType as MultiInput).separator || ","}` + : undefined} /> - {currentError && ( -

{currentError.message}

- )} + {currentError && }
) } @@ -344,7 +364,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { } }} className={cn( - "min-h-[36px] cursor-text p-2 rounded-md border bg-background", + "relative min-h-[36px] cursor-text p-2 rounded-md border bg-background", currentError ? "border-destructive" : "border-input", field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between", field.disabled && "opacity-50 cursor-not-allowed bg-muted" @@ -356,11 +376,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => { {(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && ( )} - {currentError && ( -
- {currentError.message} -
- )} + {currentError && }
) } @@ -440,6 +456,24 @@ const CopyDownDialog = ({ ) } +// Add type utilities at the top level +type DeepReadonlyField = { + readonly label: string + readonly key: T + readonly description?: string + readonly alternateMatches?: readonly string[] + readonly validations?: readonly ({ rule: string } & Record)[] + readonly fieldType: { + readonly type: string + readonly options?: readonly SelectOption[] + readonly booleanMatches?: { readonly [key: string]: boolean } + } + readonly onChange?: (value: string) => void +} + +type ReadonlyField = Readonly>; +type ReadonlyFields = readonly ReadonlyField[]; + export const ValidationStep = ({ initialData, file, onBack }: Props) => { const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi() const { toast } = useToast() @@ -508,7 +542,7 @@ export const ValidationStep = ({ initialData, file, onBack }: setCopyDownField(null) }, [data, updateData, copyDownField]) - const columns = useMemo & Meta>[]>(() => { + const columns = useMemo(() => { const baseColumns: ColumnDef & Meta>[] = [ { id: "select", @@ -530,12 +564,12 @@ export const ValidationStep = ({ initialData, file, onBack }: enableHiding: false, size: 50, }, - ...fields.map((field: Field): ColumnDef & Meta> => ({ + ...(Array.from(fields as ReadonlyFields).map((field): ColumnDef & Meta> => ({ accessorKey: field.key, header: () => (
} data={data} onCopyDown={(key) => copyValueDown(key, field.label)} /> @@ -551,7 +585,7 @@ export const ValidationStep = ({ initialData, file, onBack }: value={value} onChange={(newValue) => updateRows(rowIndex, column.id, newValue)} error={error} - field={field} + field={field as Field} /> ) }, @@ -560,7 +594,7 @@ export const ValidationStep = ({ initialData, file, onBack }: field.fieldType.type === "select" ? 150 : 200 ), - })), + }))) ] return baseColumns }, [fields, updateRows, data, copyValueDown]) @@ -585,7 +619,7 @@ export const ValidationStep = ({ initialData, file, onBack }: } } - const normalizeValue = useCallback((value: any, field: Field) => { + const normalizeValue = useCallback((value: any, field: DeepReadonlyField) => { if (field.fieldType.type === "checkbox") { if (typeof value === "boolean") return value if (typeof value === "string") { @@ -597,7 +631,7 @@ export const ValidationStep = ({ initialData, file, onBack }: } return false } - if (field.fieldType.type === "select") { + if (field.fieldType.type === "select" && field.fieldType.options) { // Ensure the value matches one of the options if (field.fieldType.options.some(opt => opt.value === value)) { return value @@ -616,11 +650,10 @@ export const ValidationStep = ({ initialData, file, onBack }: (acc, value) => { const { __index, __errors, ...values } = value - // Normalize values based on field types const normalizedValues = Object.entries(values).reduce((obj, [key, val]) => { - const field = fields.find((f: Field) => f.key === key) + const field = Array.from(fields as ReadonlyFields).find((f) => f.key === key) if (field) { - obj[key as keyof Data] = normalizeValue(val, field) + obj[key as keyof Data] = normalizeValue(val, field as Field) } else { obj[key as keyof Data] = val as string | boolean | undefined } diff --git a/inventory/src/pages/import/Import.tsx b/inventory/src/pages/import/Import.tsx index 943defb..60706a6 100644 --- a/inventory/src/pages/import/Import.tsx +++ b/inventory/src/pages/import/Import.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src"; -import type { Field, Fields, Validation, ErrorLevel } from "@/lib/react-spreadsheet-import/src/types"; +import type { ErrorLevel } from "@/lib/react-spreadsheet-import/src/types"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; @@ -333,8 +333,6 @@ const BASE_IMPORT_FIELDS = [ }, ] as const; -type ImportField = typeof BASE_IMPORT_FIELDS[number]; -type ImportFieldKey = ImportField["key"]; export function Import() { const [isOpen, setIsOpen] = useState(false);