Clean up linter errors
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,138 +192,138 @@ 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
|
||||||
<CommandItem
|
.filter(option =>
|
||||||
key={option.value}
|
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
value={option.value}
|
)
|
||||||
onSelect={(currentValue) => {
|
.map((option) => (
|
||||||
onChange(currentValue)
|
<CommandItem
|
||||||
if (field.onChange) {
|
key={option.value}
|
||||||
field.onChange(currentValue)
|
value={option.value}
|
||||||
}
|
onSelect={(currentValue) => {
|
||||||
setIsEditing(false)
|
onChange(currentValue)
|
||||||
}}
|
if (field.onChange) {
|
||||||
>
|
field.onChange(currentValue)
|
||||||
{option.label}
|
}
|
||||||
<Check
|
setSearchQuery("")
|
||||||
className={cn(
|
setIsEditing(false)
|
||||||
"ml-auto h-4 w-4",
|
}}
|
||||||
value === option.value ? "opacity-100" : "opacity-0"
|
>
|
||||||
)}
|
{option.label}
|
||||||
/>
|
<Check
|
||||||
</CommandItem>
|
className={cn(
|
||||||
))}
|
"ml-auto h-4 w-4",
|
||||||
|
value === option.value ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</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>
|
>
|
||||||
<Button
|
<PopoverTrigger asChild>
|
||||||
variant="outline"
|
<Button
|
||||||
role="combobox"
|
variant="outline"
|
||||||
aria-expanded={isEditing}
|
role="combobox"
|
||||||
className={cn(
|
aria-expanded={isEditing}
|
||||||
"w-full justify-between",
|
className={cn(
|
||||||
currentError ? "border-destructive text-destructive" : "border-input"
|
"w-full justify-between",
|
||||||
)}
|
currentError ? "border-destructive" : "border-input"
|
||||||
>
|
)}
|
||||||
{selectedValues.length > 0
|
>
|
||||||
? `${selectedValues.length} selected`
|
{selectedValues.length > 0
|
||||||
: "Select multiple..."}
|
? `${selectedValues.length} selected`
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
: "Select multiple..."}
|
||||||
</Button>
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</PopoverTrigger>
|
</Button>
|
||||||
<PopoverContent className="w-full p-0" align="start">
|
</PopoverTrigger>
|
||||||
<Command>
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start" side="bottom">
|
||||||
<CommandInput placeholder="Search options..." className="h-9" />
|
<Command shouldFilter={false}>
|
||||||
<CommandList>
|
<CommandInput
|
||||||
<CommandEmpty>No options found.</CommandEmpty>
|
placeholder="Search options..."
|
||||||
<CommandGroup>
|
className="h-9"
|
||||||
{field.fieldType.options.map((option) => (
|
value={searchQuery}
|
||||||
<CommandItem
|
onValueChange={setSearchQuery}
|
||||||
key={option.value}
|
/>
|
||||||
value={option.value}
|
<CommandList className="max-h-[200px] overflow-y-auto">
|
||||||
onSelect={(currentValue) => {
|
<CommandEmpty>No options found.</CommandEmpty>
|
||||||
const valueIndex = selectedValues.indexOf(currentValue)
|
<CommandGroup>
|
||||||
let newValues
|
{field.fieldType.options
|
||||||
if (valueIndex === -1) {
|
.filter(option =>
|
||||||
newValues = [...selectedValues, currentValue]
|
option.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
} else {
|
)
|
||||||
newValues = selectedValues.filter((_, i) => i !== valueIndex)
|
.map((option) => (
|
||||||
}
|
<CommandItem
|
||||||
onChange(newValues)
|
key={option.value}
|
||||||
// Don't close on selection for multi-select
|
value={option.value}
|
||||||
}}
|
onSelect={(currentValue) => {
|
||||||
>
|
const valueIndex = selectedValues.indexOf(currentValue)
|
||||||
<div className="flex items-center gap-2">
|
let newValues
|
||||||
<Checkbox
|
if (valueIndex === -1) {
|
||||||
checked={selectedValues.includes(option.value)}
|
newValues = [...selectedValues, currentValue]
|
||||||
className="pointer-events-none"
|
} else {
|
||||||
/>
|
newValues = selectedValues.filter((_, i) => i !== valueIndex)
|
||||||
{option.label}
|
}
|
||||||
</div>
|
onChange(newValues)
|
||||||
</CommandItem>
|
}}
|
||||||
))}
|
>
|
||||||
</CommandGroup>
|
<div className="flex items-center gap-2">
|
||||||
</CommandList>
|
<Checkbox
|
||||||
</Command>
|
checked={selectedValues.includes(option.value)}
|
||||||
</PopoverContent>
|
className="pointer-events-none"
|
||||||
</Popover>
|
/>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user