/** * Selector Template * * Generates {Module}Selector.tsx searchable combobox component. * * UI mirrors the framework's UserSelector (packages/.../features/user/components/forms/UserSelector.tsx): * an input-styled trigger, a CircleX clear affordance, an `--anchor-width` popover, and a custom * search Input with a SearchIcon + spinner/clear — minus the user-specific avatar. */ import { FrontendTemplateData } from "../../types/template-data.interface"; /** * Generate the selector component file content * * @param data - Frontend template data * @returns Generated file content */ export function generateSelectorTemplate(data: FrontendTemplateData): string { const { names, fields, extendsContent } = data; const hasNameField = extendsContent || fields.some((f) => f.name === "name"); const i18nKey = names.pluralCamel.toLowerCase(); const displayProp = hasNameField ? "name" : "id"; return `"use client"; import { Command, CommandItem, CommandList, FormFieldWrapper, Input, Popover, PopoverContent, PopoverTrigger, } from "@carlonicora/nextjs-jsonapi/components"; import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface"; import { ${names.pascalCase}Service } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Service"; import { DataListRetriever, useDataListRetriever, useDebounce } from "@carlonicora/nextjs-jsonapi/client"; import { Modules } from "@carlonicora/nextjs-jsonapi/core"; import { CircleX, RefreshCwIcon, SearchIcon, XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useRef, useState } from "react"; type ${names.pascalCase}SelectorProps = { id: string; form: any; label?: string; placeholder?: string; onChange?: (${names.camelCase}?: ${names.pascalCase}Interface) => void; isRequired?: boolean; }; export default function ${names.pascalCase}Selector({ id, form, label, placeholder, onChange, isRequired = false, }: ${names.pascalCase}SelectorProps) { const t = useTranslations(); const [open, setOpen] = useState(false); const searchTermRef = useRef(""); const [searchTerm, setSearchTerm] = useState(""); const [isSearching, setIsSearching] = useState(false); const data: DataListRetriever<${names.pascalCase}Interface> = useDataListRetriever({ retriever: (params) => { return ${names.pascalCase}Service.findMany(params); }, retrieverParams: {}, module: Modules.${names.pascalCase}, }); 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]); const set${names.pascalCase} = (${names.camelCase}?: ${names.pascalCase}Interface) => { if (onChange) onChange(${names.camelCase}); if (!${names.camelCase}) { form.setValue(id, undefined); setOpen(false); return; } form.setValue(id, { id: ${names.camelCase}.id, name: ${names.camelCase}.${displayProp} }); setOpen(false); setTimeout(() => { setOpen(false); }, 0); }; return (
{(field) => (
{field.value ? (
{field.value?.name ?? ""}
) : (
{placeholder ?? t(\`ui.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
)}
{field.value && ( set${names.pascalCase}()} /> )}
setSearchTerm(e.target.value)} value={searchTerm} /> {isSearching ? ( ) : searchTermRef.current ? ( { setSearchTerm(""); search(""); }} /> ) : ( <> )}
{data.data && data.data.length > 0 && (data.data as ${names.pascalCase}Interface[]).map((${names.camelCase}: ${names.pascalCase}Interface) => ( set${names.pascalCase}(${names.camelCase})} > {${names.camelCase}.${displayProp}} ))}
)}
); } `; }