mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
293 lines
11 KiB
TypeScript
293 lines
11 KiB
TypeScript
import {
|
|
IconAlignJustified,
|
|
IconBolt,
|
|
IconCirclesRelation,
|
|
IconSettings,
|
|
} from "@tabler/icons-react";
|
|
import { isDebug } from "core/env";
|
|
import type { Entity } from "bknd";
|
|
import { cloneDeep } from "lodash-es";
|
|
import { useRef, useState } from "react";
|
|
import {
|
|
TbCirclesRelation,
|
|
TbDatabasePlus,
|
|
TbDots,
|
|
TbPhoto,
|
|
TbPlus,
|
|
TbSitemap,
|
|
} from "react-icons/tb";
|
|
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
|
import { Button } from "ui/components/buttons/Button";
|
|
import { IconButton } from "ui/components/buttons/IconButton";
|
|
import { Empty } from "ui/components/display/Empty";
|
|
import { Message } from "ui/components/display/Message";
|
|
import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema";
|
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
|
import { Link } from "ui/components/wouter/Link";
|
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
|
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
|
import { routes, useNavigate } from "ui/lib/routes";
|
|
import { fieldSpecs } from "ui/modules/data/components/fields-specs";
|
|
import { extractSchema } from "../settings/utils/schema";
|
|
import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.fields.form";
|
|
import { RoutePathStateProvider } from "ui/hooks/use-route-path-state";
|
|
import { SchemaEditable, useBknd } from "ui/client/bknd";
|
|
|
|
export function DataSchemaEntity({ params }) {
|
|
const { $data } = useBkndData();
|
|
|
|
const [navigate] = useNavigate();
|
|
const entity = $data.entity(params.entity as string)!;
|
|
if (!entity) {
|
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
|
}
|
|
|
|
return (
|
|
<RoutePathStateProvider path={`/entity/${entity.name}/:setting?`} defaultIdentifier="fields">
|
|
<AppShell.SectionHeader
|
|
right={
|
|
<>
|
|
<Dropdown
|
|
items={[
|
|
{
|
|
label: "Data",
|
|
onClick: () =>
|
|
navigate(routes.data.root() + routes.data.entity.list(entity.name), {
|
|
absolute: true,
|
|
}),
|
|
},
|
|
{
|
|
label: "Advanced Settings",
|
|
onClick: () =>
|
|
navigate(routes.settings.path(["data", "entities", entity.name]), {
|
|
absolute: true,
|
|
}),
|
|
},
|
|
]}
|
|
position="bottom-end"
|
|
>
|
|
<IconButton Icon={TbDots} />
|
|
</Dropdown>
|
|
<SchemaEditable>
|
|
<Dropdown
|
|
items={[
|
|
{
|
|
icon: TbCirclesRelation,
|
|
label: "Add relation",
|
|
onClick: () => $data.modals.createRelation(entity.name),
|
|
},
|
|
{
|
|
icon: TbPhoto,
|
|
label: "Add media",
|
|
onClick: () => $data.modals.createMedia(entity.name),
|
|
},
|
|
() => <div className="h-px my-1 w-full bg-primary/5" />,
|
|
{
|
|
icon: TbDatabasePlus,
|
|
label: "Create Entity",
|
|
onClick: () => $data.modals.createEntity(),
|
|
},
|
|
]}
|
|
position="bottom-end"
|
|
>
|
|
<Button IconRight={TbPlus}>Add</Button>
|
|
</Dropdown>
|
|
</SchemaEditable>
|
|
</>
|
|
}
|
|
className="pl-3"
|
|
>
|
|
<div className="flex flex-row gap-4">
|
|
<Breadcrumbs2
|
|
path={[{ label: "Schema", href: "/" }, { label: entity.label }]}
|
|
backTo="/"
|
|
/>
|
|
<Link to="/" className="hidden md:inline">
|
|
<Button IconLeft={TbSitemap}>Overview</Button>
|
|
</Link>
|
|
</div>
|
|
</AppShell.SectionHeader>
|
|
<div className="flex flex-col h-full" key={entity.name}>
|
|
<Fields entity={entity} />
|
|
|
|
<BasicSettings entity={entity} />
|
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
|
identifier="relations"
|
|
title="Relations"
|
|
ActiveIcon={IconCirclesRelation}
|
|
>
|
|
<Empty
|
|
title="Relations"
|
|
description="This will soon be available here. Meanwhile, check advanced settings."
|
|
primary={{
|
|
children: "Advanced Settings",
|
|
onClick: () =>
|
|
navigate(routes.settings.path(["data", "relations"]), { absolute: true }),
|
|
}}
|
|
/>
|
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
|
identifier="indices"
|
|
title="Indices"
|
|
ActiveIcon={IconBolt}
|
|
>
|
|
<Empty
|
|
title="Indices"
|
|
description="This will soon be available here. Meanwhile, check advanced settings."
|
|
primary={{
|
|
children: "Advanced Settings",
|
|
onClick: () =>
|
|
navigate(routes.settings.path(["data", "indices"]), {
|
|
absolute: true,
|
|
}),
|
|
}}
|
|
/>
|
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
|
</div>
|
|
</RoutePathStateProvider>
|
|
);
|
|
}
|
|
|
|
const Fields = ({ entity }: { entity: Entity }) => {
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [updates, setUpdates] = useState(0);
|
|
const { actions, $data, config } = useBkndData();
|
|
const { readonly } = useBknd();
|
|
const [res, setRes] = useState<any>();
|
|
const ref = useRef<EntityFieldsFormRef>(null);
|
|
async function handleUpdate() {
|
|
if (submitting) return;
|
|
setSubmitting(true);
|
|
const fields = ref.current?.getData()!;
|
|
await actions.entity.patch(entity.name).fields.set(fields);
|
|
setSubmitting(false);
|
|
setUpdates((u) => u + 1);
|
|
}
|
|
|
|
// @todo: the return of toJSON from Fields doesn't match "type" enum
|
|
const initialFields = Object.fromEntries(entity.fields.map((f) => [f.name, f.toJSON()])) as any;
|
|
|
|
return (
|
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
|
identifier="fields"
|
|
title="Fields"
|
|
ActiveIcon={IconAlignJustified}
|
|
renderHeaderRight={({ open }) =>
|
|
open && !readonly ? (
|
|
<Button variant="primary" disabled={!open} onClick={handleUpdate}>
|
|
Update
|
|
</Button>
|
|
) : null
|
|
}
|
|
>
|
|
<div className="flex flex-col flex-grow py-3 px-4 max-w-4xl gap-3 relative">
|
|
{submitting && (
|
|
<div className="animate-fade-in absolute w-full h-full top-0 bottom-0 left-0 right-0 bg-background/65 z-50" />
|
|
)}
|
|
<EntityFieldsForm
|
|
readonly={readonly}
|
|
routePattern={`/entity/${entity.name}/fields/:sub?`}
|
|
fields={initialFields}
|
|
ref={ref}
|
|
key={String(updates)}
|
|
sortable={!readonly}
|
|
additionalFieldTypes={fieldSpecs
|
|
.filter((f) => ["relation", "media"].includes(f.type))
|
|
.map((i) => ({
|
|
...i,
|
|
onClick: () => {
|
|
switch (i.type) {
|
|
case "relation":
|
|
$data.modals.createRelation(entity.name);
|
|
break;
|
|
case "media":
|
|
$data.modals.createMedia(entity.name);
|
|
break;
|
|
}
|
|
},
|
|
}))}
|
|
defaultPrimaryFormat={config?.default_primary_format}
|
|
isNew={false}
|
|
/>
|
|
|
|
{isDebug() && !readonly && (
|
|
<div>
|
|
<div className="flex flex-row gap-1 justify-center">
|
|
<Button size="small" onClick={() => setRes(ref.current?.isValid())}>
|
|
valid
|
|
</Button>
|
|
<Button size="small" onClick={() => setRes(ref.current?.getValues())}>
|
|
values
|
|
</Button>
|
|
<Button size="small" onClick={() => setRes(ref.current?.getData())}>
|
|
data
|
|
</Button>
|
|
<Button size="small" onClick={() => setRes(ref.current?.getErrors())}>
|
|
errors
|
|
</Button>
|
|
<Button size="small" onClick={handleUpdate}>
|
|
update
|
|
</Button>
|
|
</div>
|
|
|
|
<pre className="select-text">{JSON.stringify(res, null, 2)}</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
|
);
|
|
};
|
|
|
|
const BasicSettings = ({ entity }: { entity: Entity }) => {
|
|
const d = useBkndData();
|
|
const config = d.entities?.[entity.name]?.config;
|
|
const formRef = useRef<JsonSchemaFormRef>(null);
|
|
const { readonly } = useBknd();
|
|
|
|
const schema = cloneDeep(
|
|
// @ts-ignore
|
|
d.schema.properties.entities.additionalProperties?.properties?.config,
|
|
);
|
|
|
|
const [_schema, _config] = extractSchema(schema as any, config, ["fields"]);
|
|
|
|
// set fields as enum
|
|
try {
|
|
// @ts-ignore
|
|
_schema.properties.sort_field.enum = entity.getFields().map((f) => f.name);
|
|
} catch (e) {
|
|
console.error("error setting sort_field enum", e);
|
|
}
|
|
|
|
async function handleUpdate() {
|
|
console.log("update", formRef.current?.formData());
|
|
await d.actions.entity.patch(entity.name).config(formRef.current?.formData());
|
|
}
|
|
|
|
return (
|
|
<AppShell.RouteAwareSectionHeaderAccordionItem
|
|
identifier="settings"
|
|
title="Settings"
|
|
ActiveIcon={IconSettings}
|
|
renderHeaderRight={({ open }) =>
|
|
open && !readonly ? (
|
|
<Button variant="primary" disabled={!open} onClick={handleUpdate}>
|
|
Update
|
|
</Button>
|
|
) : null
|
|
}
|
|
>
|
|
<div className="flex flex-col flex-grow py-3 px-4 max-w-4xl gap-3 relative">
|
|
<JsonSchemaForm
|
|
ref={formRef}
|
|
schema={_schema}
|
|
formData={_config}
|
|
onSubmit={console.log}
|
|
className="legacy hide-required-mark fieldset-alternative mute-root"
|
|
readonly={readonly}
|
|
/>
|
|
</div>
|
|
</AppShell.RouteAwareSectionHeaderAccordionItem>
|
|
);
|
|
};
|