// * Packages import { usePage } from "@inertiajs/react"; import { useEffect, useState } from "react"; import axios from "axios"; import { CheckIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDownIcon, LoaderCircleIcon, SearchIcon, XIcon } from "lucide-react"; import { route } from "ziggy-js"; // * Components shadcn import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; // * Other import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip"; import InputError from "@/back/Components/InputError"; import { AppEnv, DynamicPageModel, Model, Paginator, TranslationField, } from "@/back/types/global"; import { cn } from "@/lib/utils"; import useTrans from "@/hooks/use-translations"; import str from "@/hooks/use-string"; type ModelSelectProps = { name: string; label: string; targetModel?: string|null; placeholder?: string; field?: string; exclude?: number|null; selected?: number|null; required?: boolean; disabled?: boolean; parentErrors?: Record | null, setData: (name: string, value: number | null) => void; onSelectValue?: (value: Model | null) => void; }; export default function ModelSelect({ name, label, targetModel = null, placeholder = "ex: Magnifique libellé", field, exclude = null, selected = null, required = true, disabled = false, parentErrors = null, setData, onSelectValue, }: ModelSelectProps) { const { locale } = usePage().props.appEnv as AppEnv; const { errors } = usePage().props; const localErrors = parentErrors ?? errors; // List of options. const [options, setOptions] = useState([]); // Field used to identify option. // Custom by field parameter or predefined values (title, name or label). const [dynamicField, setDynamicField] = useState(field ?? null); // Model key selected. const initial = selected === 0 || selected === null || selected === undefined ? null : Number(selected); const [selectedId, setSelectedId] = useState(initial); // String research in models collection. const [search, setSearch] = useState(""); // Field value of the selected model. const [selectedValue, setSelectedValue] = useState(null); // Component states. const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); // Pagination. const [page, setPage] = useState(1); const [pageSize] = useState(5); const [lastPage, setLastPage] = useState(1); /** * Get the identification field of specific option. * @param option Model * @return string */ const getModelField = (option: Model): string => { const value = option[dynamicField as keyof Model] as unknown as TranslationField | string; if (typeof value === "object") return String(value[locale] ?? ""); return String(value ?? ""); }; /** * Updates when selecting new value. * @param id number * @return void */ const handleSelect = (id: number) => { const selectedModel = options.find((model) => model.id === id); setSelectedValue(selectedModel ? getModelField(selectedModel) : null); setSelectedId(id); setData(name, id); onSelectValue?.(selectedModel ?? null); setOpen(false); }; /** * Updates when clear the selected value. * @return void */ const handleClear = () => { setSelectedValue(null); setSelectedId(null); setData(name, null); onSelectValue?.(null); }; /** * Reset the search. * @return void */ const handleClearSearch = () => { setSearch(""); }; /** * Set call ajax. * @param pageNumber number * @return */ const fetchOptions = async (pageNumber: number) => { const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || ""; const response = await axios.post( route("back.model.list"), { model: targetModel, selectedId, search, field: dynamicField, page: pageNumber, pageSize, exclude, }, { headers: { "Content-Type": "application/json", Accept: "application/json", "X-Requested-With": "XMLHttpRequest", "X-Inertia": "false", "X-CSRF-TOKEN": csrfToken, }, } ); return response.data; }; /** * Get the localized value. * @param item DynamicPageModel * @param field string * @param locale string * @return string */ function extractLocalized( item: DynamicPageModel, field: string, locale: string ): string { const raw = item[field as keyof DynamicPageModel]; if (typeof raw === "object" && raw !== null) { return (raw as TranslationField)[locale] ?? ""; } return String(raw ?? ""); } /** * Get new options list with his target field. * @param pageNumber number * @return void */ const getData = async (pageNumber = 1) => { setLoading(true); try { const data = await fetchOptions(pageNumber) as { items: Paginator, field: string, model: DynamicPageModel, }; if (data.model) { const value = extractLocalized(data.model, data.field, locale); setSelectedValue(value); onSelectValue?.(data.model); setSelectedId(data.model['id']); } setDynamicField(data.field); setOptions(data.items.data ?? []); setPage(data.items.current_page); setLastPage(data.items.last_page ?? 1); } catch (err) { console.error(err); setOptions([]); setLastPage(1); } finally { setLoading(false); } }; /** * Get specific page. * @param pageNumber number * @return void */ const goToPage = async (pageNumber: number) => { if (pageNumber < 1 || pageNumber > lastPage) return; getData(pageNumber); }; // Fetch with debounce. useEffect(() => { setDynamicField(field ?? null); // Reset options list if empty search or null if (targetModel && search.length === 0) { getData(); return; } // Nothing to do if search is below 3 caracters. if (search.length < 3) return; // Get updated options list with search. const handler = setTimeout(() => getData(), 300); return () => clearTimeout(handler); }, [search, targetModel, field]); return (
{loading ? : }
setSearch(e.target.value)} className="w-[calc(100%-2.5rem-40px)] border-0 mx-9" /> } tooltipContent={

{str(useTrans("crud.actions.delete")).ucFirst().value()}

} type="button" variant="ghost" size="icon" className="absolute top-1/2 right-0 transform -translate-y-1/2 text-muted-foreground" onClick={handleClearSearch} disabled={search.length < 1 || disabled || loading} />
0 && lastPage !== 1 ? '265px' : '225px' }} > {!loading && options.length === 0 && (

{useTrans("back.table_no_results")}

)} {loading ? ( <> {[...Array(pageSize)].map(() => ( ))} ) : (
{options.map((option) => ( ))}
)} {/* Pagination boutons */} {options.length > 0 && lastPage !== 1 && (
{useTrans('pagination.previous')} } tooltipContent={

{useTrans('pagination.specific_page', { params: { id: String(page - 1) } })}

} type="button" variant="outline" size="sm" onClick={() => goToPage(page - 1)} disabled={page <= 1 || loading} /> {loading ? ( ) : (

{page}/{lastPage}

)} {useTrans('pagination.next')} } tooltipContent={

{useTrans('pagination.specific_page', { params: { id: String(page + 1) } })}

} type="button" variant="outline" size="sm" onClick={() => goToPage(page + 1)} disabled={page >= lastPage || loading} />
)}
{!required && selectedId !== null && ( } tooltipContent={

{str(useTrans("crud.actions.delete")).ucFirst().value()}

} type="button" variant="destructive" size="icon" onClick={handleClear} disabled={disabled || loading} /> )}
); }