Merge remote-tracking branch 'origin/release/0.6' into refactor/optimize-ui-bundle-size

# Conflicts:
#	app/build.ts
#	app/package.json
This commit is contained in:
dswbx
2025-01-18 14:13:34 +01:00
177 changed files with 3364 additions and 1616 deletions

View File

@@ -34,7 +34,11 @@ export function EntityTable2({ entity, select, ...props }: EntityTableProps) {
const field = getField(property)!;
_value = field.getValue(value, "table");
} catch (e) {
console.warn("Couldn't render value", { value, property, entity, select, ...props }, e);
console.warn(
"Couldn't render value",
{ value, property, entity, select, columns, ...props },
e
);
}
return <CellValue value={_value} property={property} />;

View File

@@ -10,7 +10,7 @@ import {
TbToggleLeft
} from "react-icons/tb";
type TFieldSpec = {
export type TFieldSpec = {
type: string;
label: string;
icon: any;

View File

@@ -9,6 +9,7 @@ import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button";
import * as Formy from "ui/components/form/Formy";
import { Popover } from "ui/components/overlay/Popover";
import { Link } from "ui/components/wouter/Link";
import { routes } from "ui/lib/routes";
import { useLocation } from "wouter";
import { EntityTable } from "../EntityTable";
@@ -82,7 +83,9 @@ export function EntityRelationalFormField({
return (
<Formy.Group>
<Formy.Label htmlFor={fieldApi.name}>{field.getLabel()}</Formy.Label>
<Formy.Label htmlFor={fieldApi.name}>
{field.getLabel({ fallback: false }) ?? entity.label}
</Formy.Label>
<div
data-disabled={fetching || disabled ? 1 : undefined}
className="data-[disabled]:opacity-70 data-[disabled]:pointer-events-none"
@@ -152,9 +155,11 @@ export function EntityRelationalFormField({
);
})}
</div>
<Button IconLeft={TbEye} onClick={handleViewItem} size="small">
View
</Button>
<Link to={routes.data.entity.edit(entity.name, _value.id as any)}>
<Button IconLeft={TbEye} size="small">
View
</Button>
</Link>
</>
) : (
<div className="pl-2">- Select -</div>

View File

@@ -1,15 +1,9 @@
import { type Static, StringEnum, StringIdentifier, Type, transformObject } from "core/utils";
import { FieldClassMap } from "data";
import type { ModalProps } from "@mantine/core";
import type { ContextModalProps } from "@mantine/modals";
import { type Static, StringEnum, StringIdentifier, Type } from "core/utils";
import { entitiesSchema, fieldsSchema, relationsSchema } from "data/data-schema";
import { omit } from "lodash-es";
import { forwardRef, useState } from "react";
import {
Modal2,
type Modal2Ref,
ModalBody,
ModalFooter,
ModalTitle
} from "ui/components/modal/Modal2";
import { useState } from "react";
import { type Modal2Ref, ModalBody, ModalFooter, ModalTitle } from "ui/components/modal/Modal2";
import { Step, Steps, useStepContext } from "ui/components/steps/Steps";
import { StepCreate } from "ui/modules/data/components/schema/create-modal/step.create";
import { StepEntity } from "./step.entity";
@@ -45,6 +39,7 @@ export type TFieldCreate = Static<typeof createFieldSchema>;
const createModalSchema = Type.Object(
{
action: schemaAction,
initial: Type.Optional(Type.Any()),
entities: Type.Optional(
Type.Object({
create: Type.Optional(Type.Array(entitySchema))
@@ -67,48 +62,59 @@ const createModalSchema = Type.Object(
);
export type TCreateModalSchema = Static<typeof createModalSchema>;
export const CreateModal = forwardRef<CreateModalRef>(function CreateModal(props, ref) {
const [path, setPath] = useState<string[]>([]);
export function CreateModal({
context,
id,
innerProps: { initialPath = [], initialState }
}: ContextModalProps<{ initialPath?: string[]; initialState?: TCreateModalSchema }>) {
const [path, setPath] = useState<string[]>(initialPath);
console.log("...", initialPath, initialState);
function close() {
// @ts-ignore
ref?.current?.close();
context.closeModal(id);
}
return (
<Modal2 ref={ref}>
<Steps path={path} lastBack={close}>
<Step id="select">
<ModalTitle path={["Create New"]} onClose={close} />
<StepSelect />
</Step>
<Step id="entity" path={["action"]}>
<ModalTitle path={["Create New", "Entity"]} onClose={close} />
<StepEntity />
</Step>
<Step id="entity-fields" path={["action", "entity"]}>
<ModalTitle path={["Create New", "Entity", "Fields"]} onClose={close} />
<StepEntityFields />
</Step>
<Step id="relation" path={["action"]}>
<ModalTitle path={["Create New", "Relation"]} onClose={close} />
<StepRelation />
</Step>
<Step id="create" path={["action"]}>
<ModalTitle path={["Create New", "Creation"]} onClose={close} />
<StepCreate />
</Step>
<Steps path={path} lastBack={close} initialPath={initialPath} initialState={initialState}>
<Step id="select">
<ModalTitle path={["Create New"]} onClose={close} />
<StepSelect />
</Step>
<Step id="entity" path={["action"]}>
<ModalTitle path={["Create New", "Entity"]} onClose={close} />
<StepEntity />
</Step>
<Step id="entity-fields" path={["action", "entity"]}>
<ModalTitle path={["Create New", "Entity", "Fields"]} onClose={close} />
<StepEntityFields />
</Step>
<Step id="relation" path={["action"]}>
<ModalTitle path={["Create New", "Relation"]} onClose={close} />
<StepRelation />
</Step>
<Step id="create" path={["action"]}>
<ModalTitle path={["Create New", "Creation"]} onClose={close} />
<StepCreate />
</Step>
{/* Templates */}
{Templates.map(([Component, meta]) => (
<Step key={meta.id} id={meta.id} path={["action"]}>
<ModalTitle path={["Create New", "Template", meta.title]} onClose={close} />
<Component />
</Step>
))}
</Steps>
</Modal2>
{/* Templates */}
{Templates.map(([Component, meta]) => (
<Step key={meta.id} id={meta.id} path={["action"]}>
<ModalTitle path={["Create New", "Template", meta.title]} onClose={close} />
<Component />
</Step>
))}
</Steps>
);
});
}
CreateModal.defaultTitle = undefined;
CreateModal.modalProps = {
withCloseButton: false,
size: "xl",
padding: 0,
classNames: {
root: "bknd-admin"
}
} satisfies Partial<ModalProps>;
export { ModalBody, ModalFooter, ModalTitle, useStepContext, relationsSchema };

View File

@@ -8,8 +8,8 @@ import {
} from "@tabler/icons-react";
import { ucFirst } from "core/utils";
import { useEffect, useState } from "react";
import { TbCirclesRelation, TbSettings } from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import { useBknd } from "ui/client/bknd";
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
import { IconButton, type IconType } from "ui/components/buttons/IconButton";
import { JsonViewer } from "ui/components/code/JsonViewer";
@@ -26,6 +26,7 @@ export function StepCreate() {
const [states, setStates] = useState<(boolean | string)[]>([]);
const [submitting, setSubmitting] = useState(false);
const $data = useBkndData();
const b = useBknd();
const items: ActionItem[] = [];
if (state.entities?.create) {
@@ -74,6 +75,10 @@ export function StepCreate() {
try {
const res = await item.run();
setStates((prev) => [...prev, res]);
if (res !== true) {
// make sure to break out
break;
}
} catch (e) {
setStates((prev) => [...prev, (e as any).message]);
}
@@ -90,7 +95,8 @@ export function StepCreate() {
states.every((s) => s === true)
);
if (items.length === states.length && states.every((s) => s === true)) {
close();
b.actions.reload().then(close);
//close();
} else {
setSubmitting(false);
}
@@ -144,12 +150,14 @@ const SummaryItem: React.FC<SummaryItemProps> = ({
}) => {
const [expanded, handlers] = useDisclosure(initialExpanded);
const error = typeof state !== "undefined" && state !== true;
const done = state === true;
return (
<div
className={twMerge(
"flex flex-col border border-muted rounded bg-background mb-2",
error && "bg-red-500/20"
error && "bg-red-500/20",
done && "bg-green-500/20"
)}
>
<div className="flex flex-row gap-4 px-2 py-2 items-center">

View File

@@ -9,12 +9,15 @@ import {
registerCustomTypeboxKinds
} from "core/utils";
import { ManyToOneRelation, type RelationType, RelationTypes } from "data";
import { type ReactNode, useEffect } from "react";
import { type ReactNode, startTransition, useEffect } from "react";
import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form";
import { TbRefresh } from "react-icons/tb";
import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button";
import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput";
import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect";
import { useStepContext } from "ui/components/steps/Steps";
import { useEvent } from "ui/hooks/use-event";
import { ModalBody, ModalFooter, type TCreateModalSchema } from "./CreateModal";
// @todo: check if this could become an issue
@@ -63,7 +66,7 @@ type ComponentCtx<T extends FieldValues = FieldValues> = {
export function StepRelation() {
const { config } = useBknd();
const entities = config.data.entities;
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
const { nextStep, stepBack, state, path, setState } = useStepContext<TCreateModalSchema>();
const {
register,
handleSubmit,
@@ -93,6 +96,22 @@ export function StepRelation() {
}
}
const flip = useEvent(() => {
const { source, target } = data;
if (source && target) {
setValue("source", target);
setValue("target", source);
} else {
if (source) {
setValue("target", source);
setValue("source", null as any);
} else {
setValue("source", target);
setValue("target", null as any);
}
}
});
return (
<>
<form onSubmit={handleSubmit(handleNext)}>
@@ -109,14 +128,23 @@ export function StepRelation() {
disabled: data.target === name
}))}
/>
<MantineSelect
control={control}
name="type"
onChange={() => setValue("config", {})}
label="Relation Type"
data={Relations.map((r) => ({ value: r.type, label: r.label }))}
allowDeselect={false}
/>
<div className="flex flex-col gap-1">
<MantineSelect
control={control}
name="type"
onChange={() => setValue("config", {})}
label="Relation Type"
data={Relations.map((r) => ({ value: r.type, label: r.label }))}
allowDeselect={false}
/>
{data.type && (
<div className="flex justify-center mt-1">
<Button size="small" IconLeft={TbRefresh} onClick={flip}>
Flip entities
</Button>
</div>
)}
</div>
<MantineSelect
control={control}
allowDeselect={false}
@@ -146,7 +174,7 @@ export function StepRelation() {
onClick: handleNext
}}
prev={{ onClick: stepBack }}
debug={{ state, data }}
debug={{ state, path, data }}
/>
</form>
</>

View File

@@ -12,7 +12,7 @@ import {
import Templates from "./templates/register";
export function StepSelect() {
const { nextStep, stepBack, state, setState } = useStepContext<TCreateModalSchema>();
const { nextStep, stepBack, state, path, setState } = useStepContext<TCreateModalSchema>();
const selected = state.action ?? null;
function handleSelect(action: TSchemaAction) {
@@ -74,6 +74,7 @@ export function StepSelect() {
}}
prev={{ onClick: stepBack }}
prevLabel="Cancel"
debug={{ state, path }}
/>
</>
);

View File

@@ -9,6 +9,7 @@ import {
transformObject
} from "core/utils";
import type { MediaFieldConfig } from "media/MediaField";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useBknd } from "ui/client/bknd";
import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput";
@@ -31,18 +32,19 @@ const schema = Type.Object({
type TCreateModalMediaSchema = Static<typeof schema>;
export function TemplateMediaComponent() {
const { stepBack, setState, state, nextStep } = useStepContext<TCreateModalSchema>();
const { stepBack, setState, state, path, nextStep } = useStepContext<TCreateModalSchema>();
const {
register,
handleSubmit,
formState: { isValid },
setValue,
formState: { isValid, errors },
watch,
control
} = useForm({
mode: "onChange",
resolver: typeboxResolver(schema),
defaultValues: Default(schema, {}) as TCreateModalMediaSchema
defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema
});
const [forbidden, setForbidden] = useState<boolean>(false);
const { config } = useBknd();
const media_enabled = config.media.enabled ?? false;
@@ -51,13 +53,16 @@ export function TemplateMediaComponent() {
name !== media_entity ? entity : undefined
);
const data = watch();
const forbidden_field_names = Object.keys(config.data.entities?.[data.entity]?.fields ?? {});
useEffect(() => {
setForbidden(forbidden_field_names.includes(data.name));
}, [forbidden_field_names, data.name]);
async function handleCreate() {
if (isValid) {
console.log("data", data);
if (isValid && !forbidden) {
const { field, relation } = convert(media_entity, data);
console.log("state", { field, relation });
setState((prev) => ({
...prev,
fields: { create: [field] },
@@ -120,6 +125,13 @@ export function TemplateMediaComponent() {
data.entity ? data.entity : "the entity"
}.`}
{...register("name")}
error={
errors.name?.message
? errors.name?.message
: forbidden
? `Property "${data.name}" already exists on entity ${data.entity}`
: undefined
}
/>
</div>
{/*<p>step template media</p>
@@ -129,12 +141,12 @@ export function TemplateMediaComponent() {
<ModalFooter
next={{
type: "submit",
disabled: !isValid || !media_enabled
disabled: !isValid || !media_enabled || forbidden
}}
prev={{
onClick: stepBack
}}
debug={{ state, data }}
debug={{ state, path, data }}
/>
</form>
</>