init code-first mode by splitting module manager

This commit is contained in:
dswbx
2025-09-04 09:21:35 +02:00
parent c9773d49a6
commit e3888537f9
22 changed files with 768 additions and 541 deletions

View File

@@ -22,6 +22,7 @@ import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell";
import { routes, useNavigate, useRouteNavigate } from "ui/lib/routes";
import { testIds } from "ui/lib/config";
import { SchemaEditable, useBknd } from "ui/client/bknd";
export function DataRoot({ children }) {
// @todo: settings routes should be centralized
@@ -73,9 +74,11 @@ export function DataRoot({ children }) {
value={context}
onChange={handleSegmentChange}
/>
<Tooltip label="New Entity">
<IconButton Icon={TbDatabasePlus} onClick={$data.modals.createEntity} />
</Tooltip>
<SchemaEditable>
<Tooltip label="New Entity">
<IconButton Icon={TbDatabasePlus} onClick={$data.modals.createEntity} />
</Tooltip>
</SchemaEditable>
</>
}
>
@@ -254,11 +257,26 @@ export function DataEmpty() {
useBrowserTitle(["Data"]);
const [navigate] = useNavigate();
const { $data } = useBkndData();
const { readonly } = useBknd();
function handleButtonClick() {
navigate(routes.data.schema.root());
}
if (readonly) {
return (
<Empty
Icon={IconDatabase}
title="No entity selected"
description="Please select an entity from the left sidebar."
primary={{
children: "Go to schema",
onClick: handleButtonClick,
}}
/>
);
}
return (
<Empty
Icon={IconDatabase}

View File

@@ -31,6 +31,7 @@ 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();
@@ -67,29 +68,31 @@ export function DataSchemaEntity({ params }) {
>
<IconButton Icon={TbDots} />
</Dropdown>
<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>
<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"
@@ -149,6 +152,7 @@ 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() {
@@ -169,7 +173,7 @@ const Fields = ({ entity }: { entity: Entity }) => {
title="Fields"
ActiveIcon={IconAlignJustified}
renderHeaderRight={({ open }) =>
open ? (
open && !readonly ? (
<Button variant="primary" disabled={!open} onClick={handleUpdate}>
Update
</Button>
@@ -181,11 +185,12 @@ const Fields = ({ entity }: { entity: Entity }) => {
<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
sortable={!readonly}
additionalFieldTypes={fieldSpecs
.filter((f) => ["relation", "media"].includes(f.type))
.map((i) => ({
@@ -205,7 +210,7 @@ const Fields = ({ entity }: { entity: Entity }) => {
isNew={false}
/>
{isDebug() && (
{isDebug() && !readonly && (
<div>
<div className="flex flex-row gap-1 justify-center">
<Button size="small" onClick={() => setRes(ref.current?.isValid())}>
@@ -237,6 +242,7 @@ 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
@@ -264,7 +270,7 @@ const BasicSettings = ({ entity }: { entity: Entity }) => {
title="Settings"
ActiveIcon={IconSettings}
renderHeaderRight={({ open }) =>
open ? (
open && !readonly ? (
<Button variant="primary" disabled={!open} onClick={handleUpdate}>
Update
</Button>
@@ -278,6 +284,7 @@ const BasicSettings = ({ entity }: { entity: Entity }) => {
formData={_config}
onSubmit={console.log}
className="legacy hide-required-mark fieldset-alternative mute-root"
readonly={readonly}
/>
</div>
</AppShell.RouteAwareSectionHeaderAccordionItem>

View File

@@ -1,4 +1,5 @@
import { Suspense, lazy } from "react";
import { SchemaEditable } from "ui/client/bknd";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { Button } from "ui/components/buttons/Button";
import * as AppShell from "ui/layouts/AppShell/AppShell";
@@ -15,9 +16,11 @@ export function DataSchemaIndex() {
<>
<AppShell.SectionHeader
right={
<Button type="button" variant="primary" onClick={$data.modals.createAny}>
Create new
</Button>
<SchemaEditable>
<Button type="button" variant="primary" onClick={$data.modals.createAny}>
Create new
</Button>
</SchemaEditable>
}
>
Schema Overview

View File

@@ -29,6 +29,7 @@ import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelec
import type { TPrimaryFieldFormat } from "data/fields/PrimaryField";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
import { SchemaEditable } from "ui/client/bknd";
const fieldsSchemaObject = originalFieldsSchemaObject;
const fieldsSchema = s.anyOf(Object.values(fieldsSchemaObject));
@@ -64,6 +65,7 @@ export type EntityFieldsFormProps = {
routePattern?: string;
defaultPrimaryFormat?: TPrimaryFieldFormat;
isNew?: boolean;
readonly?: boolean;
};
export type EntityFieldsFormRef = {
@@ -76,7 +78,7 @@ export type EntityFieldsFormRef = {
export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsFormProps>(
function EntityFieldsForm(
{ fields: _fields, sortable, additionalFieldTypes, routePattern, isNew, ...props },
{ fields: _fields, sortable, additionalFieldTypes, routePattern, isNew, readonly, ...props },
ref,
) {
const entityFields = Object.entries(_fields).map(([name, field]) => ({
@@ -162,6 +164,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
disableIndices={[0]}
renderItem={({ dnd, ...props }, index) => (
<EntityFieldMemo
readonly={readonly}
key={props.id}
field={props as any}
index={index}
@@ -181,6 +184,7 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
<div>
{fields.map((field, index) => (
<EntityField
readonly={readonly}
key={field.id}
field={field as any}
index={index}
@@ -197,20 +201,22 @@ export const EntityFieldsForm = forwardRef<EntityFieldsFormRef, EntityFieldsForm
</div>
)}
<Popover
className="flex flex-col w-full"
target={({ toggle }) => (
<SelectType
additionalFieldTypes={additionalFieldTypes}
onSelected={toggle}
onSelect={(type) => {
handleAppend(type as any);
}}
/>
)}
>
<Button className="justify-center">Add Field</Button>
</Popover>
<SchemaEditable>
<Popover
className="flex flex-col w-full"
target={({ toggle }) => (
<SelectType
additionalFieldTypes={additionalFieldTypes}
onSelected={toggle}
onSelect={(type) => {
handleAppend(type as any);
}}
/>
)}
>
<Button className="justify-center">Add Field</Button>
</Popover>
</SchemaEditable>
</div>
</div>
</div>
@@ -288,6 +294,7 @@ function EntityField({
dnd,
routePattern,
primary,
readonly,
}: {
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
index: number;
@@ -303,6 +310,7 @@ function EntityField({
defaultFormat?: TPrimaryFieldFormat;
editable?: boolean;
};
readonly?: boolean;
}) {
const prefix = `fields.${index}.field` as const;
const type = field.field.type;
@@ -393,6 +401,7 @@ function EntityField({
<span className="text-xs text-primary/50 leading-none">Required</span>
<MantineSwitch
size="sm"
disabled={readonly}
name={`${prefix}.config.required`}
control={control}
/>
@@ -433,6 +442,7 @@ function EntityField({
<div className="flex flex-row">
<MantineSwitch
label="Required"
disabled={readonly}
name={`${prefix}.config.required`}
control={control}
/>
@@ -440,11 +450,13 @@ function EntityField({
<TextInput
label="Label"
placeholder="Label"
disabled={readonly}
{...register(`${prefix}.config.label`)}
/>
<Textarea
label="Description"
placeholder="Description"
disabled={readonly}
{...register(`${prefix}.config.description`)}
/>
{!hidden.includes("virtual") && (
@@ -452,7 +464,7 @@ function EntityField({
label="Virtual"
name={`${prefix}.config.virtual`}
control={control}
disabled={disabled.includes("virtual")}
disabled={disabled.includes("virtual") || readonly}
/>
)}
</div>
@@ -468,6 +480,7 @@ function EntityField({
...value,
});
}}
readonly={readonly}
/>
</ErrorBoundary>
</div>
@@ -478,16 +491,18 @@ function EntityField({
return <JsonViewer json={json} expand={4} />;
})()}
</Tabs.Panel>
<div className="flex flex-row justify-end">
<Button
IconLeft={TbTrash}
onClick={handleDelete(index)}
size="small"
variant="subtlered"
>
Delete
</Button>
</div>
{!readonly && (
<div className="flex flex-row justify-end">
<Button
IconLeft={TbTrash}
onClick={handleDelete(index)}
size="small"
variant="subtlered"
>
Delete
</Button>
</div>
)}
</Tabs>
</div>
)}
@@ -498,9 +513,11 @@ function EntityField({
const SpecificForm = ({
field,
onChange,
readonly,
}: {
field: FieldArrayWithId<TFieldsFormSchema, "fields", "id">;
onChange: (value: any) => void;
readonly?: boolean;
}) => {
const type = field.field.type;
const specificData = omit(field.field.config, commonProps);
@@ -513,6 +530,7 @@ const SpecificForm = ({
uiSchema={dataFieldsUiSchema.config}
className="legacy hide-required-mark fieldset-alternative mute-root"
onChange={onChange}
readonly={readonly}
/>
);
};

View File

@@ -54,7 +54,7 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
properties,
}: SettingProps<Schema>) {
const [submitting, setSubmitting] = useState(false);
const { actions } = useBknd();
const { actions, readonly } = useBknd();
const formRef = useRef<JsonSchemaFormRef>(null);
const schemaLocalModalRef = useRef<SettingsSchemaModalRef>(null);
const schemaModalRef = useRef<SettingsSchemaModalRef>(null);
@@ -120,14 +120,14 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
extractedKeys.find((key) => window.location.pathname.endsWith(key)) ?? extractedKeys[0];
const onToggleEdit = useEvent(() => {
if (!editAllowed) return;
if (!editAllowed || readonly) return;
setEditing((prev) => !prev);
//formRef.current?.cancel();
});
const onSave = useEvent(async () => {
if (!editAllowed || !editing) return;
if (!editAllowed || !editing || readonly) return;
if (formRef.current?.validateForm()) {
setSubmitting(true);
@@ -215,14 +215,14 @@ export function Setting<Schema extends s.ObjectSchema = s.ObjectSchema>({
>
<IconButton Icon={TbSettings} />
</Dropdown>
<Button onClick={onToggleEdit} disabled={!editAllowed}>
<Button onClick={onToggleEdit} disabled={!editAllowed || readonly}>
{editing ? "Cancel" : "Edit"}
</Button>
{editing && (
<Button
variant="primary"
onClick={onSave}
disabled={submitting || !editAllowed}
disabled={submitting || !editAllowed || readonly}
>
{submitting ? "Save..." : "Save"}
</Button>