ldc-groupe/resources/ts/back/Components/Inputs/ModelSelect.tsx
2025-10-29 10:29:57 +01:00

185 lines
7 KiB
TypeScript

// * Packages
import { usePage } from "@inertiajs/react";
import { useEffect, useMemo, useState } from "react";
// * Components shadcn
import { Label } from "@/components/ui/label";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
// * Other
import InputError from "@/back/Components/InputError";
import { AppEnv, Model, TranslationField } from "@/back/types/global";
import { cn } from "@/lib/utils";
import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import useTrans from "@/hooks/use-translations";
import BtnOptionnalLinkWithTooltip from "../BtnOptionnalLinkWithTooltip";
import str from "@/hooks/use-string";
type ModelSelectProps = {
name: string;
label: string;
placeholder?: string;
options: Array<Model>;
field: string;
required?: boolean;
disabled?: boolean;
selected?: number | null;
setData: (name: string, value: number | null) => void;
onSelectValue?: (value: string | null) => void;
};
export default function ModelSelect({
name,
label,
placeholder = "ex: Magnifique libellé",
options = [],
field = "label",
selected = null,
required = true,
disabled = false,
setData,
onSelectValue,
}: ModelSelectProps) {
const { locale } = usePage().props.appEnv as AppEnv;
const { errors } = usePage().props;
const [open, setOpen] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<number | null>(
selected ?? null,
);
// Get label of an option.
const getFieldLabel = (option: Model): string => {
const value = option[field as keyof typeof option];
if (typeof value === "object") {
return String((value as TranslationField)[locale]);
}
return String(value ?? "");
};
// Sort the options list.
const sortedModels = useMemo(() => {
return [...options].sort(
(model1, model2) => getFieldLabel(model1) > getFieldLabel(model2),
);
}, [options, field, locale]);
// Get the label of the selected value.
const getSelectedLabel = (): string => {
const current = sortedModels.find((model) => model.id === selectedId);
return current ? getFieldLabel(current) : placeholder;
};
// Update the field.
const handleSelect = (modelId: number) => {
const selectedModel = sortedModels.find((m) => m.id === modelId);
const value = selectedModel ? getFieldLabel(selectedModel) : null;
setSelectedId(modelId);
setData(name, modelId);
if (onSelectValue) onSelectValue(value);
setOpen(false);
};
// Reset the value.
const handleClear = () => {
setSelectedId(null);
setData(name, null);
if (onSelectValue) onSelectValue(null);
};
useEffect(() => {
const selectedModel = sortedModels.find((m) => m.id === selectedId);
const value = selectedModel ? getFieldLabel(selectedModel) : null;
if (onSelectValue) onSelectValue(value);
}, []);
return (
<div className="flex flex-col w-full justify-start items-start gap-1.5">
<Label htmlFor={name}>{label + (required ? " *" : "")}</Label>
<div className="flex w-full items-center gap-1.5">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("justify-between w-full", {
"text-muted-foreground hover:text-muted-foreground focus:text-muted-foreground":
selectedId === null,
})}
disabled={disabled}
>
{getSelectedLabel()}
<ChevronsUpDownIcon className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] p-0"
align="start"
>
<Command>
<CommandInput
placeholder={placeholder}
className="border-0 focus:border-0 h-9 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none shadow-none"
/>
<CommandList>
<CommandEmpty>
{useTrans("back.table_no_results")}
</CommandEmpty>
<CommandGroup>
{sortedModels.map((option) => (
<CommandItem
key={option.id}
value={getFieldLabel(option)}
onSelect={() =>
handleSelect(option.id)
}
>
{getFieldLabel(option)}
<CheckIcon
className={cn(
"ml-auto",
selectedId === option.id
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{!required && selectedId !== null && (
<BtnOptionnalLinkWithTooltip
content={<XIcon />}
tooltipContent={
<p>
{str(useTrans("crud.actions.delete"))
.ucFirst()
.value()}
</p>
}
type="button"
variant="destructive"
size="icon"
onClick={handleClear}
/>
)}
</div>
<InputError message={errors[name]} />
</div>
);
}