Clean up linter errors
This commit is contained in:
@@ -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<T extends string> = {
|
||||
data: RawData[]
|
||||
@@ -91,6 +92,8 @@ export type Column<T extends string> =
|
||||
|
||||
export type Columns<T extends string> = Column<T>[]
|
||||
|
||||
type ReadonlyField<T extends string> = TsDeepReadonly<Field<T>>;
|
||||
|
||||
export const MatchColumnsStep = <T extends string>({
|
||||
data,
|
||||
headerValues,
|
||||
@@ -108,7 +111,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
|
||||
const onChange = useCallback(
|
||||
(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
|
||||
|
||||
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) => {
|
||||
if (columnIndex === index) {
|
||||
// Set the new column value
|
||||
return setColumn(column, field, data, autoMapSelectValues)
|
||||
return setColumn(column, field as Field<T>, 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 = <T extends string>({
|
||||
(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<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T>, 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<T>
|
||||
})) as unknown as Fields<T>
|
||||
|
||||
return findUnmatchedRequiredFields(typedFields, columns)
|
||||
}, [fields, columns])
|
||||
|
||||
const handleOnContinue = useCallback(async () => {
|
||||
if (unmatchedRequiredFields.length > 0) {
|
||||
|
||||
@@ -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<T extends string> = {
|
||||
onChange: (value: T, columnIndex: number) => void
|
||||
onSubChange: (value: string, columnIndex: number, entry: string) => void
|
||||
}
|
||||
|
||||
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
|
||||
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 = <T extends string>({ 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 = <T extends string>({ column, onChange, onSubChange
|
||||
align="start"
|
||||
className="z-[1500]"
|
||||
>
|
||||
{selectOptions.map((option: { value: string; label: string }) => (
|
||||
{selectOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
@@ -86,7 +88,7 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
|
||||
<Accordion type="multiple" className="w-full">
|
||||
<AccordionItem value="options" className="border-none">
|
||||
<AccordionTrigger className="py-2 text-sm hover:no-underline">
|
||||
{getAccordionTitle<T>(fields, column, translations)}
|
||||
{getAccordionTitle(fields, column, translations)}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2">
|
||||
@@ -107,13 +109,15 @@ export const TemplateColumn = <T extends string>({ column, onChange, onSubChange
|
||||
align="start"
|
||||
className="z-[1000]"
|
||||
>
|
||||
{fields
|
||||
.find((field: Field<T>) => "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) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
))
|
||||
})()}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn } from "../MatchColumnsStep"
|
||||
import { ColumnType, MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn, MatchedMultiSelectColumn } from "../MatchColumnsStep"
|
||||
|
||||
export const setSubColumn = <T>(
|
||||
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>,
|
||||
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T> | MatchedMultiSelectColumn<T>,
|
||||
entry: 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 allMathced = options.every(({ value }) => !!value)
|
||||
if (allMathced) {
|
||||
const allMatched = options.every(({ value }) => !!value)
|
||||
|
||||
if (oldColumn.type === ColumnType.matchedMultiSelect) {
|
||||
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[] }
|
||||
}
|
||||
|
||||
if (allMatched) {
|
||||
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelectOptions }
|
||||
} else {
|
||||
return { ...oldColumn, matchedOptions: options as MatchedOptions<T>[], type: ColumnType.matchedSelect }
|
||||
|
||||
@@ -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<T extends string> = {
|
||||
initialData: (Data<T> & 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 } }) => (
|
||||
<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) {
|
||||
switch (field.fieldType.type) {
|
||||
case "select":
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Popover open={isEditing} onOpenChange={(open) => {
|
||||
if (!open) handleBlur()
|
||||
setIsEditing(open)
|
||||
}}>
|
||||
<div className="relative">
|
||||
<Popover open={isEditing} onOpenChange={setIsEditing}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -167,7 +182,7 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
aria-expanded={isEditing}
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
currentError ? "border-destructive text-destructive" : "border-input"
|
||||
currentError ? "border-destructive" : "border-input"
|
||||
)}
|
||||
disabled={field.disabled}
|
||||
>
|
||||
@@ -177,138 +192,138 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search options..." className="h-9" />
|
||||
<CommandList>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start" side="bottom">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Search options..."
|
||||
className="h-9"
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
/>
|
||||
<CommandList className="max-h-[200px] overflow-y-auto">
|
||||
<CommandEmpty>No options found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{field.fieldType.options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={(currentValue) => {
|
||||
onChange(currentValue)
|
||||
if (field.onChange) {
|
||||
field.onChange(currentValue)
|
||||
}
|
||||
setIsEditing(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === option.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
{field.fieldType.options
|
||||
.filter(option =>
|
||||
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={(currentValue) => {
|
||||
onChange(currentValue)
|
||||
if (field.onChange) {
|
||||
field.onChange(currentValue)
|
||||
}
|
||||
setSearchQuery("")
|
||||
setIsEditing(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === option.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{currentError && (
|
||||
<p className="text-xs text-destructive">{currentError.message}</p>
|
||||
)}
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
case "multi-select":
|
||||
const selectedValues = Array.isArray(value) ? value : value ? [value] : []
|
||||
return (
|
||||
<Popover open={isEditing} onOpenChange={(open) => {
|
||||
if (!open) handleBlur()
|
||||
setIsEditing(open)
|
||||
}}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={isEditing}
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
currentError ? "border-destructive text-destructive" : "border-input"
|
||||
)}
|
||||
>
|
||||
{selectedValues.length > 0
|
||||
? `${selectedValues.length} selected`
|
||||
: "Select multiple..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search options..." className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No options found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{field.fieldType.options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={(currentValue) => {
|
||||
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
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedValues.includes(option.value)}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
{option.label}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="relative">
|
||||
<Popover
|
||||
open={isEditing}
|
||||
onOpenChange={setIsEditing}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={isEditing}
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
currentError ? "border-destructive" : "border-input"
|
||||
)}
|
||||
>
|
||||
{selectedValues.length > 0
|
||||
? `${selectedValues.length} selected`
|
||||
: "Select multiple..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start" side="bottom">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Search options..."
|
||||
className="h-9"
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
/>
|
||||
<CommandList className="max-h-[200px] overflow-y-auto">
|
||||
<CommandEmpty>No options found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{field.fieldType.options
|
||||
.filter(option =>
|
||||
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={(currentValue) => {
|
||||
const valueIndex = selectedValues.indexOf(currentValue)
|
||||
let newValues
|
||||
if (valueIndex === -1) {
|
||||
newValues = [...selectedValues, currentValue]
|
||||
} else {
|
||||
newValues = selectedValues.filter((_, i) => i !== valueIndex)
|
||||
}
|
||||
onChange(newValues)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedValues.includes(option.value)}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
{option.label}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(checked)
|
||||
}}
|
||||
/>
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
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:
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
@@ -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 && (
|
||||
<p className="text-xs text-destructive">{currentError.message}</p>
|
||||
)}
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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") && (
|
||||
<ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} />
|
||||
)}
|
||||
{currentError && (
|
||||
<div className="absolute left-0 -bottom-5 text-xs text-destructive">
|
||||
{currentError.message}
|
||||
</div>
|
||||
)}
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</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>) => {
|
||||
const { translations, fields, onClose, onSubmit, rowHook, tableHook, allowInvalidSubmit } = useRsi<T>()
|
||||
const { toast } = useToast()
|
||||
@@ -508,7 +542,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
setCopyDownField(null)
|
||||
}, [data, updateData, copyDownField])
|
||||
|
||||
const columns = useMemo<ColumnDef<Data<T> & Meta>[]>(() => {
|
||||
const columns = useMemo(() => {
|
||||
const baseColumns: ColumnDef<Data<T> & Meta>[] = [
|
||||
{
|
||||
id: "select",
|
||||
@@ -530,12 +564,12 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
enableHiding: false,
|
||||
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,
|
||||
header: () => (
|
||||
<div className="group">
|
||||
<ColumnHeader
|
||||
field={field}
|
||||
field={field as Field<T>}
|
||||
data={data}
|
||||
onCopyDown={(key) => copyValueDown(key, field.label)}
|
||||
/>
|
||||
@@ -551,7 +585,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
value={value}
|
||||
onChange={(newValue) => updateRows(rowIndex, column.id, newValue)}
|
||||
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 :
|
||||
200
|
||||
),
|
||||
})),
|
||||
})))
|
||||
]
|
||||
return baseColumns
|
||||
}, [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 (typeof value === "boolean") return value
|
||||
if (typeof value === "string") {
|
||||
@@ -597,7 +631,7 @@ export const ValidationStep = <T extends string>({ 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 = <T extends string>({ 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<T>) => f.key === key)
|
||||
const field = Array.from(fields as ReadonlyFields<T>).find((f) => f.key === key)
|
||||
if (field) {
|
||||
obj[key as keyof Data<T>] = normalizeValue(val, field)
|
||||
obj[key as keyof Data<T>] = normalizeValue(val, field as Field<T>)
|
||||
} else {
|
||||
obj[key as keyof Data<T>] = val as string | boolean | undefined
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user