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