211 lines
7.1 KiB
TypeScript
211 lines
7.1 KiB
TypeScript
// * Packages
|
|
import { usePage } from "@inertiajs/react";
|
|
import { useEffect, useState } from "react";
|
|
import { CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon } from "lucide-react";
|
|
// * Components shadcn
|
|
import { Label } from "@/components/ui/label";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
// * Other
|
|
import BtnOptionnalLinkWithTooltip from "@/back/Components//BtnOptionnalLinkWithTooltip";
|
|
import InputError from "@/back/Components/InputError";
|
|
import { AppEnv } from "@/back/types/global";
|
|
import { cn } from "@/lib/utils";
|
|
import useTrans from "@/hooks/use-translations";
|
|
import str from "@/hooks/use-string";
|
|
|
|
type EnumSelectProps = {
|
|
name: string;
|
|
label: string;
|
|
options?: Record<string, { label: string; name: string; value: number }>;
|
|
placeholder?: string;
|
|
exclude?: number[];
|
|
selected?: number|null;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
parentErrors?: Record<string, string> | null,
|
|
setData: (name: string, value: number | null) => void;
|
|
onSelectValue?: (value: number | null) => void;
|
|
};
|
|
|
|
export default function EnumSelect({
|
|
name,
|
|
label,
|
|
options = {},
|
|
placeholder = "ex: Magnifique libellé",
|
|
exclude = [],
|
|
selected = null,
|
|
required = true,
|
|
disabled = false,
|
|
parentErrors = null,
|
|
setData,
|
|
onSelectValue,
|
|
}: EnumSelectProps) {
|
|
const { locale } = usePage().props.appEnv as AppEnv;
|
|
const { errors } = usePage().props;
|
|
const localErrors = parentErrors ?? errors;
|
|
|
|
// List of options.
|
|
const initalOptions = Object.fromEntries(
|
|
Object.entries(options).filter(([key, item]) =>
|
|
!exclude.includes(item.value)
|
|
)
|
|
)
|
|
const [currentOptions, setCurrentOptions] = useState<Record<string, { label: string; name: string; value: number }>>(initalOptions);
|
|
|
|
// Model key selected.
|
|
const initial = selected === 0 || selected === null || selected === undefined ? null : Number(selected);
|
|
const [selectedEnum, setSelectedEnum] = useState<number|null>(initial);
|
|
|
|
// String research in models collection.
|
|
const [search, setSearch] = useState<string>("");
|
|
|
|
// Component states.
|
|
const [open, setOpen] = useState<boolean>(false);
|
|
|
|
/**
|
|
* Get the identification field of specific option.
|
|
* @param option Model
|
|
* @return string
|
|
*/
|
|
const getEnumLabel = (value: number): string => {
|
|
const targetEnum = Object.values(initalOptions).find(option => option.value === value);
|
|
if (!targetEnum) return String(value);
|
|
if (typeof value === "object") return String(targetEnum.label[locale as keyof typeof targetEnum.label] ?? "");
|
|
return targetEnum.label;
|
|
};
|
|
|
|
/**
|
|
* Updates when selecting new value.
|
|
* @param value number
|
|
* @return void
|
|
*/
|
|
const handleSelect = (value: number) => {
|
|
setSelectedEnum(value);
|
|
setData(name, value);
|
|
onSelectValue?.(value);
|
|
setOpen(false);
|
|
};
|
|
|
|
/**
|
|
* Updates when clear the selected value.
|
|
* @return void
|
|
*/
|
|
const handleClear = () => {
|
|
setSelectedEnum(null);
|
|
setData(name, null);
|
|
onSelectValue?.(null);
|
|
};
|
|
|
|
/**
|
|
* Reset the search.
|
|
* @return void
|
|
*/
|
|
const handleClearSearch = () => {
|
|
setSearch("");
|
|
};
|
|
|
|
// Fetch with debounce.
|
|
useEffect(() => {
|
|
// Reset options list if empty search or null
|
|
if (search.length === 0) {
|
|
setCurrentOptions(initalOptions);
|
|
return;
|
|
}
|
|
|
|
const optionsFiltered = Object.fromEntries(
|
|
Object.entries(initalOptions).filter(([key, item]) =>
|
|
item.label.toLowerCase().includes(search)
|
|
)
|
|
);
|
|
|
|
// Get updated options list with search.
|
|
setCurrentOptions(optionsFiltered);
|
|
}, [search, options]);
|
|
|
|
return (
|
|
<div className="flex flex-col w-full gap-1.5">
|
|
<Label htmlFor={name}>
|
|
{label} {required && "*"}
|
|
</Label>
|
|
<div className="flex items-center gap-1.5 w-full">
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
disabled={disabled}
|
|
className={cn(
|
|
"w-full h-10 justify-between whitespace-normal text-start",
|
|
{ "text-muted-foreground": !selectedEnum },
|
|
{ "border-destructive/50": localErrors[name] },
|
|
)}
|
|
>
|
|
{selectedEnum ? getEnumLabel(selectedEnum) : placeholder}
|
|
<ChevronsUpDownIcon className="opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
|
<div className="relative">
|
|
<div className="absolute top-1/2 left-2 transform -translate-y-1/2 text-muted-foreground">
|
|
<SearchIcon className="w-4 h-4" />
|
|
</div>
|
|
<Input
|
|
type="text"
|
|
placeholder={placeholder}
|
|
value={search}
|
|
disabled={disabled}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-[calc(100%-2.5rem-40px)] border-0 mx-9"
|
|
/>
|
|
<BtnOptionnalLinkWithTooltip
|
|
content={<XIcon />}
|
|
tooltipContent={<p>{str(useTrans("crud.actions.delete")).ucFirst().value()}</p>}
|
|
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}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={cn(`overflow-auto border-t flex flex-col justify-between p-1 gap-1 ${Object.values(initalOptions).length > 5 ? "h-[225px]" : "h-auto"}`)}
|
|
>
|
|
{Object.values(currentOptions).length === 0 && (
|
|
<p className="text-sm text-muted-foreground text-center py-5">{useTrans("back.table_no_results")}</p>
|
|
)}
|
|
<div className="flex flex-col justify-between">
|
|
{Object.values(currentOptions).map((option) => (
|
|
<Button
|
|
key={option.value}
|
|
className="w-full h-auto min-h-10 justify-between whitespace-normal text-start"
|
|
variant="ghost"
|
|
disabled={disabled}
|
|
onClick={() => handleSelect(option.value)}
|
|
>
|
|
<span>{option.label}</span>
|
|
{selectedEnum === option.value && <CheckIcon />}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
{!required && selectedEnum !== 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={localErrors[name]} />
|
|
</div>
|
|
);
|
|
}
|