"use client"; import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { CircleX, Plus, RefreshCwIcon, SearchIcon, XIcon } from "lucide-react"; import { Command, CommandItem, CommandList } from "../../shadcnui/ui/command"; import { Input } from "../../shadcnui/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "../../shadcnui/ui/popover"; import { FormFieldWrapper } from "./FormFieldWrapper"; import { type DataListRetriever, useDataListRetriever } from "../../hooks/useDataListRetriever"; import { useDebounce } from "../../hooks/useDebounce"; type BaseEntity = { id: string }; type EntitySelectorProps = { id: string; form: any; module: any; retriever: (params: any) => Promise; retrieverParams?: Record; getLabel?: (entity: T) => string; label?: string; placeholder?: string; emptyText?: string; isRequired?: boolean; disabled?: boolean; description?: string; ready?: boolean; onChange?: (entity?: T) => void; onCreateNew?: (name: string) => Promise; createNewLabel?: (searchTerm: string) => string; toFormValue?: (entity: T) => Record; getSelectedItemDisplay?: (value: Record) => ReactNode; renderOption?: (entity: T, onSelect: () => void) => ReactNode; filterData?: (data: T[]) => T[]; renderAfter?: (props: { form: any; selectedEntity?: T }) => ReactNode; autoSelectSingle?: boolean; useFormWatch?: boolean; }; export default function EntitySelector({ id, form, module, retriever, retrieverParams, ready = true, getLabel = (entity: T) => (entity as any).name, label, placeholder = "Search...", emptyText, isRequired = false, disabled = false, description, onChange, onCreateNew, createNewLabel, toFormValue = (entity: T) => ({ id: entity.id, name: (entity as any).name }), getSelectedItemDisplay, renderOption, filterData, renderAfter, autoSelectSingle = false, useFormWatch: shouldUseFormWatch = false, }: EntitySelectorProps) { const [open, setOpen] = useState(false); const searchTermRef = useRef(""); const [searchTerm, setSearchTerm] = useState(""); const [isSearching, setIsSearching] = useState(false); const [isCreating, setIsCreating] = useState(false); const [selectedEntity, setSelectedEntity] = useState(undefined); const watchedValue = shouldUseFormWatch ? form.watch(id) : undefined; const data: DataListRetriever = useDataListRetriever({ retriever: (params) => retriever(params), retrieverParams: retrieverParams ?? {}, ready, module, }); useEffect(() => { if (ready) data.setReady(true); }, [ready]); const search = useCallback( async (searchedTerm: string) => { try { if (searchedTerm === searchTermRef.current) return; setIsSearching(true); searchTermRef.current = searchedTerm; await data.search(searchedTerm); } finally { setIsSearching(false); } }, [searchTermRef, data], ); const updateSearchTerm = useDebounce(search, 500); useEffect(() => { setIsSearching(true); updateSearchTerm(searchTerm); }, [updateSearchTerm, searchTerm]); useEffect(() => { if ( autoSelectSingle && data.isLoaded && data.data && data.data.length === 1 && searchTerm === "" && !form.getValues(id) ) { const entity = data.data[0]; form.setValue(id, toFormValue(entity)); setSelectedEntity(entity); } }, [data.isLoaded, data.data, searchTerm, form, id, autoSelectSingle, toFormValue]); const handleSelect = (entity?: T) => { setSelectedEntity(entity); if (onChange) onChange(entity); if (!entity) { form.setValue(id, undefined); setOpen(false); return; } form.setValue(id, toFormValue(entity)); setOpen(false); setTimeout(() => setOpen(false), 0); }; const handleCreateNew = async () => { if (!onCreateNew) return; const trimmed = searchTerm.trim(); if (!trimmed || isCreating) return; setIsCreating(true); try { const created = await onCreateNew(trimmed); if (created) { handleSelect(created); setSearchTerm(""); searchTermRef.current = ""; } } finally { setIsCreating(false); } }; const displayData = filterData ? filterData((data.data as T[]) ?? []) : ((data.data as T[]) ?? []); const hasValue = (value: any) => { if (value == null) return false; if (typeof value === "object") return !!value.id; return !!value; }; return (
{(field) => { const effectiveValue = shouldUseFormWatch ? watchedValue : field.value; return (
{hasValue(effectiveValue) ? getSelectedItemDisplay ? getSelectedItemDisplay(effectiveValue) : (effectiveValue?.name ?? "") : placeholder} {hasValue(effectiveValue) && !disabled && ( { e.stopPropagation(); e.preventDefault(); handleSelect(); }} /> )}
setSearchTerm(e.target.value)} value={searchTerm} /> {isSearching ? ( ) : searchTermRef.current ? ( { setSearchTerm(""); search(""); }} /> ) : null}
{onCreateNew && ( {isCreating ? ( ) : ( )} {createNewLabel ? createNewLabel(searchTerm) : searchTerm.trim() || ""} )} {renderOption ? displayData.map((entity: T) => renderOption(entity, () => handleSelect(entity))) : displayData.map((entity: T) => ( handleSelect(entity)} > {getLabel(entity)} ))} {emptyText && displayData.length === 0 && !onCreateNew && (
{emptyText}
)}
); }}
{renderAfter && renderAfter({ form, selectedEntity })}
); }