Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/entities/EntityForm.tsx
T

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>
);
}