171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import { ReactNode, useRef, useState } from "react";
|
|
import clsx from "~/utils/shared/ClassesUtils";
|
|
import HintTooltip from "../tooltips/HintTooltip";
|
|
import ColorBadge from "../badges/ColorBadge";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Input } from "../input";
|
|
import { Select } from "~/ui/components/Select";
|
|
|
|
interface Props {
|
|
name?: string;
|
|
title?: string;
|
|
withLabel?: boolean;
|
|
options: {
|
|
name: string | ReactNode;
|
|
value: string | number | undefined;
|
|
disabled?: boolean;
|
|
component?: ReactNode;
|
|
color?: number;
|
|
}[];
|
|
value?: string | number | undefined;
|
|
setValue?: React.Dispatch<React.SetStateAction<string | number | undefined>>;
|
|
defaultValue?: string | number | undefined;
|
|
className?: string;
|
|
required?: boolean;
|
|
disabled?: boolean;
|
|
help?: string;
|
|
hint?: ReactNode;
|
|
borderless?: boolean;
|
|
autoFocus?: boolean;
|
|
placeholder?: string;
|
|
withColors?: boolean;
|
|
withSearch?: boolean;
|
|
selectClassName?: string;
|
|
canClear?: boolean;
|
|
dirty?: boolean;
|
|
size?: "sm" | "default";
|
|
style?: React.CSSProperties;
|
|
hideIcon?: boolean;
|
|
icon?: React.ReactNode;
|
|
}
|
|
export default function InputSelect({
|
|
name,
|
|
title,
|
|
withLabel = true,
|
|
value,
|
|
defaultValue,
|
|
options,
|
|
setValue,
|
|
className,
|
|
required,
|
|
disabled,
|
|
help,
|
|
hint,
|
|
borderless,
|
|
// autoFocus,
|
|
placeholder,
|
|
withColors,
|
|
withSearch = false,
|
|
selectClassName,
|
|
canClear = true,
|
|
size = "default",
|
|
style,
|
|
icon,
|
|
hideIcon = false,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
const refSearch = useRef<HTMLInputElement>(null);
|
|
const [inputSearch, setInputSearch] = useState("");
|
|
const filteredOptions = () => {
|
|
let filteredItems = options.filter((f) => f.value);
|
|
if (inputSearch.trim()) {
|
|
filteredItems = options.filter((item) => {
|
|
return (
|
|
item.name?.toString().toLowerCase().includes(inputSearch.toLowerCase()) || item.value?.toString().toLowerCase().includes(inputSearch.toLowerCase())
|
|
);
|
|
});
|
|
}
|
|
|
|
if (canClear) {
|
|
return [
|
|
{
|
|
name: getPlaceholder(),
|
|
value: "{null}",
|
|
disabled: false,
|
|
},
|
|
...filteredItems,
|
|
];
|
|
}
|
|
|
|
return filteredItems;
|
|
};
|
|
|
|
const getPlaceholder = () => {
|
|
return withLabel ? placeholder || `${t("shared.select")}...` : title || `${t("shared.select")}...`;
|
|
};
|
|
|
|
const showLabel = Boolean(withLabel && title);
|
|
|
|
// Rich label content rendered into Subframe's label slot (spans only → inline-safe).
|
|
const labelContent: ReactNode = showLabel ? (
|
|
<span className="flex w-full items-center justify-between gap-2">
|
|
<span className="flex items-center space-x-1 truncate">
|
|
<span className="flex space-x-1 truncate">
|
|
<span className="truncate">{title}</span>
|
|
{required && title && <span className="ml-1 text-red-600">*</span>}
|
|
</span>
|
|
{help && <HintTooltip text={help} />}
|
|
</span>
|
|
{hint}
|
|
</span>
|
|
) : null;
|
|
|
|
return (
|
|
<div className={clsx(className, "")} style={style}>
|
|
<Select
|
|
className={clsx("w-full max-w-none", selectClassName)}
|
|
name={name}
|
|
value={value !== undefined ? value.toString() : undefined}
|
|
defaultValue={defaultValue !== undefined ? defaultValue.toString() : undefined}
|
|
onValueChange={(e) => {
|
|
if (e === "{null}") {
|
|
setValue?.("");
|
|
} else {
|
|
setValue?.(e);
|
|
}
|
|
}}
|
|
disabled={disabled}
|
|
required={required}
|
|
borderless={borderless}
|
|
size={size === "sm" ? "small" : "medium"}
|
|
showLabel={showLabel}
|
|
label={labelContent}
|
|
icon={!hideIcon ? icon : undefined}
|
|
placeholder={getPlaceholder()}
|
|
>
|
|
{withSearch && options.length > 30 && (
|
|
<Input
|
|
ref={refSearch}
|
|
autoFocus
|
|
type="text"
|
|
className="mb-1 px-2 py-1.5"
|
|
placeholder={t("shared.search")}
|
|
value={inputSearch}
|
|
onChange={(e) => setInputSearch(e.target.value)}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
onKeyDownCapture={(e) => e.stopPropagation()}
|
|
onKeyUp={(e) => e.stopPropagation()}
|
|
/>
|
|
)}
|
|
{filteredOptions().length === 0 && (
|
|
<Select.Item disabled value={"{empty}"}>
|
|
<div className="text-muted-foreground w-full text-center">{t("shared.noRecords")}</div>
|
|
</Select.Item>
|
|
)}
|
|
{filteredOptions().map((item, idx) => {
|
|
return (
|
|
<Select.Item key={idx} disabled={item.disabled} value={item.value?.toString() ?? ""}>
|
|
{("component" in item && item.component) || (
|
|
<div className="flex items-center space-x-2">
|
|
{withColors && "color" in item && <ColorBadge color={item.color} />}
|
|
<div>{item.name}</div>
|
|
</div>
|
|
)}
|
|
</Select.Item>
|
|
);
|
|
})}
|
|
</Select>
|
|
</div>
|
|
);
|
|
}
|