mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
init code-first mode by splitting module manager
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import type { ModuleConfigs, ModuleSchemas } from "modules";
|
||||
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
||||
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
|
||||
import { getDefaultConfig, getDefaultSchema } from "modules/manager/ModuleManager";
|
||||
import {
|
||||
createContext,
|
||||
startTransition,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useApi } from "ui/client";
|
||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||
import { AppReduced } from "./utils/AppReduced";
|
||||
@@ -15,6 +23,7 @@ export type BkndAdminOptions = {
|
||||
};
|
||||
type BkndContext = {
|
||||
version: number;
|
||||
readonly: boolean;
|
||||
schema: ModuleSchemas;
|
||||
config: ModuleConfigs;
|
||||
permissions: string[];
|
||||
@@ -48,7 +57,12 @@ export function BkndProvider({
|
||||
}) {
|
||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
||||
const [schema, setSchema] =
|
||||
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions" | "fallback">>();
|
||||
useState<
|
||||
Pick<
|
||||
BkndContext,
|
||||
"version" | "schema" | "config" | "permissions" | "fallback" | "readonly"
|
||||
>
|
||||
>();
|
||||
const [fetched, setFetched] = useState(false);
|
||||
const [error, setError] = useState<boolean>();
|
||||
const errorShown = useRef<boolean>(false);
|
||||
@@ -97,6 +111,7 @@ export function BkndProvider({
|
||||
? res.body
|
||||
: ({
|
||||
version: 0,
|
||||
mode: "db",
|
||||
schema: getDefaultSchema(),
|
||||
config: getDefaultConfig(),
|
||||
permissions: [],
|
||||
@@ -173,3 +188,8 @@ export function useBkndOptions(): BkndAdminOptions {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function SchemaEditable({ children }: { children: ReactNode }) {
|
||||
const { readonly } = useBknd();
|
||||
return !readonly ? children : null;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { BkndProvider, type BkndAdminOptions, useBknd } from "./BkndProvider";
|
||||
export { BkndProvider, type BkndAdminOptions, useBknd, SchemaEditable } from "./BkndProvider";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user