ldc-laravel-base-project/resources/ts/back/Components/Inputs/EnumSelect.tsx
2026-07-09 17:32:17 +02:00

224 lines
7.1 KiB
TypeScript

import { usePage } from "@inertiajs/react";
import { CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon } from "lucide-react";
import { useState } from "react";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import InputError from "@/back/Components/InputError";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { cn } from "@/lib/utils";
interface 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: John Doe",
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;
// Model key selected.
const initial = selected && selected !== 0 ? selected : null;
const [selectedEnum, setSelectedEnum] = useState<number | null>(initial);
// String research in models collection.
const [search, setSearch] = useState<string>("");
// List of options.
const initalOptions = Object.fromEntries(
Object.entries(options).filter(([, item]) => !exclude.includes(item.value)),
);
const currentOptions =
search.length === 0
? initalOptions
: Object.fromEntries(
Object.entries(initalOptions).filter(([, item]) =>
item.label.toLowerCase().includes(search),
),
);
// Component states.
const [open, setOpen] = useState<boolean>(false);
const transNoResults = useTrans("uwil-backend::back.table_no_results");
const transDelete = useTrans("uwil-backend::crud.actions.delete");
/**
* Get the identification field of specific option.
* @param value number
* @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("");
};
return (
<div className="flex flex-col w-full gap-1.5">
<Label htmlFor={name} className="flex items-center gap-1.5">
{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
? str(getEnumLabel(selectedEnum)).ucFirst().value()
: 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("uwil-backend::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">
{transNoResults}
</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 font-normal"
variant="ghost"
disabled={disabled}
onClick={() => {
handleSelect(option.value);
}}
>
<span>{str(option.label).ucFirst().value()}</span>
{selectedEnum === option.value && <CheckIcon />}
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
{!required && selectedEnum !== null && (
<BtnOptionnalLinkWithTooltip
content={<XIcon />}
tooltipContent={<p>{str(transDelete).ucFirst().value()}</p>}
type="button"
variant="destructive"
size="icon"
onClick={handleClear}
/>
)}
</div>
<InputError message={localErrors[name]} />
</div>
);
}