221 lines
8.2 KiB
TypeScript
221 lines
8.2 KiB
TypeScript
import { Ref, useImperativeHandle, useRef, useState, forwardRef, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Colors } from "~/application/enums/shared/Colors";
|
|
import ColorBadge from "~/components/ui/badges/ColorBadge";
|
|
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
|
|
import ButtonSecondary from "~/components/ui/buttons/ButtonSecondary";
|
|
import ButtonTertiary from "~/components/ui/buttons/ButtonTertiary";
|
|
import TrashIcon from "~/components/ui/icons/TrashIcon";
|
|
import InputCheckboxInline from "~/components/ui/input/InputCheckboxInline";
|
|
import InputSelector from "~/components/ui/input/InputSelector";
|
|
import InputText, { RefInputText } from "~/components/ui/input/InputText";
|
|
import ErrorModal, { RefErrorModal } from "~/components/ui/modals/ErrorModal";
|
|
import SlideOverWideEmpty from "~/components/ui/slideOvers/SlideOverWideEmpty";
|
|
import { getColors } from "~/utils/shared/ColorUtils";
|
|
import { updateItemByIdx } from "~/utils/shared/ObjectUtils";
|
|
|
|
export type OptionValue = {
|
|
id?: string | null;
|
|
parentId?: string | null;
|
|
order: number;
|
|
value: string;
|
|
name: string | null;
|
|
color?: Colors;
|
|
options?: OptionValue[];
|
|
};
|
|
export interface RefPropertyOptionsForm {
|
|
set: (options: OptionValue[]) => void;
|
|
}
|
|
|
|
interface Props {
|
|
title: string;
|
|
onSet: (items: OptionValue[]) => void;
|
|
}
|
|
|
|
const PropertyOptionsForm = ({ title, onSet }: Props, ref: Ref<RefPropertyOptionsForm>) => {
|
|
useImperativeHandle(ref, () => ({ set }));
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const errorModal = useRef<RefErrorModal>(null);
|
|
|
|
const inputOption = useRef<RefInputText>(null);
|
|
|
|
const [withColors, setWithColors] = useState(false);
|
|
const [withDescriptions, setWithDescriptions] = useState(false);
|
|
|
|
const [open, setOpen] = useState(false);
|
|
const [items, setItems] = useState<OptionValue[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!withDescriptions) {
|
|
setItems((items) => items.map((f) => ({ ...f, name: null })));
|
|
}
|
|
}, [withDescriptions]);
|
|
|
|
useEffect(() => {
|
|
if (!withColors) {
|
|
setItems((items) => items.map((f) => ({ ...f, color: undefined })));
|
|
}
|
|
}, [withColors]);
|
|
|
|
function set(options: OptionValue[]) {
|
|
setItems(options);
|
|
if (options.length === 0 && items.length === 0) {
|
|
addOption();
|
|
}
|
|
setWithColors(options.some((f) => f.color));
|
|
setWithDescriptions(options.some((f) => f.name));
|
|
|
|
setOpen(true);
|
|
}
|
|
|
|
function close() {
|
|
setOpen(false);
|
|
}
|
|
|
|
function save() {
|
|
const emptyItems = items.filter((f) => !f.value || f.value.trim() === "");
|
|
if (emptyItems.length > 0) {
|
|
errorModal.current?.show(t("entities.errors.selectOptionCannotBeEmpty"));
|
|
return;
|
|
}
|
|
onSet(items);
|
|
close();
|
|
}
|
|
|
|
function addOption() {
|
|
const maxOrder = items.length === 0 ? 1 : Math.max(...items.map((o) => o.order));
|
|
setItems([
|
|
...items,
|
|
{
|
|
id: null,
|
|
parentId: null,
|
|
order: maxOrder + 1,
|
|
value: "",
|
|
name: null,
|
|
color: Colors.UNDEFINED,
|
|
options: [],
|
|
},
|
|
]);
|
|
|
|
setTimeout(() => {
|
|
inputOption.current?.input.current?.focus();
|
|
}, 1);
|
|
}
|
|
|
|
return (
|
|
<SlideOverWideEmpty title={t("models.property.optionEditor.titleWithProperty", { property: t(title) })} open={open} onClose={() => setOpen(false)}>
|
|
<div className="">
|
|
<div>
|
|
<div className="mt-3 sm:mt-5">
|
|
<div className="mt-4 space-y-3">
|
|
<div className="flex space-x-3">
|
|
<InputCheckboxInline name="with-colors" title={t("models.property.optionEditor.withColors")} description="" value={withColors} setValue={setWithColors} />
|
|
<InputCheckboxInline
|
|
name="with-descriptions"
|
|
title={t("models.property.optionEditor.withDescriptions")}
|
|
description=""
|
|
value={withDescriptions}
|
|
setValue={setWithDescriptions}
|
|
/>
|
|
</div>
|
|
<div className="w-full">
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<label htmlFor="select" className="text-foreground/80 block text-sm font-medium">{t("models.property.options")}</label>
|
|
<div className="flex space-x-2">
|
|
<ButtonTertiary onClick={addOption}>
|
|
<div className="-mx-1 text-xs">{t("models.property.optionEditor.add")}</div>
|
|
</ButtonTertiary>
|
|
</div>
|
|
</div>
|
|
<div className="border-border bg-secondary mt-1 space-y-2 rounded-md border-2 border-dashed p-2">
|
|
{items.map((option, idx) => {
|
|
return (
|
|
<div key={option.order} className="mt-1 flex items-center space-x-1 rounded-md">
|
|
<InputText
|
|
ref={inputOption}
|
|
type="text"
|
|
title="shared.value"
|
|
withLabel={false}
|
|
name={"select-option-" + idx}
|
|
value={option.value}
|
|
placeholder={t("shared.value")}
|
|
setValue={(e) =>
|
|
updateItemByIdx(items, setItems, idx, {
|
|
value: e,
|
|
})
|
|
}
|
|
className="flex-auto"
|
|
/>
|
|
|
|
{withDescriptions && (
|
|
<InputText
|
|
type="text"
|
|
title=""
|
|
withLabel={false}
|
|
name={"select-option-name-" + idx}
|
|
placeholder={t("shared.name")}
|
|
value={option.name ?? undefined}
|
|
setValue={(e) =>
|
|
updateItemByIdx(items, setItems, idx, {
|
|
name: e,
|
|
})
|
|
}
|
|
className="flex-auto"
|
|
/>
|
|
)}
|
|
|
|
{withColors && (
|
|
<div className="w-32">
|
|
<InputSelector
|
|
name="color"
|
|
title={t("models.group.color")}
|
|
withSearch={false}
|
|
value={option.color ?? Colors.UNDEFINED}
|
|
withLabel={false}
|
|
setValue={(e) =>
|
|
updateItemByIdx(items, setItems, idx, {
|
|
color: Number(e),
|
|
})
|
|
}
|
|
selectPlaceholder={""}
|
|
className="w-32"
|
|
options={
|
|
getColors().map((color) => {
|
|
return {
|
|
name: <ColorBadge color={color} />,
|
|
value: color,
|
|
};
|
|
}) ?? []
|
|
}
|
|
></InputSelector>
|
|
</div>
|
|
)}
|
|
|
|
<ButtonSecondary disabled={items.length === 1} type="button" onClick={() => setItems([...items.filter((_, index) => index !== idx)])}>
|
|
<TrashIcon className="text-muted-foreground h-4 w-4" aria-hidden="true" />
|
|
</ButtonSecondary>
|
|
</div>
|
|
);
|
|
})}
|
|
<div className="w-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="border-border mt-5 border-t pt-3 sm:mt-6">
|
|
<div className="flex items-center justify-end space-x-2">
|
|
<ButtonSecondary onClick={close}>{t("shared.cancel")}</ButtonSecondary>
|
|
<ButtonPrimary onClick={save}>{t("shared.save")}</ButtonPrimary>
|
|
</div>
|
|
</div>
|
|
<ErrorModal ref={errorModal} />
|
|
</div>
|
|
</SlideOverWideEmpty>
|
|
);
|
|
};
|
|
|
|
export default forwardRef(PropertyOptionsForm);
|