Clean up linter errors

This commit is contained in:
2025-02-19 22:05:21 -05:00
parent 24e2d01ccc
commit 468f85c45d
5 changed files with 223 additions and 168 deletions

View File

@@ -7,7 +7,7 @@ import { setColumn } from "./utils/setColumn"
import { setIgnoreColumn } from "./utils/setIgnoreColumn" import { setIgnoreColumn } from "./utils/setIgnoreColumn"
import { setSubColumn } from "./utils/setSubColumn" import { setSubColumn } from "./utils/setSubColumn"
import { normalizeTableData } from "./utils/normalizeTableData" import { normalizeTableData } from "./utils/normalizeTableData"
import type { Field, RawData } from "../../types" import type { Field, RawData, Fields } from "../../types"
import { getMatchedColumns } from "./utils/getMatchedColumns" import { getMatchedColumns } from "./utils/getMatchedColumns"
import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields" import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields"
import { toast } from "sonner" import { toast } from "sonner"
@@ -23,6 +23,7 @@ import {
AlertDialogPortal, AlertDialogPortal,
AlertDialogOverlay, AlertDialogOverlay,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { DeepReadonly as TsDeepReadonly } from "ts-essentials"
export type MatchColumnsProps<T extends string> = { export type MatchColumnsProps<T extends string> = {
data: RawData[] data: RawData[]
@@ -91,6 +92,8 @@ export type Column<T extends string> =
export type Columns<T extends string> = Column<T>[] export type Columns<T extends string> = Column<T>[]
type ReadonlyField<T extends string> = TsDeepReadonly<Field<T>>;
export const MatchColumnsStep = <T extends string>({ export const MatchColumnsStep = <T extends string>({
data, data,
headerValues, headerValues,
@@ -108,7 +111,7 @@ export const MatchColumnsStep = <T extends string>({
const onChange = useCallback( const onChange = useCallback(
(value: T, columnIndex: number) => { (value: T, columnIndex: number) => {
const field = fields.find((field: Field<T>) => field.key === value) const field = (fields as Fields<T>).find((f) => f.key === value)
if (!field) return if (!field) return
const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key) const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key)
@@ -117,7 +120,7 @@ export const MatchColumnsStep = <T extends string>({
columns.map<Column<T>>((column, index) => { columns.map<Column<T>>((column, index) => {
if (columnIndex === index) { if (columnIndex === index) {
// Set the new column value // Set the new column value
return setColumn(column, field, data, autoMapSelectValues) return setColumn(column, field as Field<T>, data, autoMapSelectValues)
} else if (index === existingFieldIndex) { } else if (index === existingFieldIndex) {
// Clear the old column that had this field // Clear the old column that had this field
toast.warning(translations.matchColumnsStep.duplicateColumnWarningTitle, { toast.warning(translations.matchColumnsStep.duplicateColumnWarningTitle, {
@@ -159,13 +162,24 @@ export const MatchColumnsStep = <T extends string>({
(value: string, columnIndex: number, entry: string) => { (value: string, columnIndex: number, entry: string) => {
setColumns( setColumns(
columns.map((column, index) => columns.map((column, index) =>
columnIndex === index && "matchedOptions" in column ? setSubColumn(column, entry, value) : column, columnIndex === index && "matchedOptions" in column
? setSubColumn(column as MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T>, entry, value)
: column,
), ),
) )
}, },
[columns, setColumns], [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<T>
})) as unknown as Fields<T>
return findUnmatchedRequiredFields(typedFields, columns)
}, [fields, columns])
const handleOnContinue = useCallback(async () => { const handleOnContinue = useCallback(async () => {
if (unmatchedRequiredFields.length > 0) { if (unmatchedRequiredFields.length > 0) {

View File

@@ -1,7 +1,7 @@
import { useRsi } from "../../../hooks/useRsi" import { useRsi } from "../../../hooks/useRsi"
import type { Column } from "../MatchColumnsStep" import type { Column } from "../MatchColumnsStep"
import { ColumnType } from "../MatchColumnsStep" import { ColumnType } from "../MatchColumnsStep"
import type { Fields, Field } from "../../../types" import type { Fields } from "../../../types"
import { import {
Card, Card,
CardContent, CardContent,
@@ -27,10 +27,12 @@ type TemplateColumnProps<T extends string> = {
onChange: (value: T, columnIndex: number) => void onChange: (value: T, columnIndex: number) => void
onSubChange: (value: string, columnIndex: number, entry: string) => void onSubChange: (value: string, columnIndex: number, entry: string) => void
} }
const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: any) => { const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: any) => {
const fieldLabel = fields.find((field: Field<T>) => "value" in column && field.key === column.value)!.label const field = fields.find((f) => "value" in column && f.key === column.value)
return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${ if (!field) return ""
"matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length return `${translations.matchColumnsStep.matchDropdownTitle} ${field.label} (${
"matchedOptions" in column ? column.matchedOptions.filter((option) => !option.value).length : 0
} ${translations.matchColumnsStep.unmatched})` } ${translations.matchColumnsStep.unmatched})`
} }
@@ -42,9 +44,9 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
column.type === ColumnType.matchedCheckbox || column.type === ColumnType.matchedCheckbox ||
column.type === ColumnType.matchedSelectOptions column.type === ColumnType.matchedSelectOptions
const isSelect = "matchedOptions" in column 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 : 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) { if (isIgnored) {
return null return null
@@ -67,7 +69,7 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
align="start" align="start"
className="z-[1500]" className="z-[1500]"
> >
{selectOptions.map((option: { value: string; label: string }) => ( {selectOptions.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>
@@ -86,7 +88,7 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
<Accordion type="multiple" className="w-full"> <Accordion type="multiple" className="w-full">
<AccordionItem value="options" className="border-none"> <AccordionItem value="options" className="border-none">
<AccordionTrigger className="py-2 text-sm hover:no-underline"> <AccordionTrigger className="py-2 text-sm hover:no-underline">
{getAccordionTitle<T>(fields, column, translations)} {getAccordionTitle(fields, column, translations)}
</AccordionTrigger> </AccordionTrigger>
<AccordionContent> <AccordionContent>
<div className="space-y-2"> <div className="space-y-2">
@@ -107,13 +109,15 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
align="start" align="start"
className="z-[1000]" className="z-[1000]"
> >
{fields {(() => {
.find((field: Field<T>) => "value" in column && field.key === column.value) const field = fields.find((f) => "value" in column && f.key === column.value)
?.fieldType.options.map((option: { value: string; label: string }) => ( if (!field || !("fieldType" in field) || !("options" in field.fieldType)) return null
return field.fieldType.options.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>
))} ))
})()}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@@ -1,12 +1,18 @@
import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn } from "../MatchColumnsStep" import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn, MatchedMultiSelectColumn } from "../MatchColumnsStep"
export const setSubColumn = <T>( export const setSubColumn = <T>(
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>, oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T>,
entry: string, entry: string,
value: string, value: string,
): MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> => { ): MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T> => {
const options = oldColumn.matchedOptions.map((option) => (option.entry === entry ? { ...option, value } : option)) const options = oldColumn.matchedOptions.map((option) => (option.entry === entry ? { ...option, value } : option))
const allMathced = options.every(({ value }) => !!value) const allMatched = options.every(({ value }) => !!value)
if (allMathced) {
if (oldColumn.type === ColumnType.matchedMultiSelect) {
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[] }
}
if (allMatched) {
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelectOptions } return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelectOptions }
} else { } else {
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelect } return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelect }

View File

@@ -3,7 +3,7 @@ import { useRsi } from "../../hooks/useRsi"
import type { Meta } from "./types" import type { Meta } from "./types"
import { addErrorsAndRunHooks } from "./utils/dataMutations" import { addErrorsAndRunHooks } from "./utils/dataMutations"
import type { Data, Field, SelectOption, MultiInput } from "../../types" 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 { cn } from "@/lib/utils"
import { import {
Command, Command,
@@ -50,6 +50,12 @@ import {
AlertDialogPortal, AlertDialogPortal,
AlertDialogOverlay, AlertDialogOverlay,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
type Props<T extends string> = { type Props<T extends string> = {
initialData: (Data<T> & Meta)[] initialData: (Data<T> & Meta)[]
@@ -68,6 +74,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(value ?? "") const [inputValue, setInputValue] = useState(value ?? "")
const [validationError, setValidationError] = useState<{level: string, message: string} | undefined>(error) const [validationError, setValidationError] = useState<{level: string, message: string} | undefined>(error)
const [searchQuery, setSearchQuery] = useState("")
const validateRegex = (val: string) => { const validateRegex = (val: string) => {
const regexValidation = field.validations?.find(v => v.rule === "regex") const regexValidation = field.validations?.find(v => v.rule === "regex")
@@ -100,22 +107,19 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
return value return value
} }
const isRequired = field.validations?.some(v => v.rule === "required")
// Determine the current validation state // Determine the current validation state
const getValidationState = () => { const getValidationState = () => {
// Never show validation during editing // Never show validation during editing
if (isEditing) return undefined if (isEditing) return undefined
// Only show validation errors if there's a value // Only show validation errors if there's a value and we're not editing
if (value) { if (value && !isEditing) {
if (error) return error if (error) return error
if (validationError) return validationError 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 return undefined
} }
@@ -145,21 +149,32 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
// Handle blur for all input types // Handle blur for all input types
const handleBlur = () => { const handleBlur = () => {
validateAndCommit(inputValue) validateAndCommit(inputValue)
setIsEditing(false)
} }
// Show editing UI only when actually editing // Show editing UI only when actually editing
const shouldShowEditUI = isEditing const shouldShowEditUI = isEditing
const ValidationIcon = ({ error }: { error: { level: string, message: string } }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="absolute right-2 top-2 text-destructive">
<AlertCircle className="h-4 w-4" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{error.message}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
if (shouldShowEditUI) { if (shouldShowEditUI) {
switch (field.fieldType.type) { switch (field.fieldType.type) {
case "select": case "select":
return ( return (
<div className="space-y-1"> <div className="relative">
<Popover open={isEditing} onOpenChange={(open) => { <Popover open={isEditing} onOpenChange={setIsEditing}>
if (!open) handleBlur()
setIsEditing(open)
}}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@@ -167,7 +182,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
aria-expanded={isEditing} aria-expanded={isEditing}
className={cn( className={cn(
"w-full justify-between", "w-full justify-between",
currentError ? "border-destructive text-destructive" : "border-input" currentError ? "border-destructive" : "border-input"
)} )}
disabled={field.disabled} disabled={field.disabled}
> >
@@ -177,13 +192,22 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0" align="start"> <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start" side="bottom">
<Command> <Command shouldFilter={false}>
<CommandInput placeholder="Search options..." className="h-9" /> <CommandInput
<CommandList> placeholder="Search options..."
className="h-9"
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList className="max-h-[200px] overflow-y-auto">
<CommandEmpty>No options found.</CommandEmpty> <CommandEmpty>No options found.</CommandEmpty>
<CommandGroup> <CommandGroup>
{field.fieldType.options.map((option) => ( {field.fieldType.options
.filter(option =>
option.label.toLowerCase().includes(searchQuery.toLowerCase())
)
.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
value={option.value} value={option.value}
@@ -192,6 +216,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
if (field.onChange) { if (field.onChange) {
field.onChange(currentValue) field.onChange(currentValue)
} }
setSearchQuery("")
setIsEditing(false) setIsEditing(false)
}} }}
> >
@@ -209,18 +234,17 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{currentError && ( {currentError && <ValidationIcon error={currentError} />}
<p className="text-xs text-destructive">{currentError.message}</p>
)}
</div> </div>
) )
case "multi-select": case "multi-select":
const selectedValues = Array.isArray(value) ? value : value ? [value] : [] const selectedValues = Array.isArray(value) ? value : value ? [value] : []
return ( return (
<Popover open={isEditing} onOpenChange={(open) => { <div className="relative">
if (!open) handleBlur() <Popover
setIsEditing(open) open={isEditing}
}}> onOpenChange={setIsEditing}
>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@@ -228,7 +252,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
aria-expanded={isEditing} aria-expanded={isEditing}
className={cn( className={cn(
"w-full justify-between", "w-full justify-between",
currentError ? "border-destructive text-destructive" : "border-input" currentError ? "border-destructive" : "border-input"
)} )}
> >
{selectedValues.length > 0 {selectedValues.length > 0
@@ -237,13 +261,22 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0" align="start"> <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start" side="bottom">
<Command> <Command shouldFilter={false}>
<CommandInput placeholder="Search options..." className="h-9" /> <CommandInput
<CommandList> placeholder="Search options..."
className="h-9"
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList className="max-h-[200px] overflow-y-auto">
<CommandEmpty>No options found.</CommandEmpty> <CommandEmpty>No options found.</CommandEmpty>
<CommandGroup> <CommandGroup>
{field.fieldType.options.map((option) => ( {field.fieldType.options
.filter(option =>
option.label.toLowerCase().includes(searchQuery.toLowerCase())
)
.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
value={option.value} value={option.value}
@@ -256,7 +289,6 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
newValues = selectedValues.filter((_, i) => i !== valueIndex) newValues = selectedValues.filter((_, i) => i !== valueIndex)
} }
onChange(newValues) onChange(newValues)
// Don't close on selection for multi-select
}} }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -273,42 +305,25 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{currentError && <ValidationIcon error={currentError} />}
</div>
) )
case "checkbox": case "checkbox":
return ( return (
<div className="flex items-center gap-2"> <div className="relative flex items-center gap-2">
<Checkbox <Checkbox
checked={Boolean(value)} checked={Boolean(value)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
onChange(checked) onChange(checked)
}} }}
/> />
{currentError && <ValidationIcon error={currentError} />}
</div> </div>
) )
case "multi-input": case "multi-input":
return (
<Input
value={inputValue}
onChange={(e) => {
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: default:
return ( return (
<div className="space-y-1"> <div className="relative">
<Input <Input
value={inputValue} value={inputValue}
onChange={(e) => { onChange={(e) => {
@@ -317,18 +332,23 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleBlur() handleBlur()
setIsEditing(false)
} }
}} }}
onBlur={handleBlur} onBlur={() => {
handleBlur()
setIsEditing(false)
}}
className={cn( className={cn(
"w-full bg-transparent", "w-full",
currentError ? "border-destructive text-destructive" : "" currentError ? "border-destructive" : ""
)} )}
autoFocus={!error} autoFocus
placeholder={field.fieldType.type === "multi-input"
? `Enter values separated by ${(field.fieldType as MultiInput).separator || ","}`
: undefined}
/> />
{currentError && ( {currentError && <ValidationIcon error={currentError} />}
<p className="text-xs text-destructive">{currentError.message}</p>
)}
</div> </div>
) )
} }
@@ -344,7 +364,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
} }
}} }}
className={cn( 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", currentError ? "border-destructive" : "border-input",
field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between", field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between",
field.disabled && "opacity-50 cursor-not-allowed bg-muted" 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") && ( {(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && (
<ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} /> <ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} />
)} )}
{currentError && ( {currentError && <ValidationIcon error={currentError} />}
<div className="absolute left-0 -bottom-5 text-xs text-destructive">
{currentError.message}
</div>
)}
</div> </div>
) )
} }
@@ -440,6 +456,24 @@ const CopyDownDialog = ({
) )
} }
// Add type utilities at the top level
type DeepReadonlyField<T extends string> = {
readonly label: string
readonly key: T
readonly description?: string
readonly alternateMatches?: readonly string[]
readonly validations?: readonly ({ rule: string } & Record<string, any>)[]
readonly fieldType: {
readonly type: string
readonly options?: readonly SelectOption[]
readonly booleanMatches?: { readonly [key: string]: boolean }
}
readonly onChange?: (value: string) => void
}
type ReadonlyField<T extends string> = Readonly<Field<T>>;
type ReadonlyFields<T extends string> = readonly ReadonlyField<T>[];
export const ValidationStep = <T extends string>({ initialData, file, onBack }: Props<T>) => { export const ValidationStep = <T extends string>({ initialData, file, onBack }: Props<T>) => {
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>() const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>()
const { toast } = useToast() const { toast } = useToast()
@@ -508,7 +542,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
setCopyDownField(null) setCopyDownField(null)
}, [data, updateData, copyDownField]) }, [data, updateData, copyDownField])
const columns = useMemo<ColumnDef<Data<T> & Meta>[]>(() => { const columns = useMemo(() => {
const baseColumns: ColumnDef<Data<T> & Meta>[] = [ const baseColumns: ColumnDef<Data<T> & Meta>[] = [
{ {
id: "select", id: "select",
@@ -530,12 +564,12 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
enableHiding: false, enableHiding: false,
size: 50, size: 50,
}, },
...fields.map((field: Field<T>): ColumnDef<Data<T> & Meta> => ({ ...(Array.from(fields as ReadonlyFields<T>).map((field): ColumnDef<Data<T> & Meta> => ({
accessorKey: field.key, accessorKey: field.key,
header: () => ( header: () => (
<div className="group"> <div className="group">
<ColumnHeader <ColumnHeader
field={field} field={field as Field<T>}
data={data} data={data}
onCopyDown={(key) => copyValueDown(key, field.label)} onCopyDown={(key) => copyValueDown(key, field.label)}
/> />
@@ -551,7 +585,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
value={value} value={value}
onChange={(newValue) => updateRows(rowIndex, column.id, newValue)} onChange={(newValue) => updateRows(rowIndex, column.id, newValue)}
error={error} error={error}
field={field} field={field as Field<string>}
/> />
) )
}, },
@@ -560,7 +594,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
field.fieldType.type === "select" ? 150 : field.fieldType.type === "select" ? 150 :
200 200
), ),
})), })))
] ]
return baseColumns return baseColumns
}, [fields, updateRows, data, copyValueDown]) }, [fields, updateRows, data, copyValueDown])
@@ -585,7 +619,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
} }
} }
const normalizeValue = useCallback((value: any, field: Field<T>) => { const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => {
if (field.fieldType.type === "checkbox") { if (field.fieldType.type === "checkbox") {
if (typeof value === "boolean") return value if (typeof value === "boolean") return value
if (typeof value === "string") { if (typeof value === "string") {
@@ -597,7 +631,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
} }
return false return false
} }
if (field.fieldType.type === "select") { if (field.fieldType.type === "select" && field.fieldType.options) {
// Ensure the value matches one of the options // Ensure the value matches one of the options
if (field.fieldType.options.some(opt => opt.value === value)) { if (field.fieldType.options.some(opt => opt.value === value)) {
return value return value
@@ -616,11 +650,10 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
(acc, value) => { (acc, value) => {
const { __index, __errors, ...values } = value const { __index, __errors, ...values } = value
// Normalize values based on field types
const normalizedValues = Object.entries(values).reduce((obj, [key, val]) => { const normalizedValues = Object.entries(values).reduce((obj, [key, val]) => {
const field = fields.find((f: Field<T>) => f.key === key) const field = Array.from(fields as ReadonlyFields<T>).find((f) => f.key === key)
if (field) { if (field) {
obj[key as keyof Data<T>] = normalizeValue(val, field) obj[key as keyof Data<T>] = normalizeValue(val, field as Field<T>)
} else { } else {
obj[key as keyof Data<T>] = val as string | boolean | undefined obj[key as keyof Data<T>] = val as string | boolean | undefined
} }

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { ReactSpreadsheetImport } from "@/lib/react-spreadsheet-import/src"; 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 { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Code } from "@/components/ui/code"; import { Code } from "@/components/ui/code";
@@ -333,8 +333,6 @@ const BASE_IMPORT_FIELDS = [
}, },
] as const; ] as const;
type ImportField = typeof BASE_IMPORT_FIELDS[number];
type ImportFieldKey = ImportField["key"];
export function Import() { export function Import() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);