185 lines
7 KiB
TypeScript
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>
|
|
);
|
|
}
|