"use client" import { useState } from "react" import type { FieldValues, FieldPath } from "react-hook-form" import { useFormContext, useController, } from "react-hook-form" import { useQuery, useQueryClient } from "@tanstack/react-query" import { FieldShell } from "../field-shell" import { AsyncSelectField, AsyncMultiSelectField, type AsyncSelectFieldProps, type AsyncMultiSelectFieldProps, } from "../controls/async-select-field" import { Field, FieldLabel, FieldError, FieldDescription } from "@/shared/components/ui/field" import { Button } from "@/shared/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/shared/components/ui/dialog" import { ScrollArea } from "@/shared/components/ui/scroll-area" import { PlusIcon } from "lucide-react" // ── Inline create types ── export type InlineCreateFormProps = { onSuccess: (newItem?: { value: string; label: string }) => void } export type InlineCreateConfig = { createForm: (props: InlineCreateFormProps) => React.ReactNode createLabel?: string } function extractItems(response: unknown): any[] { if (Array.isArray(response)) return response if (response && typeof response === "object") { const obj = response as Record if (obj.data && typeof obj.data === "object" && !Array.isArray(obj.data)) { const nested = obj.data as Record if (Array.isArray(nested.data)) return nested.data } if (Array.isArray(obj.data)) return obj.data } return [] } // ── Props forwarded to the underlying control ── type AsyncSelectControlProps = Omit< AsyncSelectFieldProps, keyof import("../types").BaseFieldControlProps | "options" | "loading" | "onInputValueChange" > type AsyncMultiSelectControlProps = Omit< AsyncMultiSelectFieldProps, keyof import("../types").BaseFieldControlProps | "options" | "loading" | "onInputValueChange" > // ── Shared base props ── type BaseRhfAsyncFieldProps = { label?: string description?: string required?: boolean disabled?: boolean queryKey: string[] staleTime?: number } & Partial type WithLoadOptions = { loadOptions: () => Promise listFn?: never mapOption?: never } type WithCrudClient = { loadOptions?: never listFn: () => Promise mapOption?: (item: TItem) => any } type DataSource = WithLoadOptions | WithCrudClient function useAsyncOptions( queryKey: string[], source: DataSource, staleTime?: number, ) { return useQuery({ queryKey, queryFn: async () => { if ("loadOptions" in source && source.loadOptions) { return source.loadOptions() } if ("listFn" in source && source.listFn) { const response = await source.listFn() const items = extractItems(response) return source.mapOption ? items.map(source.mapOption) : items } return [] }, staleTime: staleTime ?? 5 * 60 * 1000, }) } // ── Single-select wrapper ── type RhfAsyncSelectFieldProps< TValues extends FieldValues, TName extends FieldPath, TItem = unknown, > = { name: TName multiple?: false } & BaseRhfAsyncFieldProps & DataSource & AsyncSelectControlProps export function RhfAsyncSelectField< TValues extends FieldValues, TName extends FieldPath, TItem = unknown, >(props: RhfAsyncSelectFieldProps) { const { name, label, description, required, disabled, queryKey, staleTime, loadOptions, listFn, mapOption, createForm, createLabel, ...controlProps } = props const source = { loadOptions, listFn, mapOption } as DataSource const { control } = useFormContext() const { field, fieldState: { error } } = useController({ name, control, disabled }) const { data: options = [], isLoading } = useAsyncOptions(queryKey, source, staleTime) const [inputValue, setInputValue] = useState("") const [isCreateOpen, setIsCreateOpen] = useState(false) const queryClient = useQueryClient() const getLabel = controlProps.getOptionLabel ?? ((o: any) => o.label) const filtered = inputValue ? options.filter((o) => String(getLabel(o)).toLowerCase().includes(inputValue.toLowerCase())) : options const handleCreateSuccess = (newItem?: { value: string; label: string }) => { queryClient.invalidateQueries({ queryKey }) if (newItem) { field.onChange(newItem) } setIsCreateOpen(false) } // When a createForm is provided, render a custom label row with the "+" button if (createForm) { return ( {label && (
{label} {required && *}
)} {description && {description}} {error && {error.message}} { if (!v) setIsCreateOpen(false) }}> Add {createLabel ?? label} {createForm({ onSuccess: handleCreateSuccess })}
) } return ( ) } // ── Multi-select wrapper ── type RhfAsyncMultiSelectFieldProps< TValues extends FieldValues, TName extends FieldPath, TItem = unknown, > = { name: TName multiple: true } & BaseRhfAsyncFieldProps & DataSource & AsyncMultiSelectControlProps export function RhfAsyncMultiSelectField< TValues extends FieldValues, TName extends FieldPath, TItem = unknown, >(props: RhfAsyncMultiSelectFieldProps) { const { name, label, description, required, disabled, queryKey, staleTime, loadOptions, listFn, mapOption, ...controlProps } = props const source = { loadOptions, listFn, mapOption } as DataSource const { control } = useFormContext() const { field, fieldState: { error } } = useController({ name, control, disabled }) const { data: options = [], isLoading } = useAsyncOptions(queryKey, source, staleTime) const [inputValue, setInputValue] = useState("") const getLabel = controlProps.getOptionLabel ?? ((o: any) => o.label) const filtered = inputValue ? options.filter((o) => String(getLabel(o)).toLowerCase().includes(inputValue.toLowerCase())) : options return ( ) }