374 lines
16 KiB
TypeScript
374 lines
16 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useNavigate } from "react-router";
|
|
import InputText, { RefInputText } from "../ui/input/InputText";
|
|
import { useTranslation } from "react-i18next";
|
|
import InputGroup from "../ui/forms/InputGroup";
|
|
import EntityIcon from "../layouts/icons/EntityIcon";
|
|
import QuestionMarkFilledIcon from "../ui/icons/QuestionMarkFilledIcon";
|
|
import InputNumber from "../ui/input/InputNumber";
|
|
import InputCheckboxWithDescription from "../ui/input/InputCheckboxWithDescription";
|
|
import { Entity } from "@toolctrl/db/prisma-browser";
|
|
import FormGroup from "../ui/forms/FormGroup";
|
|
import { useAdminData } from "~/utils/data/useAdminData";
|
|
import StringUtils from "~/utils/shared/StringUtils";
|
|
import { DefaultVisibility } from "~/application/dtos/shared/DefaultVisibility";
|
|
import InputSelect from "../ui/input/InputSelect";
|
|
import VisibilityHelper from "~/utils/helpers/VisibilityHelper";
|
|
import Constants from "~/application/Constants";
|
|
import { DefaultEntityTypes } from "~/application/dtos/shared/DefaultEntityTypes";
|
|
import UrlUtils from "~/utils/app/UrlUtils";
|
|
import { adminPath, apiPath } from "~/utils/app/CanonicalUrlUtils";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
|
|
interface Props {
|
|
item?: Entity | null;
|
|
}
|
|
export default function EntityForm({ item }: Props) {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const adminData = useAdminData();
|
|
const rootData = useRootData();
|
|
|
|
const inputName = useRef<RefInputText>(null);
|
|
|
|
const [title, setTitle] = useState(item?.title ?? "");
|
|
const [titlePlural, setTitlePlural] = useState(item?.titlePlural ?? "");
|
|
const [name, setName] = useState(item?.name ?? "");
|
|
const [type, setType] = useState(item?.type ?? DefaultEntityTypes.AppOnly);
|
|
|
|
const [slug, setSlug] = useState(item?.slug ?? "");
|
|
const [icon, setIcon] = useState(
|
|
item?.icon ??
|
|
`<svg stroke="currentColor" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5"> <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> </svg>`
|
|
);
|
|
const [order, setOrder] = useState<number | undefined>(
|
|
item?.order ?? (adminData.entities.length > 0 ? Math.max(...adminData.entities.map((o) => o.order)) : 0) + 1
|
|
);
|
|
const [prefix, setPrefix] = useState(item?.prefix ?? "");
|
|
const [isAutogenerated, setIsAutogenerated] = useState(item?.isAutogenerated ?? true);
|
|
const [hasApi, setHasApi] = useState(item?.hasApi ?? true);
|
|
const [active, setActive] = useState(item?.active ?? true);
|
|
|
|
const [showInSidebar, setShowInSidebar] = useState(item?.showInSidebar ?? true);
|
|
const [hasTags, setHasTags] = useState(item?.hasTags ?? true);
|
|
const [hasComments, setHasComments] = useState(item?.hasComments ?? true);
|
|
const [hasTasks, setHasTasks] = useState(item?.hasTasks ?? false);
|
|
const [hasActivity, setHasActivity] = useState(item ? item.hasActivity : true);
|
|
const [hasBulkDelete, setHasBulkDelete] = useState(item ? item.hasBulkDelete : false);
|
|
const [hasViews, setHasViews] = useState(item ? item.hasViews : false);
|
|
|
|
const [defaultVisibility, setDefaultVisibility] = useState<string | number | undefined>(item?.defaultVisibility ?? Constants.DEFAULT_ROW_VISIBILITY);
|
|
|
|
const [onCreated, setOnCreated] = useState(item?.onCreated ?? "redirectToOverview");
|
|
const [onEdit, setOnEdit] = useState(item?.onEdit ?? "editRoute");
|
|
const apiRowsPath = apiPath(`entities/${slug || "entity-name"}/rows`, { mode: "host-aware" });
|
|
const apiRowsUrl = rootData.apiBaseUrl ? new URL(apiRowsPath, rootData.apiBaseUrl).toString() : apiRowsPath;
|
|
|
|
useEffect(() => {
|
|
setTimeout(() => {
|
|
inputName.current?.input.current?.focus();
|
|
}, 100);
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// useEffect(() => {
|
|
// if (!item) {
|
|
// const slug = UrlUtils.slugify(t(titlePlural));
|
|
// setSlug(slug);
|
|
// if (titlePlural.length >= 3) {
|
|
// setPrefix(t(titlePlural).substring(0, 3).toUpperCase());
|
|
// }
|
|
// }
|
|
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
// }, [titlePlural]);
|
|
|
|
return (
|
|
<FormGroup id={item?.id} onCancel={() => navigate(adminPath("entities", { mode: "host-aware" }))} editing={true} canDelete={false}>
|
|
<InputGroup title={t("models.entity.object")}>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
|
<InputText
|
|
ref={inputName}
|
|
className="sm:col-span-6"
|
|
name="name"
|
|
title={t("models.entity.name")}
|
|
value={name}
|
|
setValue={setName}
|
|
required
|
|
help={t("models.entity.form.help.name")}
|
|
autoComplete="off"
|
|
onBlur={() => {
|
|
if (!title.trim()) {
|
|
const generateTitle = (str: string) => {
|
|
return str
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2") // Insert space before capital letters
|
|
.split(" ") // Split by spaces
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // Capitalize the first letter of each word
|
|
.join(" "); // Join the words back together
|
|
};
|
|
const newTitle = generateTitle(name);
|
|
if (newTitle) {
|
|
setTitle(newTitle);
|
|
setPrefix(t(newTitle).substring(0, 3).toUpperCase());
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<InputText
|
|
className="sm:col-span-6"
|
|
name="title"
|
|
title={t("models.entity.title")}
|
|
value={title}
|
|
setValue={setTitle}
|
|
required
|
|
autoComplete="off"
|
|
help={t("models.entity.form.help.title")}
|
|
withTranslation
|
|
/>
|
|
<InputText
|
|
className="sm:col-span-6"
|
|
name="slug"
|
|
title={t("models.entity.slug")}
|
|
value={slug}
|
|
setValue={(e) => {
|
|
const withoutStartingSlash = e?.toString().replace(/^\//, "");
|
|
setSlug(withoutStartingSlash);
|
|
}}
|
|
required
|
|
autoComplete="off"
|
|
help={t("models.entity.form.help.slug")}
|
|
onBlur={() => {
|
|
setSlug(UrlUtils.slugify(slug));
|
|
if (!titlePlural.trim()) {
|
|
const words = slug.split(/[- ]/).map((w) => StringUtils.capitalize(w.toLowerCase()));
|
|
setTitlePlural(words.join(" "));
|
|
}
|
|
}}
|
|
/>
|
|
<InputText
|
|
className="sm:col-span-6"
|
|
name="title-plural"
|
|
title={t("models.entity.titlePlural")}
|
|
value={titlePlural}
|
|
setValue={setTitlePlural}
|
|
required
|
|
autoComplete="off"
|
|
help={t("models.entity.form.help.titlePlural")}
|
|
withTranslation
|
|
/>
|
|
</div>
|
|
</InputGroup>
|
|
<InputGroup title={t("models.entity.permissions")}>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
|
<InputSelect
|
|
className="sm:col-span-12"
|
|
name="default-visibility"
|
|
title={t("models.entity.defaultVisibility")}
|
|
value={defaultVisibility}
|
|
setValue={setDefaultVisibility}
|
|
options={[
|
|
{
|
|
name: VisibilityHelper.getVisibilityTitle(t, DefaultVisibility.Private),
|
|
value: DefaultVisibility.Private,
|
|
},
|
|
{
|
|
name: VisibilityHelper.getVisibilityTitle(t, DefaultVisibility.Tenant),
|
|
value: DefaultVisibility.Tenant,
|
|
},
|
|
{
|
|
name: VisibilityHelper.getVisibilityTitle(t, DefaultVisibility.Public),
|
|
value: DefaultVisibility.Public,
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
</InputGroup>
|
|
<InputGroup title={t("models.entity.form.sections.display")}>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
|
<InputSelect
|
|
className="sm:col-span-3"
|
|
name="type"
|
|
title={t("models.entity.type")}
|
|
value={type}
|
|
// display="name"
|
|
setValue={(e) => setType(e?.toString() ?? DefaultEntityTypes.AppOnly)}
|
|
options={[
|
|
{ name: t("models.entity.typeOptions.app"), value: DefaultEntityTypes.AppOnly },
|
|
{ name: t("models.entity.typeOptions.admin"), value: DefaultEntityTypes.AdminOnly },
|
|
{ name: t("models.entity.typeOptions.appAndAdmin"), value: DefaultEntityTypes.All },
|
|
{ name: t("models.entity.typeOptions.system"), value: DefaultEntityTypes.System },
|
|
]}
|
|
required
|
|
/>
|
|
<InputNumber
|
|
className="sm:col-span-3"
|
|
name="order"
|
|
title={t("models.entity.order")}
|
|
value={order}
|
|
setValue={setOrder}
|
|
disabled={!item}
|
|
min={1}
|
|
max={99}
|
|
required
|
|
help={t("models.entity.form.help.order")}
|
|
// readOnly={item === undefined}
|
|
/>
|
|
|
|
<InputText
|
|
className="sm:col-span-3"
|
|
name="prefix"
|
|
title={t("models.entity.prefix")}
|
|
value={prefix}
|
|
setValue={(e) => setPrefix(e.toString().toUpperCase())}
|
|
minLength={2}
|
|
maxLength={5}
|
|
required
|
|
help={t("models.entity.form.help.prefix")}
|
|
/>
|
|
|
|
<InputText
|
|
className="sm:col-span-3"
|
|
name="icon"
|
|
title={t("models.entity.icon")}
|
|
// required
|
|
value={icon}
|
|
setValue={setIcon}
|
|
hint={<div className="text-muted-foreground">{t("models.entity.form.iconHint")}</div>}
|
|
button={
|
|
<div className="absolute inset-y-0 right-0 flex py-0.5 pr-0.5 ">
|
|
<kbd className="border-border text-muted-foreground bg-secondary inline-flex w-10 items-center justify-center rounded border px-1 text-center font-sans text-xs font-medium">
|
|
{icon ? (
|
|
<EntityIcon className="text-muted-foreground h-7 w-7" icon={icon} title={title} />
|
|
) : (
|
|
<QuestionMarkFilledIcon className="text-muted-foreground h-5 w-5" aria-hidden="true" />
|
|
)}
|
|
</kbd>
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
</InputGroup>
|
|
<InputGroup title={t("models.entity.form.sections.behavior")}>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
|
<InputSelect
|
|
className="sm:col-span-6"
|
|
name="onCreated"
|
|
title={t("models.entity.form.onCreated")}
|
|
value={onCreated}
|
|
setValue={(e) => setOnCreated(e?.toString() ?? "")}
|
|
options={[
|
|
{ value: "redirectToOverview", name: t("models.entity.form.onCreatedOptions.redirectToOverview") },
|
|
{ value: "redirectToEdit", name: t("models.entity.form.onCreatedOptions.redirectToEdit") },
|
|
{ value: "redirectToList", name: t("models.entity.form.onCreatedOptions.redirectToList") },
|
|
{ value: "redirectToNew", name: t("models.entity.form.onCreatedOptions.redirectToNew") },
|
|
{ value: "addAnother", name: t("models.entity.form.onCreatedOptions.addAnother") },
|
|
{ value: "redirectToGroup", name: t("models.entity.form.onCreatedOptions.redirectToGroup") },
|
|
]}
|
|
/>
|
|
<InputSelect
|
|
className="sm:col-span-6"
|
|
name="onEdit"
|
|
title={t("models.entity.form.onEdit")}
|
|
value={onEdit}
|
|
setValue={(e) => setOnEdit(e?.toString() ?? "")}
|
|
options={[
|
|
{ value: "editRoute", name: t("models.entity.form.onEditOptions.editRoute") },
|
|
{ value: "overviewRoute", name: t("models.entity.form.onEditOptions.overviewRoute") },
|
|
{ value: "overviewAlwaysEditable", name: t("models.entity.form.onEditOptions.overviewAlwaysEditable") },
|
|
]}
|
|
/>
|
|
</div>
|
|
</InputGroup>
|
|
<InputGroup title={t("shared.settings")}>
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
|
<div className="divide-border col-span-12 divide-y">
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-api"
|
|
title={t("models.entity.hasApi")}
|
|
value={hasApi}
|
|
setValue={setHasApi}
|
|
description={t("models.entity.form.descriptions.hasApi", { apiRowsUrl })}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="is-autogenerated"
|
|
title={t("models.entity.isAutogenerated")}
|
|
value={isAutogenerated}
|
|
setValue={setIsAutogenerated}
|
|
description={t("models.entity.form.descriptions.isAutogenerated")}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="active"
|
|
title={t("models.entity.active")}
|
|
value={active}
|
|
setValue={setActive}
|
|
description={t("models.entity.form.descriptions.active")}
|
|
/>
|
|
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="show-in-sidebar"
|
|
title={t("models.entity.showInSidebar")}
|
|
value={showInSidebar}
|
|
setValue={setShowInSidebar}
|
|
description={t("models.entity.form.descriptions.showInSidebar")}
|
|
/>
|
|
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-tags"
|
|
title={t("models.entity.hasTags")}
|
|
value={hasTags}
|
|
setValue={setHasTags}
|
|
description={t("models.entity.form.descriptions.hasTags")}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-comments"
|
|
title={t("models.entity.hasComments")}
|
|
value={hasComments}
|
|
setValue={setHasComments}
|
|
description={t("models.entity.form.descriptions.hasComments")}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-tasks"
|
|
title={t("models.entity.hasTasks")}
|
|
value={hasTasks}
|
|
setValue={setHasTasks}
|
|
description={t("models.entity.form.descriptions.hasTasks")}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-activity"
|
|
title={t("models.entity.hasActivity")}
|
|
value={hasActivity}
|
|
setValue={setHasActivity}
|
|
description={t("models.entity.form.descriptions.hasActivity")}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-bulk-delete"
|
|
title={t("models.entity.hasBulkDelete")}
|
|
value={hasBulkDelete}
|
|
setValue={setHasBulkDelete}
|
|
description={t("models.entity.form.descriptions.hasBulkDelete")}
|
|
/>
|
|
<InputCheckboxWithDescription
|
|
className="col-span-12"
|
|
name="has-views"
|
|
title={t("models.entity.hasViews")}
|
|
value={hasViews}
|
|
setValue={setHasViews}
|
|
description={t("models.entity.form.descriptions.hasViews")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</InputGroup>
|
|
</FormGroup>
|
|
);
|
|
}
|