fix: on delete
Run Tests / test (pull_request) Successful in 7m24s

This commit is contained in:
2026-09-27 17:57:45 +05:30
parent b294ee68ac
commit 282c9f6054
38 changed files with 742 additions and 655 deletions
+3 -3
View File
@@ -17,9 +17,9 @@ export type BkndAdminConfig = {
*/
basepath?: string;
/**
* Sub-path for the Admin UI within the base path
* @default ``
*/
* Sub-path for the Admin UI within the base path
* @default ``
*/
admin_basepath?: string;
/**
* Path to return to when clicking the logo
+1 -1
View File
@@ -4,7 +4,7 @@ export {
type ClientProviderProps,
useApi,
useBaseUrl,
useClientContext
useClientContext,
} from "./ClientProvider";
export * from "./api/use-api";
-1
View File
@@ -22,7 +22,6 @@ export class AppReduced {
protected appJson: AppType,
protected _options: BkndAdminProps["config"] = {},
) {
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return constructEntity(name, entity);
});
+5 -1
View File
@@ -4,7 +4,11 @@ export function Logo({
scale = 0.2,
fill,
...props
}: { scale?: number; fill?: string; theme?: string }) {
}: {
scale?: number;
fill?: string;
theme?: string;
}) {
const t = useTheme();
const theme = props.theme ?? t.theme;
const svgFill = fill ? fill : theme === "light" ? "black" : "white";
@@ -117,7 +117,11 @@ const ArrayAdd = ({
schema,
path: _path,
label = "Add",
}: { schema: JsonSchema; path: string; label?: string }) => {
}: {
schema: JsonSchema;
path: string;
label?: string;
}) => {
const {
setValue,
value: { currentIndex },
+6 -1
View File
@@ -64,6 +64,11 @@ export function Step({
path = [],
id,
...rest
}: { children: React.ReactNode; disabled?: boolean; id: string; path?: string[] }) {
}: {
children: React.ReactNode;
disabled?: boolean;
id: string;
path?: string[];
}) {
return <div {...rest}>{children}</div>;
}
+6 -1
View File
@@ -35,7 +35,12 @@ export function checkMaxReached({
current = 0,
overwrite,
added,
}: { maxItems?: number; current?: number; overwrite?: boolean; added: number }) {
}: {
maxItems?: number;
current?: number;
overwrite?: boolean;
added: number;
}) {
if (!maxItems) {
return {
reject: false,
+6 -1
View File
@@ -101,7 +101,12 @@ export function useNavigate() {
}
}
const _url = options?.absolute ? `~/${app.options.basepath}/${app.options.admin_basepath}${url}`.replace(/\/+/g, "/") : url;
const _url = options?.absolute
? `~/${app.options.basepath}/${app.options.admin_basepath}${url}`.replace(
/\/+/g,
"/",
)
: url;
const state = {
...options?.state,
referrer: location,
@@ -283,7 +283,10 @@ function EntityJsonFormField({
fieldApi,
field,
...props
}: { fieldApi: TFieldApi; field: JsonField }) {
}: {
fieldApi: TFieldApi;
field: JsonField;
}) {
const [error, setError] = useState<any>(null);
const handleUpdate = useEvent((value: any) => {
setError(null);
@@ -312,7 +315,10 @@ function EntityEnumFormField({
fieldApi,
field,
...props
}: { fieldApi: TFieldApi; field: EnumField }) {
}: {
fieldApi: TFieldApi;
field: EnumField;
}) {
const handleUpdate = useEvent((e: React.ChangeEvent<HTMLTextAreaElement>) => {
fieldApi.handleChange(e.target.value);
});
@@ -1,6 +1,12 @@
import { Switch, TextInput } from "@mantine/core";
import { IconDatabase } from "@tabler/icons-react";
import { ManyToOneRelation, type RelationType, RelationTypes } from "data/relations";
import {
DEFAULT_RELATION_CASCADE,
ManyToOneRelation,
type RelationType,
RelationCascades,
RelationTypes,
} from "data/relations";
import type { ReactNode } from "react";
import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form";
import { TbRefresh } from "react-icons/tb";
@@ -46,7 +52,9 @@ const schema = s.strictObject({
type: s.string({ enum: Relations.map((r) => r.type) }),
source: stringIdentifier,
target: stringIdentifier,
config: s.object({}),
config: s.object({
on_delete: s.string({ enum: RelationCascades }).optional(),
}),
});
type ComponentCtx<T extends FieldValues = FieldValues> = {
@@ -186,6 +194,25 @@ const Callout = ({ children }: { children: ReactNode }) => (
<div className="bg-primary/5 py-4 px-5 rounded-lg mt-10">{children}</div>
);
function OnDelete({
control,
label = "On target delete",
}: {
control: Control<any>;
label?: string;
}) {
return (
<MantineSelect
control={control}
name="config.on_delete"
label={label}
allowDeselect={false}
defaultValue={DEFAULT_RELATION_CASCADE}
data={RelationCascades.map((cascade) => ({ value: cascade, label: cascade }))}
/>
);
}
function ManyToOne({ register, control, data: { source, target, config } }: ComponentCtx) {
return (
<>
@@ -209,6 +236,7 @@ function ManyToOne({ register, control, data: { source, target, config } }: Comp
control={control}
placeholder={String(ManyToOneRelation.DEFAULTS.with_limit)}
/>
<OnDelete control={control} />
</div>
<div />
<div className="flex flex-col gap-4">
@@ -222,30 +250,32 @@ function ManyToOne({ register, control, data: { source, target, config } }: Comp
</div>
{source && target && config && (
<Callout>
<>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source}.{config.mappedBy || target}_id {"→"} {target}
</pre>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source}.{config.mappedBy || target}_id {"→"} {target}
</pre>
<p>
Many <Pre>{source}</Pre> will each have one reference to <Pre>{target}</Pre>.
</p>
<p>
A property <Pre>{config.mappedBy || target}_id</Pre> will be added to{" "}
<Pre>{source}</Pre> (which references <Pre>{target}</Pre>).
</p>
<p>
When creating <Pre>{source}</Pre>, a reference to <Pre>{target}</Pre> is{" "}
<i>{config.required ? "required" : "optional"}</i>.
</p>
<p>
If a <Pre>{target}</Pre> entry is deleted, the reference is{" "}
<i>{config.on_delete ?? DEFAULT_RELATION_CASCADE}</i>.
</p>
{config.sourceCardinality ? (
<p>
Many <Pre>{source}</Pre> will each have one reference to <Pre>{target}</Pre>.
<Pre>{target}</Pre> should not have more than{" "}
<Pre>{config.sourceCardinality}</Pre> referencing entr
{config.sourceCardinality === 1 ? "y" : "ies"} to <Pre>{source}</Pre>.
</p>
<p>
A property <Pre>{config.mappedBy || target}_id</Pre> will be added to{" "}
<Pre>{source}</Pre> (which references <Pre>{target}</Pre>).
</p>
<p>
When creating <Pre>{source}</Pre>, a reference to <Pre>{target}</Pre> is{" "}
<i>{config.required ? "required" : "optional"}</i>.
</p>
{config.sourceCardinality ? (
<p>
<Pre>{target}</Pre> should not have more than{" "}
<Pre>{config.sourceCardinality}</Pre> referencing entr
{config.sourceCardinality === 1 ? "y" : "ies"} to <Pre>{source}</Pre>.
</p>
) : null}
</>
) : null}
</Callout>
)}
</>
@@ -258,7 +288,7 @@ function OneToOne({
data: {
source,
target,
config: { mappedBy, required },
config: { mappedBy, required, on_delete },
},
}: ComponentCtx) {
return (
@@ -271,6 +301,7 @@ function OneToOne({
placeholder={target}
/>
<Switch label="Required" {...register("config.required")} />
<OnDelete control={control} />
</div>
<div />
<div className="flex flex-col gap-4">
@@ -283,24 +314,26 @@ function OneToOne({
</div>
{source && target && (
<Callout>
<>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source}.{mappedBy || target}_id {"↔"} {target}
</pre>
<p>
A single entry of <Pre>{source}</Pre> will have a reference to{" "}
<Pre>{target}</Pre>.
</p>
<p>
A property <Pre>{mappedBy || target}_id</Pre> will be added to{" "}
<Pre>{source}</Pre> (which references <Pre>{target}</Pre>).
</p>
<p>
When creating <Pre>{source}</Pre>, a reference to <Pre>{target}</Pre> is{" "}
<i>{required ? "required" : "optional"}</i>.
</p>
</>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source}.{mappedBy || target}_id {"↔"} {target}
</pre>
<p>
A single entry of <Pre>{source}</Pre> will have a reference to <Pre>{target}</Pre>
.
</p>
<p>
A property <Pre>{mappedBy || target}_id</Pre> will be added to <Pre>{source}</Pre>{" "}
(which references <Pre>{target}</Pre>).
</p>
<p>
When creating <Pre>{source}</Pre>, a reference to <Pre>{target}</Pre> is{" "}
<i>{required ? "required" : "optional"}</i>.
</p>
<p>
If a <Pre>{target}</Pre> entry is deleted, the reference is{" "}
<i>{on_delete ?? DEFAULT_RELATION_CASCADE}</i>.
</p>
</Callout>
)}
</>
@@ -334,6 +367,7 @@ function ManyToMany({ register, control, data: { source, target, config } }: Com
{...register("config.connectionTableMappedName")}
placeholder={table}
/>
<OnDelete control={control} label="On delete (both references)" />
</div>
<div className="flex flex-col gap-4">
{/*<TextInput
@@ -345,18 +379,20 @@ function ManyToMany({ register, control, data: { source, target, config } }: Com
</div>
{source && target && (
<Callout>
<>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source} {"→"} {table} {"←"} {target}
</pre>
<p>
Many <Pre>{source}</Pre> will have many <Pre>{target}</Pre>.
</p>
<p>
A connection table <Pre>{table}</Pre> will be created to store the relations.
</p>
</>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source} {"→"} {table} {"←"} {target}
</pre>
<p>
Many <Pre>{source}</Pre> will have many <Pre>{target}</Pre>.
</p>
<p>
A connection table <Pre>{table}</Pre> will be created to store the relations.
</p>
<p>
If an entry of <Pre>{source}</Pre> or <Pre>{target}</Pre> is deleted, the
connection is <i>{config.on_delete ?? DEFAULT_RELATION_CASCADE}</i>.
</p>
</Callout>
)}
</>
@@ -391,26 +427,24 @@ function Polymorphic({ register, control, data: { type, source, target, config }
</div>
{source && target && (
<Callout>
<>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source} {"←"} {target}
</pre>
<pre className="mb-2 opacity-70 flex flex-row items-center gap-2">
<IconDatabase className="size-4" />
{source} {"←"} {target}
</pre>
<p>
<Pre>{source}</Pre> will have many <Pre>{target}</Pre>.
</p>
<p>
<Pre>{target}</Pre> will get additional properties <Pre>reference</Pre> and{" "}
<Pre>entity_id</Pre> to make the (polymorphic) reference.
</p>
{config.targetCardinality ? (
<p>
<Pre>{source}</Pre> will have many <Pre>{target}</Pre>.
<Pre>{source}</Pre> should not have more than{" "}
<Pre>{config.targetCardinality}</Pre> reference
{config.targetCardinality === 1 ? "" : "s"} to <Pre>{target}</Pre>.
</p>
<p>
<Pre>{target}</Pre> will get additional properties <Pre>reference</Pre> and{" "}
<Pre>entity_id</Pre> to make the (polymorphic) reference.
</p>
{config.targetCardinality ? (
<p>
<Pre>{source}</Pre> should not have more than{" "}
<Pre>{config.targetCardinality}</Pre> reference
{config.targetCardinality === 1 ? "" : "s"} to <Pre>{target}</Pre>.
</p>
) : null}
</>
) : null}
</Callout>
)}
</>
+11 -2
View File
@@ -110,7 +110,12 @@ const EntityLinkList = ({
title,
context,
suggestCreate = false,
}: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => {
}: {
entities: Entity[];
title?: string;
context: "data" | "schema";
suggestCreate?: boolean;
}) => {
const { $data } = useBkndData();
const { readonly } = useBknd();
const navigate = useRouteNavigate();
@@ -195,7 +200,11 @@ const EntityContextMenu = ({
entity,
children,
enabled = true,
}: { entity: Entity; children: DropdownClickableChild; enabled?: boolean }) => {
}: {
entity: Entity;
children: DropdownClickableChild;
enabled?: boolean;
}) => {
if (!enabled) return children;
const [navigate] = useNavigate();
const { $data } = useBkndData();
@@ -66,7 +66,10 @@ const relationAnyOfValues = {
export const DataSettings = ({
schema,
config,
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
}: {
schema: ModuleSchemas["data"];
config: ModuleConfigs["data"];
}) => {
const { app, readonly } = useBknd();
const prefix = app.getAbsolutePath("settings");
const entities = Object.keys(config.entities ?? {});
@@ -69,11 +69,7 @@ function CustomUserAvatarDropzone() {
showPlaceholder,
actions: { openFileInput },
} = Media.useDropzone();
const {
isOver,
isOverAccepted,
files: [file] = [],
} = Media.useDropzoneState();
const { isOver, isOverAccepted, files: [file] = [] } = Media.useDropzoneState();
return (
<div
+3 -1
View File
@@ -98,7 +98,9 @@ input[disabled]::placeholder {
}
.flex-animate {
transition: flex-grow 0.2s ease, background-color 0.2s ease;
transition:
flex-grow 0.2s ease,
background-color 0.2s ease;
}
.flex-initial {
flex: 0 1 auto;
+1 -1
View File
@@ -1 +1 @@
declare module '*.css';
declare module "*.css";