Merge pull request #265 from bknd-io/feat/admin-ui-customizations

feat: add admin options for entities and app shell
This commit is contained in:
dswbx
2025-09-19 20:04:07 +02:00
committed by GitHub
17 changed files with 733 additions and 35 deletions

View File

@@ -11,10 +11,12 @@ export interface AppEntity<IdType = number | string> {
export interface DB { export interface DB {
// make sure to make unknown as "any" // make sure to make unknown as "any"
[key: string]: { /* [key: string]: {
id: PrimaryFieldType; id: PrimaryFieldType;
[key: string]: any; [key: string]: any;
}; }; */
// @todo: that's not good, but required for admin options
[key: string]: any;
} }
export const config = { export const config = {

View File

@@ -42,6 +42,9 @@ export class DataApi extends ModuleApi<DataApiOptions> {
) { ) {
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData; type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
type T = RepositoryResultJSON<Data>; type T = RepositoryResultJSON<Data>;
// @todo: if none found, still returns meta...
return this.readMany(entity, { return this.readMany(entity, {
...query, ...query,
limit: 1, limit: 1,

View File

@@ -1,24 +1,60 @@
import { MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import React, { type ReactNode } from "react"; import React, { type ReactNode } from "react";
import { BkndProvider, type BkndAdminOptions } from "ui/client/bknd"; import { BkndProvider } from "ui/client/bknd";
import { useTheme } from "ui/client/use-theme"; import { useTheme, type AppTheme } from "ui/client/use-theme";
import { Logo } from "ui/components/display/Logo"; import { Logo } from "ui/components/display/Logo";
import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as AppShell from "ui/layouts/AppShell/AppShell";
import { ClientProvider, useBkndWindowContext, type ClientProviderProps } from "./client"; import { ClientProvider, useBkndWindowContext, type ClientProviderProps } from "./client";
import { createMantineTheme } from "./lib/mantine/theme"; import { createMantineTheme } from "./lib/mantine/theme";
import { Routes } from "./routes"; import { Routes } from "./routes";
import type { BkndAdminAppShellOptions, BkndAdminEntitiesOptions } from "ui/options";
export type BkndAdminProps = { export type BkndAdminProps = {
/**
* Base URL of the API, only needed if you are not using the `withProvider` prop
*/
baseUrl?: string; baseUrl?: string;
/**
* Whether to wrap Admin in a <ClientProvider />
*/
withProvider?: boolean | ClientProviderProps; withProvider?: boolean | ClientProviderProps;
config?: BkndAdminOptions; /**
* Admin UI customization options
*/
config?: {
/**
* Base path of the Admin UI
* @default `/`
*/
basepath?: string;
/**
* Path to return to when clicking the logo
* @default `/`
*/
logo_return_path?: string;
/**
* Theme of the Admin UI
* @default `system`
*/
theme?: AppTheme;
/**
* Entities configuration like headers, footers, actions, field renders, etc.
*/
entities?: BkndAdminEntitiesOptions;
/**
* App shell configuration like user menu actions.
*/
appShell?: BkndAdminAppShellOptions;
};
children?: ReactNode;
}; };
export default function Admin({ export default function Admin({
baseUrl: baseUrlOverride, baseUrl: baseUrlOverride,
withProvider = false, withProvider = false,
config: _config = {}, config: _config = {},
children,
}: BkndAdminProps) { }: BkndAdminProps) {
const { theme } = useTheme(); const { theme } = useTheme();
const Provider = ({ children }: any) => const Provider = ({ children }: any) =>
@@ -47,7 +83,9 @@ export default function Admin({
<Provider> <Provider>
<MantineProvider {...createMantineTheme(theme as any)}> <MantineProvider {...createMantineTheme(theme as any)}>
<Notifications position="top-right" /> <Notifications position="top-right" />
<Routes BkndWrapper={BkndWrapper} basePath={config?.basepath} /> <Routes BkndWrapper={BkndWrapper} basePath={config?.basepath}>
{children}
</Routes>
</MantineProvider> </MantineProvider>
</Provider> </Provider>
); );

View File

@@ -12,16 +12,11 @@ import {
import { useApi } from "ui/client"; import { useApi } from "ui/client";
import { type TSchemaActions, getSchemaActions } from "./schema/actions"; import { type TSchemaActions, getSchemaActions } from "./schema/actions";
import { AppReduced } from "./utils/AppReduced"; import { AppReduced } from "./utils/AppReduced";
import type { AppTheme } from "ui/client/use-theme";
import { Message } from "ui/components/display/Message"; import { Message } from "ui/components/display/Message";
import { useNavigate } from "ui/lib/routes"; import { useNavigate } from "ui/lib/routes";
import type { BkndAdminProps } from "ui/Admin";
export type BkndAdminOptions = { export type BkndContext = {
basepath?: string;
logo_return_path?: string;
theme?: AppTheme;
};
type BkndContext = {
version: number; version: number;
readonly: boolean; readonly: boolean;
schema: ModuleSchemas; schema: ModuleSchemas;
@@ -31,7 +26,7 @@ type BkndContext = {
requireSecrets: () => Promise<void>; requireSecrets: () => Promise<void>;
actions: ReturnType<typeof getSchemaActions>; actions: ReturnType<typeof getSchemaActions>;
app: AppReduced; app: AppReduced;
options: BkndAdminOptions; options: BkndAdminProps["config"];
fallback: boolean; fallback: boolean;
}; };
@@ -53,7 +48,7 @@ export function BkndProvider({
includeSecrets?: boolean; includeSecrets?: boolean;
children: any; children: any;
fallback?: React.ReactNode; fallback?: React.ReactNode;
options?: BkndAdminOptions; options?: BkndAdminProps["config"];
}) { }) {
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets); const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
const [schema, setSchema] = const [schema, setSchema] =
@@ -180,7 +175,7 @@ export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndCo
return ctx; return ctx;
} }
export function useBkndOptions(): BkndAdminOptions { export function useBkndOptions(): BkndAdminProps["config"] {
const ctx = useContext(BkndContext); const ctx = useContext(BkndContext);
return ( return (
ctx.options ?? { ctx.options ?? {

View File

@@ -1 +1 @@
export { BkndProvider, type BkndAdminOptions, useBknd, SchemaEditable } from "./BkndProvider"; export { BkndProvider, type BkndContext, useBknd, SchemaEditable } from "./BkndProvider";

View File

@@ -4,7 +4,7 @@ import type { EntityRelation } from "data/relations";
import { constructEntity, constructRelation } from "data/schema/constructor"; import { constructEntity, constructRelation } from "data/schema/constructor";
import { RelationAccessor } from "data/relations/RelationAccessor"; import { RelationAccessor } from "data/relations/RelationAccessor";
import { Flow, TaskMap } from "flows"; import { Flow, TaskMap } from "flows";
import type { BkndAdminOptions } from "ui/client/BkndProvider"; import type { BkndAdminProps } from "ui/Admin";
export type AppType = ReturnType<App["toJSON"]>; export type AppType = ReturnType<App["toJSON"]>;
@@ -20,7 +20,7 @@ export class AppReduced {
constructor( constructor(
protected appJson: AppType, protected appJson: AppType,
protected _options: BkndAdminOptions = {}, protected _options: BkndAdminProps["config"] = {},
) { ) {
//console.log("received appjson", appJson); //console.log("received appjson", appJson);

View File

@@ -1,6 +1,7 @@
export { default as Admin, type BkndAdminProps } from "./Admin"; export { default as Admin, type BkndAdminProps } from "./Admin";
export * from "./components/form/json-schema-form"; export * from "./components/form/json-schema-form";
export { JsonViewer } from "./components/code/JsonViewer"; export { JsonViewer } from "./components/code/JsonViewer";
export type * from "./options";
// bknd admin ui // bknd admin ui
export { Button } from "./components/buttons/Button"; export { Button } from "./components/buttons/Button";

View File

@@ -16,7 +16,7 @@ import { useTheme } from "ui/client/use-theme";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton"; import { IconButton } from "ui/components/buttons/IconButton";
import { Logo } from "ui/components/display/Logo"; import { Logo } from "ui/components/display/Logo";
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown"; import { Dropdown, type DropdownProps } from "ui/components/overlay/Dropdown";
import { Link } from "ui/components/wouter/Link"; import { Link } from "ui/components/wouter/Link";
import { useEvent } from "ui/hooks/use-event"; import { useEvent } from "ui/hooks/use-event";
import { useNavigate } from "ui/lib/routes"; import { useNavigate } from "ui/lib/routes";
@@ -26,6 +26,7 @@ import { autoFormatString } from "core/utils";
import { appShellStore } from "ui/store"; import { appShellStore } from "ui/store";
import { getVersion } from "core/env"; import { getVersion } from "core/env";
import { McpIcon } from "ui/routes/tools/mcp/components/mcp-icon"; import { McpIcon } from "ui/routes/tools/mcp/components/mcp-icon";
import { useAppShellAdminOptions } from "ui/options";
export function HeaderNavigation() { export function HeaderNavigation() {
const [location, navigate] = useLocation(); const [location, navigate] = useLocation();
@@ -144,7 +145,9 @@ export function Header({ hasSidebar = true }) {
} }
function UserMenu() { function UserMenu() {
const { config, options } = useBknd(); const { config } = useBknd();
const uiOptions = useAppShellAdminOptions();
const auth = useAuth(); const auth = useAuth();
const [navigate] = useNavigate(); const [navigate] = useNavigate();
const { logout_route } = useBkndWindowContext(); const { logout_route } = useBkndWindowContext();
@@ -159,7 +162,8 @@ function UserMenu() {
navigate("/auth/login"); navigate("/auth/login");
} }
const items: DropdownItem[] = [ const items: DropdownProps["items"] = [
...(uiOptions.userMenu ?? []),
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings }, { label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
{ {
label: "OpenAPI", label: "OpenAPI",

View File

@@ -16,6 +16,7 @@ import { Alert } from "ui/components/display/Alert";
import { bkndModals } from "ui/modals"; import { bkndModals } from "ui/modals";
import type { EnumField, JsonField, JsonSchemaField } from "data/fields"; import type { EnumField, JsonField, JsonSchemaField } from "data/fields";
import type { RelationField } from "data/relations"; import type { RelationField } from "data/relations";
import { useEntityAdminOptions } from "ui/options";
// simplify react form types 🤦 // simplify react form types 🤦
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>; export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
@@ -44,6 +45,7 @@ export function EntityForm({
action, action,
}: EntityFormProps) { }: EntityFormProps) {
const fields = entity.getFillableFields(action, true); const fields = entity.getFillableFields(action, true);
const options = useEntityAdminOptions(entity, action);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -107,16 +109,29 @@ export function EntityForm({
> >
<Form.Field <Form.Field
name={field.name} name={field.name}
children={(props) => ( children={(props) => {
<EntityFormField const fieldOptions = options.field(field.name);
field={field} if (fieldOptions?.render) {
fieldApi={props} const custom = fieldOptions.render(action, entity, field, {
disabled={fieldsDisabled} handleChange: props.handleChange,
tabIndex={key + 1} value: props.state.value,
action={action} data,
data={data} });
/> if (custom) {
)} return custom;
}
}
return (
<EntityFormField
field={field}
fieldApi={props}
disabled={fieldsDisabled}
tabIndex={key + 1}
action={action}
data={data}
/>
);
}}
/> />
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -0,0 +1,12 @@
import { useBknd } from "ui/client/BkndProvider";
import type { DropdownProps } from "ui/components/overlay/Dropdown";
export type BkndAdminAppShellOptions = {
userMenu?: DropdownProps["items"];
};
export function useAppShellAdminOptions() {
const { options } = useBknd();
const userMenu = options?.appShell?.userMenu ?? [];
return { userMenu };
}

View File

@@ -0,0 +1,85 @@
import type { DB, Field } from "bknd";
import type { ReactNode } from "react";
import type { Entity } from "data/entities";
import { useBknd } from "ui/client/BkndProvider";
import type { DropdownProps } from "ui/components/overlay/Dropdown";
import type { ButtonProps } from "ui/components/buttons/Button";
export type BkndAdminEntityContext = "list" | "create" | "update";
export type BkndAdminEntitiesOptions = {
[E in keyof DB]?: BkndAdminEntityOptions<E>;
};
export type BkndAdminEntityOptions<E extends keyof DB | string> = {
/**
* Header to be rendered depending on the context
*/
header?: (
context: BkndAdminEntityContext,
entity: Entity,
data?: DB[E],
) => ReactNode | void | undefined;
/**
* Footer to be rendered depending on the context
*/
footer?: (
context: BkndAdminEntityContext,
entity: Entity,
data?: DB[E],
) => ReactNode | void | undefined;
/**
* Actions to be rendered depending on the context
*/
actions?: (
context: BkndAdminEntityContext,
entity: Entity,
data?: DB[E],
) => {
/**
* Primary actions are always visible
*/
primary?: (ButtonProps | undefined | null | false)[];
/**
* Context actions are rendered in a dropdown
*/
context?: DropdownProps["items"];
};
/**
* Field UI overrides
*/
fields?: {
[F in keyof DB[E]]?: BkndAdminEntityFieldOptions<E>;
};
};
export type BkndAdminEntityFieldOptions<E extends keyof DB | string> = {
/**
* Override the rendering of a certain field
*/
render?: (
context: BkndAdminEntityContext,
entity: Entity,
field: Field,
ctx: {
data?: DB[E];
value?: DB[E][keyof DB[E]];
handleChange: (value: any) => void;
},
) => ReactNode | void | undefined;
};
export function useEntityAdminOptions(entity: Entity, context: BkndAdminEntityContext, data?: any) {
const b = useBknd();
const opts = b.options?.entities?.[entity.name];
const footer = opts?.footer?.(context, entity, data) ?? null;
const header = opts?.header?.(context, entity, data) ?? null;
const actions = opts?.actions?.(context, entity, data);
return {
footer,
header,
field: (name: string) => opts?.fields?.[name],
actions,
};
}

View File

@@ -0,0 +1,2 @@
export * from "./entities";
export * from "./app-shell";

View File

@@ -18,6 +18,7 @@ import { EntityForm } from "ui/modules/data/components/EntityForm";
import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useEntityAdminOptions } from "ui/options";
export function DataEntityUpdate({ params }) { export function DataEntityUpdate({ params }) {
return <DataEntityUpdateImpl params={params} key={params.entity} />; return <DataEntityUpdateImpl params={params} key={params.entity} />;
@@ -53,6 +54,7 @@ function DataEntityUpdateImpl({ params }) {
}, },
); );
const options = useEntityAdminOptions(entity, "update", $q.data);
const backHref = routes.data.entity.list(entity.name); const backHref = routes.data.entity.list(entity.name);
const goBack = () => _goBack({ fallback: backHref }); const goBack = () => _goBack({ fallback: backHref });
@@ -125,6 +127,7 @@ function DataEntityUpdateImpl({ params }) {
<Dropdown <Dropdown
position="bottom-end" position="bottom-end"
items={[ items={[
...(options.actions?.context ?? []),
{ {
label: "Inspect", label: "Inspect",
onClick: () => { onClick: () => {
@@ -156,6 +159,10 @@ function DataEntityUpdateImpl({ params }) {
> >
<IconButton Icon={TbDots} /> <IconButton Icon={TbDots} />
</Dropdown> </Dropdown>
{options.actions?.primary?.map(
(button, key) =>
button && <Button variant="primary" {...button} type="button" key={key} />,
)}
<Form.Subscribe <Form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]} selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => ( children={([canSubmit, isSubmitting]) => (
@@ -185,6 +192,7 @@ function DataEntityUpdateImpl({ params }) {
</div> </div>
) : ( ) : (
<AppShell.Scrollable> <AppShell.Scrollable>
{options.header}
{error && ( {error && (
<div className="flex flex-row dark:bg-red-950 bg-red-100 p-4"> <div className="flex flex-row dark:bg-red-950 bg-red-100 p-4">
<b className="mr-2">Update failed: </b> {error} <b className="mr-2">Update failed: </b> {error}
@@ -200,6 +208,8 @@ function DataEntityUpdateImpl({ params }) {
action="update" action="update"
className="flex flex-grow flex-col gap-3 p-3" className="flex flex-grow flex-col gap-3 p-3"
/> />
{options.footer}
{targetRelations.length > 0 ? ( {targetRelations.length > 0 ? (
<EntityDetailRelations <EntityDetailRelations
id={entityId} id={entityId}
@@ -247,7 +257,8 @@ function EntityDetailRelations({
return { return {
as: "button", as: "button",
type: "button", type: "button",
label: ucFirst(other.entity.label), //label: ucFirst(other.entity.label),
label: ucFirst(other.reference),
onClick: () => handleClick(relation), onClick: () => handleClick(relation),
active: selected?.other(entity).reference === other.reference, active: selected?.other(entity).reference === other.reference,
badge: relation.type(), badge: relation.type(),

View File

@@ -13,6 +13,10 @@ import { EntityForm } from "ui/modules/data/components/EntityForm";
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
import { s } from "bknd/utils"; import { s } from "bknd/utils";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useEntityAdminOptions } from "ui/options";
import { Dropdown } from "ui/components/overlay/Dropdown";
import { TbDots } from "react-icons/tb";
import { IconButton } from "ui/components/buttons/IconButton";
export function DataEntityCreate({ params }) { export function DataEntityCreate({ params }) {
const { $data } = useBkndData(); const { $data } = useBkndData();
@@ -23,6 +27,7 @@ export function DataEntityCreate({ params }) {
} else if (entity.type === "system") { } else if (entity.type === "system") {
return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />; return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />;
} }
const options = useEntityAdminOptions(entity, "create");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useBrowserTitle(["Data", entity.label, "Create"]); useBrowserTitle(["Data", entity.label, "Create"]);
@@ -71,7 +76,16 @@ export function DataEntityCreate({ params }) {
<AppShell.SectionHeader <AppShell.SectionHeader
right={ right={
<> <>
{options.actions?.context && (
<Dropdown position="bottom-end" items={options.actions.context}>
<IconButton Icon={TbDots} />
</Dropdown>
)}
<Button onClick={goBack}>Cancel</Button> <Button onClick={goBack}>Cancel</Button>
{options.actions?.primary?.map(
(button, key) =>
button && <Button {...button} type="button" key={key} variant="primary" />,
)}
<Form.Subscribe <Form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]} selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => ( children={([canSubmit, isSubmitting]) => (
@@ -96,6 +110,7 @@ export function DataEntityCreate({ params }) {
/> />
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable key={entity.name}> <AppShell.Scrollable key={entity.name}>
{options.header}
{error && ( {error && (
<div className="flex flex-row dark:bg-red-950 bg-red-100 p-4"> <div className="flex flex-row dark:bg-red-950 bg-red-100 p-4">
<b className="mr-2">Create failed: </b> {error} <b className="mr-2">Create failed: </b> {error}
@@ -110,6 +125,7 @@ export function DataEntityCreate({ params }) {
action="create" action="create"
className="flex flex-grow flex-col gap-3 p-3" className="flex flex-grow flex-col gap-3 p-3"
/> />
{options.footer}
</AppShell.Scrollable> </AppShell.Scrollable>
</> </>
); );

View File

@@ -17,6 +17,7 @@ import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal"
import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
import { s } from "bknd/utils"; import { s } from "bknd/utils";
import { pick } from "core/utils/objects"; import { pick } from "core/utils/objects";
import { useEntityAdminOptions } from "ui/options";
const searchSchema = s.partialObject({ const searchSchema = s.partialObject({
...pick(repoQuery.properties, ["select", "where", "sort"]), ...pick(repoQuery.properties, ["select", "where", "sort"]),
@@ -36,6 +37,7 @@ function DataEntityListImpl({ params }) {
if (!entity) { if (!entity) {
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />; return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
} }
const options = useEntityAdminOptions(entity, "list");
useBrowserTitle(["Data", entity?.label ?? params.entity]); useBrowserTitle(["Data", entity?.label ?? params.entity]);
const [navigate] = useNavigate(); const [navigate] = useNavigate();
@@ -100,6 +102,7 @@ function DataEntityListImpl({ params }) {
<> <>
<Dropdown <Dropdown
items={[ items={[
...(options.actions?.context ?? []),
{ {
label: "Settings", label: "Settings",
onClick: () => navigate(routes.data.schema.entity(entity.name)), onClick: () => navigate(routes.data.schema.entity(entity.name)),
@@ -120,6 +123,10 @@ function DataEntityListImpl({ params }) {
> >
<IconButton Icon={TbDots} /> <IconButton Icon={TbDots} />
</Dropdown> </Dropdown>
{options.actions?.primary?.map(
(button, key) =>
button && <Button variant="primary" {...button} type="button" key={key} />,
)}
<EntityCreateButton entity={entity} /> <EntityCreateButton entity={entity} />
</> </>
} }
@@ -127,6 +134,7 @@ function DataEntityListImpl({ params }) {
<AppShell.SectionHeaderTitle>{entity.label}</AppShell.SectionHeaderTitle> <AppShell.SectionHeaderTitle>{entity.label}</AppShell.SectionHeaderTitle>
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable key={entity.name}> <AppShell.Scrollable key={entity.name}>
{options.header}
<div className="flex flex-col flex-grow p-3 gap-3"> <div className="flex flex-col flex-grow p-3 gap-3">
{/*<div className="w-64"> {/*<div className="w-64">
<SearchInput placeholder={`Filter ${entity.label}`} /> <SearchInput placeholder={`Filter ${entity.label}`} />
@@ -134,7 +142,7 @@ function DataEntityListImpl({ params }) {
<div <div
data-updating={isUpdating ? 1 : undefined} data-updating={isUpdating ? 1 : undefined}
className="data-[updating]:opacity-50 transition-opacity pb-10" className="data-[updating]:opacity-50 transition-opacity"
> >
<EntityTable2 <EntityTable2
data={data ?? null} data={data ?? null}
@@ -152,6 +160,7 @@ function DataEntityListImpl({ params }) {
/> />
</div> </div>
</div> </div>
{options.footer}
</AppShell.Scrollable> </AppShell.Scrollable>
</Fragment> </Fragment>
); );

View File

@@ -20,7 +20,12 @@ const TestRoutes = lazy(() => import("./test"));
export function Routes({ export function Routes({
BkndWrapper, BkndWrapper,
basePath = "", basePath = "",
}: { BkndWrapper: ComponentType<{ children: ReactNode }>; basePath?: string }) { children,
}: {
BkndWrapper: ComponentType<{ children: ReactNode }>;
basePath?: string;
children?: ReactNode;
}) {
const { theme } = useTheme(); const { theme } = useTheme();
const ctx = useBkndWindowContext(); const ctx = useBkndWindowContext();
const actualBasePath = basePath || ctx.admin_basepath; const actualBasePath = basePath || ctx.admin_basepath;
@@ -44,6 +49,8 @@ export function Routes({
</Suspense> </Suspense>
</Route> </Route>
{children}
<Route path="/" component={RootEmpty} /> <Route path="/" component={RootEmpty} />
<Route path="/data" nest> <Route path="/data" nest>
<Suspense fallback={null}> <Suspense fallback={null}>

498
docs/bknd_ui_in_docs.patch Normal file
View File

@@ -0,0 +1,498 @@
Subject: [PATCH] bknd ui in docs
---
Index: docs/package.json
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/package.json b/docs/package.json
--- a/docs/package.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
+++ b/docs/package.json (date 1758275461936)
@@ -32,7 +32,8 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
- "twoslash": "^0.3.2"
+ "twoslash": "^0.3.2",
+ "bknd": "file:../app"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
Index: docs/package-lock.json
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/package-lock.json b/docs/package-lock.json
--- a/docs/package-lock.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
+++ b/docs/package-lock.json (date 1758275464962)
@@ -10,6 +10,7 @@
"dependencies": {
"@iconify/react": "^6.0.0",
"@orama/orama": "^3.1.10",
+ "bknd": "file:../app",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "^15.6.1",
@@ -44,6 +45,110 @@
"wrangler": "^4.25.1"
}
},
+ "../app": {
+ "name": "bknd",
+ "version": "0.18.0-rc.3",
+ "license": "FSL-1.1-MIT",
+ "dependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "@codemirror/lang-html": "^6.4.9",
+ "@codemirror/lang-json": "^6.0.1",
+ "@hello-pangea/dnd": "^18.0.1",
+ "@hono/swagger-ui": "^0.5.1",
+ "@mantine/core": "^7.17.1",
+ "@mantine/hooks": "^7.17.1",
+ "@tanstack/react-form": "^1.0.5",
+ "@uiw/react-codemirror": "^4.23.10",
+ "@xyflow/react": "^12.4.4",
+ "aws4fetch": "^1.0.20",
+ "bcryptjs": "^3.0.2",
+ "dayjs": "^1.11.13",
+ "fast-xml-parser": "^5.0.8",
+ "hono": "4.8.3",
+ "json-schema-library": "10.0.0-rc7",
+ "json-schema-to-ts": "^3.1.1",
+ "jsonv-ts": "0.8.2",
+ "kysely": "0.27.6",
+ "lodash-es": "^4.17.21",
+ "oauth4webapi": "^2.11.1",
+ "object-path-immutable": "^4.1.2",
+ "picocolors": "^1.1.1",
+ "radix-ui": "^1.1.3",
+ "swr": "^2.3.3"
+ },
+ "bin": {
+ "bknd": "dist/cli/index.js"
+ },
+ "devDependencies": {
+ "@aws-sdk/client-s3": "^3.758.0",
+ "@bluwy/giget-core": "^0.1.2",
+ "@clack/prompts": "^0.11.0",
+ "@cloudflare/vitest-pool-workers": "^0.8.38",
+ "@cloudflare/workers-types": "^4.20250606.0",
+ "@dagrejs/dagre": "^1.1.4",
+ "@hono/vite-dev-server": "^0.19.1",
+ "@hookform/resolvers": "^4.1.3",
+ "@libsql/client": "^0.15.9",
+ "@mantine/modals": "^7.17.1",
+ "@mantine/notifications": "^7.17.1",
+ "@playwright/test": "^1.51.1",
+ "@rjsf/core": "5.22.2",
+ "@standard-schema/spec": "^1.0.0",
+ "@tabler/icons-react": "3.18.0",
+ "@tailwindcss/postcss": "^4.0.12",
+ "@tailwindcss/vite": "^4.0.12",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
+ "@types/node": "^22.13.10",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "@vitest/coverage-v8": "^3.0.9",
+ "autoprefixer": "^10.4.21",
+ "clsx": "^2.1.1",
+ "dotenv": "^16.4.7",
+ "jotai": "^2.12.2",
+ "jsdom": "^26.0.0",
+ "kysely-d1": "^0.3.0",
+ "kysely-generic-sqlite": "^1.2.1",
+ "libsql-stateless-easy": "^1.8.0",
+ "open": "^10.1.0",
+ "openapi-types": "^12.1.3",
+ "postcss": "^8.5.3",
+ "postcss-preset-mantine": "^1.17.0",
+ "postcss-simple-vars": "^7.0.1",
+ "posthog-js-lite": "^3.4.2",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.2",
+ "react-icons": "5.2.1",
+ "react-json-view-lite": "^2.4.1",
+ "sql-formatter": "^15.4.11",
+ "tailwind-merge": "^3.0.2",
+ "tailwindcss": "^4.0.12",
+ "tailwindcss-animate": "^1.0.7",
+ "tsc-alias": "^1.8.11",
+ "tsup": "^8.4.0",
+ "tsx": "^4.19.3",
+ "uuid": "^11.1.0",
+ "vite": "^6.3.5",
+ "vite-plugin-circular-dependency": "^0.5.0",
+ "vite-tsconfig-paths": "^5.1.4",
+ "vitest": "^3.0.9",
+ "wouter": "^3.6.0",
+ "wrangler": "^4.37.1"
+ },
+ "engines": {
+ "node": ">=22.13"
+ },
+ "optionalDependencies": {
+ "@hono/node-server": "^1.14.3"
+ },
+ "peerDependencies": {
+ "react": ">=19",
+ "react-dom": ">=19"
+ }
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -4363,6 +4468,10 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/bknd": {
+ "resolved": "../app",
+ "link": true
+ },
"node_modules/blake3-wasm": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
Index: docs/content/docs/(documentation)/meta.json
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/content/docs/(documentation)/meta.json b/docs/content/docs/(documentation)/meta.json
--- a/docs/content/docs/(documentation)/meta.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
+++ b/docs/content/docs/(documentation)/meta.json (date 1758275212528)
@@ -20,6 +20,7 @@
"./extending/config",
"./extending/events",
"./extending/plugins",
+ "./extending/admin",
"---Integration---",
"./integration/introduction",
"./integration/(frameworks)/",
Index: docs/content/docs/(documentation)/extending/admin.mdx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/content/docs/(documentation)/extending/admin.mdx b/docs/content/docs/(documentation)/extending/admin.mdx
new file mode 100644
--- /dev/null (date 1758281513770)
+++ b/docs/content/docs/(documentation)/extending/admin.mdx (date 1758281513770)
@@ -0,0 +1,56 @@
+---
+title: Admin UI
+tags: ["documentation"]
+---
+import { TypeTable } from 'fumadocs-ui/components/type-table';
+import { Button } from "bknd-ui"
+import { BkndUi } from "@/app/_components/BkndUi";
+
+Describe how to extend the Admin UI.
+
+<BkndUi code={`import { Button } from "bknd/ui"
+
+function ButtonDemo() {
+ return <div className="flex flex-row gap-4">
+ <Button variant="primary">
+ Primary
+ </Button>
+ <Button variant="outline">
+ Outline
+ </Button>
+ <Button variant="ghost">
+ Ghost
+ </Button>
+ <Button variant="red">
+ Red
+ </Button>
+ <Button variant="subtlered">
+ Subtlered
+ </Button>
+ </div>
+}`}>
+ <div className="flex flex-row gap-4">
+ <Button variant="primary">
+ Primary
+ </Button>
+ <Button variant="outline">
+ Outline
+ </Button>
+ <Button variant="ghost">
+ Ghost
+ </Button>
+ <Button variant="red">
+ Red
+ </Button>
+ <Button variant="subtlered">
+ Subtlered
+ </Button>
+ </div>
+</BkndUi>
+
+<AutoTypeTable path="../app/src/ui/Admin.tsx" name="BkndAdminProps" />
+
+<AutoTypeTable path="../app/src/ui/options/index.ts" name="BkndAdminEntityOptions" />
+<AutoTypeTable path="../app/src/ui/options/index.ts" name="BkndAdminEntityFieldOptions" />
+
+<AutoTypeTable path="../app/src/ui/options/index.ts" name="BkndAdminAppShellOptions" />
\ No newline at end of file
Index: docs/tsconfig.json
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
--- a/docs/tsconfig.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
+++ b/docs/tsconfig.json (date 1758275691058)
@@ -18,6 +18,7 @@
"paths": {
"@/.source": ["./.source/index.ts"],
"@/bknd/*": ["../app/src/"],
+ "bknd-ui": ["../app/dist/ui/index.js"],
"@/*": ["./*"]
},
Index: docs/app/_components/BkndUi.tsx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/app/_components/BkndUi.tsx b/docs/app/_components/BkndUi.tsx
new file mode 100644
--- /dev/null (date 1758280846757)
+++ b/docs/app/_components/BkndUi.tsx (date 1758280846757)
@@ -0,0 +1,36 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { BkndUiClient } from "./BkndUiClient";
+
+// Server component that reads CSS from disk
+export function BkndUi({ children, code }: { children: React.ReactNode; code?: string }) {
+ let bkndCss = "";
+
+ try {
+ // Try to read the CSS file from the app directory
+ const cssPath = join(process.cwd(), "node_modules", "bknd", "dist", "ui", "main.css");
+ bkndCss = readFileSync(cssPath, "utf-8");
+ } catch (error) {
+ console.warn("Could not read bknd CSS file:", error);
+ // Fallback CSS
+ bkndCss = `
+ .bknd-admin {
+ --color-primary: #18181b;
+ --color-background: #fafafa;
+ --color-muted: #e4e4e7;
+ color: var(--color-primary);
+ background: var(--color-background);
+ padding: 16px;
+ border: 1px solid var(--color-muted);
+ border-radius: 8px;
+ font-family: system-ui, sans-serif;
+ }
+ `;
+ }
+
+ return (
+ <BkndUiClient bkndCss={bkndCss} code={code}>
+ {children}
+ </BkndUiClient>
+ );
+}
Index: docs/next.config.mjs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/next.config.mjs b/docs/next.config.mjs
--- a/docs/next.config.mjs (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
+++ b/docs/next.config.mjs (date 1758281330764)
@@ -19,6 +19,9 @@
webpack(config) {
config.resolve.alias["@/bknd"] = path.resolve(__dirname, "../app/src");
config.resolve.alias["@"] = path.resolve(__dirname);
+
+ // @todo: this doesn't work with turbo
+ config.resolve.alias["bknd-ui"] = path.resolve(__dirname, "../app/dist/ui/index.js");
return config;
},
eslint: {
Index: docs/app/_components/BkndUiClient.tsx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/app/_components/BkndUiClient.tsx b/docs/app/_components/BkndUiClient.tsx
new file mode 100644
--- /dev/null (date 1758281176041)
+++ b/docs/app/_components/BkndUiClient.tsx (date 1758281176041)
@@ -0,0 +1,62 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+import { Card } from "fumadocs-ui/components/card";
+import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
+import { useTheme } from "next-themes";
+
+interface BkndUiClientProps {
+ bkndCss: string;
+ children: React.ReactNode;
+ code?: string;
+}
+
+export function BkndUiClient({ bkndCss, children, code }: BkndUiClientProps) {
+ const { theme } = useTheme();
+ const containerRef = useRef<HTMLDivElement>(null);
+ const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
+ const [mountPoint, setMountPoint] = useState<HTMLDivElement | null>(null);
+
+ useEffect(() => {
+ if (!containerRef.current || shadowRoot) return;
+
+ // Create shadow root
+ const shadow = containerRef.current.attachShadow({ mode: "open" });
+
+ // Create style element with the CSS passed from server
+ const style = document.createElement("style");
+ style.textContent = `
+ :host {
+ display: block;
+ isolation: isolate;
+ }
+ ${bkndCss}
+ `;
+
+ shadow.appendChild(style);
+
+ // Create mount point for React content
+ const mount = document.createElement("div");
+ mount.className = `bknd-admin ${theme}`;
+ shadow.appendChild(mount);
+
+ setShadowRoot(shadow);
+ setMountPoint(mount);
+ }, [shadowRoot, bkndCss]);
+
+ return (
+ <>
+ {/* @ts-ignore */}
+ <Card className="p-0">
+ <div className="flex flex-col justify-center items-center p-4">
+ <div ref={containerRef} className="bknd-ui-shadow-host">
+ <div>Loading...</div>
+ {shadowRoot && mountPoint && createPortal(children, mountPoint)}
+ </div>
+ </div>
+ {code && <DynamicCodeBlock lang="tsx" code={code} />}
+ </Card>
+ </>
+ );
+}
Index: docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx b/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx
--- a/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
+++ b/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx (date 1758280630236)
@@ -1,5 +1,5 @@
---
-title: "MCP"
+title: "Tools & Resources"
description: "Tools & Resources of the built-in full featured MCP server."
tags: ["documentation"]
---
@@ -24,7 +24,7 @@
Create a new user
-<JsonSchemaTypeTable schema={{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"role":{"type":"string","enum":[]}},"required":["email","password"]}} key={"auth_user_create"} />
+<JsonSchemaTypeTable schema={{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"role":{"type":"string"}},"required":["email","password"]}} key={"auth_user_create"} />
### `auth_user_password_change`
@@ -48,61 +48,61 @@
Delete many
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_delete_many"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_delete_many"} />
### `data_entity_delete_one`
Delete one
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"}}}} key={"data_entity_delete_one"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"}}}} key={"data_entity_delete_one"} />
### `data_entity_fn_count`
Count entities
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_count"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_count"} />
### `data_entity_fn_exists`
Check if entity exists
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_exists"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_exists"} />
### `data_entity_info`
Retrieve entity info
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"}}}} key={"data_entity_info"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"}}}} key={"data_entity_info"} />
### `data_entity_insert`
Insert one or many
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"anyOf":[{"type":"object","properties":{}},{"type":"array","items":{"type":"object","properties":{}}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_insert"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"anyOf":[{"type":"object","properties":{}},{"type":"array","items":{"type":"object","properties":{}}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_insert"} />
### `data_entity_read_many`
Query entities
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"limit":{"type":"number","default":10,"$target":"json"},"offset":{"type":"number","default":0,"$target":"json"},"sort":{"type":"string","default":"id","$target":"json"},"where":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"},"select":{"type":"array","$target":"json","items":{"type":"string"}},"join":{"type":"array","$target":"json","items":{"type":"string"}},"with":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_read_many"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"limit":{"type":"number","default":10,"$target":"json"},"offset":{"type":"number","default":0,"$target":"json"},"sort":{"type":"string","default":"id","$target":"json"},"where":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"},"select":{"type":"array","$target":"json","items":{"type":"string"}},"join":{"type":"array","$target":"json","items":{"type":"string"}},"with":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_read_many"} />
### `data_entity_read_one`
Read one
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"offset":{"type":"number","default":0,"$target":"query"},"sort":{"type":"string","default":"id","$target":"query"},"select":{"type":"array","$target":"query","items":{"type":"string"}}}}} key={"data_entity_read_one"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"offset":{"type":"number","default":0,"$target":"query"},"sort":{"type":"string","default":"id","$target":"query"},"select":{"type":"array","$target":"query","items":{"type":"string"}}}}} key={"data_entity_read_one"} />
### `data_entity_update_many`
Update many
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","update","where"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"update":{"type":"object","$target":"json","properties":{}},"where":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_update_many"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","update","where"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"update":{"type":"object","$target":"json","properties":{}},"where":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_update_many"} />
### `data_entity_update_one`
Update one
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id","json"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_update_one"} />
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id","json"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_update_one"} />
### `data_sync`
@@ -306,7 +306,7 @@
Update Server configuration
-<JsonSchemaTypeTable schema={{"type":"object","additionalProperties":false,"properties":{"full":{"type":"boolean","default":false},"return_config":{"type":"boolean","description":"If the new configuration should be returned","default":false},"value":{"type":"object","additionalProperties":false,"properties":{"cors":{"type":"object","additionalProperties":false,"properties":{"origin":{"type":"string","default":"*"},"allow_methods":{"type":"array","default":["GET","POST","PATCH","PUT","DELETE"],"uniqueItems":true,"items":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"]}},"allow_headers":{"type":"array","default":["Content-Type","Content-Length","Authorization","Accept"],"items":{"type":"string"}},"allow_credentials":{"type":"boolean","default":true}}},"mcp":{"type":"object","additionalProperties":false,"properties":{"enabled":{"type":"boolean","default":false}}}}}},"required":["value"]}} key={"config_server_update"} />
+<JsonSchemaTypeTable schema={{"type":"object","additionalProperties":false,"properties":{"full":{"type":"boolean","default":false},"return_config":{"type":"boolean","description":"If the new configuration should be returned","default":false},"value":{"type":"object","additionalProperties":false,"properties":{"cors":{"type":"object","additionalProperties":false,"properties":{"origin":{"type":"string","default":"*"},"allow_methods":{"type":"array","default":["GET","POST","PATCH","PUT","DELETE"],"uniqueItems":true,"items":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"]}},"allow_headers":{"type":"array","default":["Content-Type","Content-Length","Authorization","Accept"],"items":{"type":"string"}},"allow_credentials":{"type":"boolean","default":true}}},"mcp":{"type":"object","additionalProperties":false,"properties":{"enabled":{"type":"boolean","default":false},"path":{"type":"string","default":"/api/system/mcp"}}}}}},"required":["value"]}} key={"config_server_update"} />
## Resources