Merge remote-tracking branch 'origin/release/0.10' into feat/remove-admin-config

# Conflicts:
#	app/src/modules/server/AdminController.tsx
#	app/src/ui/Admin.tsx
This commit is contained in:
dswbx
2025-03-11 13:56:27 +01:00
498 changed files with 14118 additions and 5427 deletions

View File

@@ -11,7 +11,7 @@ export function useCreateUserModal() {
const open = async () => {
const loading = bkndModals.open("overlay", {
content: "Loading..."
content: "Loading...",
});
const schema = await api.auth.actionSchema("password", "create");
@@ -23,8 +23,8 @@ export function useCreateUserModal() {
schema,
uiSchema: {
password: {
"ui:widget": "password"
}
"ui:widget": "password",
},
},
autoCloseAfterSubmit: false,
onSubmit: async (data, ctx) => {
@@ -41,11 +41,11 @@ export function useCreateUserModal() {
} else {
ctx.setError("Unknown error");
}
}
},
},
{
title: "Create User"
}
title: "Create User",
},
);
};

View File

@@ -1,4 +1,5 @@
import type { FieldApi, FormApi } from "@tanstack/react-form";
import type { FieldApi, ReactFormExtendedApi } from "@tanstack/react-form";
import type { JSX } from "react";
import {
type Entity,
type EntityData,
@@ -6,8 +7,9 @@ import {
type Field,
JsonField,
JsonSchemaField,
RelationField
RelationField,
} from "data";
import { useStore } from "@tanstack/react-store";
import { MediaField } from "media/MediaField";
import { type ComponentProps, Suspense } from "react";
import { JsonEditor } from "ui/components/code/JsonEditor";
@@ -17,6 +19,13 @@ import { Media } from "ui/elements";
import { useEvent } from "ui/hooks/use-event";
import { EntityJsonSchemaFormField } from "./fields/EntityJsonSchemaFormField";
import { EntityRelationalFormField } from "./fields/EntityRelationalFormField";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
import { Alert } from "ui/components/display/Alert";
// simplify react form types 🤦
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
// biome-ignore format: ...
export type TFieldApi = FieldApi<any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any, any>;
type EntityFormProps = {
entity: Entity;
@@ -24,7 +33,7 @@ type EntityFormProps = {
data?: EntityData;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
fieldsDisabled: boolean;
Form: FormApi<any>;
Form: FormApi;
className?: string;
action: "create" | "update";
};
@@ -37,10 +46,9 @@ export function EntityForm({
Form,
data,
className,
action
action,
}: EntityFormProps) {
const fields = entity.getFillableFields(action, true);
console.log("data", { data, fields });
return (
<form onSubmit={handleSubmit}>
@@ -94,20 +102,28 @@ export function EntityForm({
const _key = `${entity.name}-${field.name}-${key}`;
return (
<Form.Field
<ErrorBoundary
key={_key}
name={field.name}
children={(props) => (
<EntityFormField
field={field}
fieldApi={props}
disabled={fieldsDisabled}
tabIndex={key + 1}
action={action}
data={data}
/>
)}
/>
fallback={
<Alert.Exception className="font-mono">
Field error: {field.name}
</Alert.Exception>
}
>
<Form.Field
name={field.name}
children={(props) => (
<EntityFormField
field={field}
fieldApi={props}
disabled={fieldsDisabled}
tabIndex={key + 1}
action={action}
data={data}
/>
)}
/>
</ErrorBoundary>
);
})}
</div>
@@ -120,9 +136,9 @@ export function EntityForm({
type EntityFormFieldProps<
T extends keyof JSX.IntrinsicElements = "input",
F extends Field = Field
F extends Field = Field,
> = ComponentProps<T> & {
fieldApi: FieldApi<any, any>;
fieldApi: TFieldApi;
field: F;
action: "create" | "update";
data?: EntityData;
@@ -203,9 +219,9 @@ function EntityMediaFormField({
field,
entity,
entityId,
disabled
disabled,
}: {
formApi: FormApi<any>;
formApi: FormApi;
field: MediaField;
entity: Entity;
entityId?: number;
@@ -213,7 +229,7 @@ function EntityMediaFormField({
}) {
if (!entityId) return;
const value = formApi.useStore((state) => {
const value = useStore(formApi.store, (state) => {
const val = state.values[field.name];
if (!val || typeof val === "undefined") return [];
if (Array.isArray(val)) return val;
@@ -232,7 +248,7 @@ function EntityMediaFormField({
entity={{
name: entity.name,
id: entityId,
field: field.name
field: field.name,
}}
/>
</Formy.Group>
@@ -243,7 +259,7 @@ function EntityJsonFormField({
fieldApi,
field,
...props
}: { fieldApi: FieldApi<any, any>; field: JsonField }) {
}: { fieldApi: TFieldApi; field: JsonField }) {
const handleUpdate = useEvent((value: any) => {
fieldApi.handleChange(value);
});
@@ -279,7 +295,7 @@ function EntityEnumFormField({
fieldApi,
field,
...props
}: { fieldApi: FieldApi<any, any>; field: EnumField }) {
}: { fieldApi: TFieldApi; field: EnumField }) {
const handleUpdate = useEvent((e: React.ChangeEvent<HTMLTextAreaElement>) => {
fieldApi.handleChange(e.target.value);
});

View File

@@ -9,7 +9,7 @@ import {
TbChevronsRight,
TbSelector,
TbSquare,
TbSquareCheckFilled
TbSquareCheckFilled,
} from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import { Button } from "ui/components/buttons/Button";
@@ -58,7 +58,7 @@ export const EntityTable: React.FC<EntityTableProps> = ({
perPage = 10,
perPageOptions,
onClickPerPage,
classNames
classNames,
}) => {
select = select && select.length > 0 ? select : entity.getSelect();
total = total || data.length;
@@ -157,7 +157,7 @@ export const EntityTable: React.FC<EntityTableProps> = ({
<Dropdown
items={perPageOptions.map((perPage) => ({
label: String(perPage),
perPage
perPage,
}))}
position="top-end"
onClickItem={(item: any) => onClickPerPage?.(item.perPage)}
@@ -182,7 +182,7 @@ export const EntityTable: React.FC<EntityTableProps> = ({
const SortIndicator = ({
sort,
field
field,
}: {
sort: Pick<EntityTableProps, "sort">["sort"];
field: string;
@@ -220,7 +220,7 @@ const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNav
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
{ value: current + 1, Icon: TbChevronRight, disabled: current === total },
{ value: total, Icon: TbChevronsRight, disabled: current === total }
{ value: total, Icon: TbChevronsRight, disabled: current === total },
] as const;
return navMap.map((nav, key) => (

View File

@@ -1,5 +1,6 @@
import type { Entity, EntityData } from "data";
import { CellValue, DataTable, type DataTableProps } from "ui/components/table/DataTable";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
type EntityTableProps<Data extends EntityData = EntityData> = Omit<
DataTableProps<Data>,
@@ -37,11 +38,15 @@ export function EntityTable2({ entity, select, ...props }: EntityTableProps) {
console.warn(
"Couldn't render value",
{ value, property, entity, select, columns, ...props },
e
e,
);
}
return <CellValue value={_value} property={property} />;
return (
<ErrorBoundary fallback={String(_value)}>
<CellValue value={_value} property={property} />
</ErrorBoundary>
);
}
return (

View File

@@ -16,13 +16,33 @@ function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEnti
dragHandle: ".drag-handle",
position: { x: 0, y: 0 },
sourcePosition: Position.Right,
targetPosition: Position.Left
targetPosition: Position.Left,
};
});
}
function relationsToEdges(relations: AppDataConfig["relations"]) {
return Object.entries(relations ?? {}).map(([name, relation]) => {
return Object.entries(relations ?? {}).flatMap(([name, relation]) => {
if (relation.type === "m:n") {
const conn_table = `${relation.source}_${relation.target}`;
return [
{
id: name,
target: relation.source,
source: conn_table,
targetHandle: `${relation.source}:id`,
sourceHandle: `${conn_table}:${relation.source}_id`,
},
{
id: `${name}-2`,
target: relation.target,
source: conn_table,
targetHandle: `${relation.target}:id`,
sourceHandle: `${conn_table}:${relation.target}_id`,
},
];
}
let sourceHandle = relation.source + `:${relation.target}`;
if (relation.config?.mappedBy) {
sourceHandle = `${relation.source}:${relation.config?.mappedBy}`;
@@ -36,46 +56,48 @@ function relationsToEdges(relations: AppDataConfig["relations"]) {
source: relation.source,
target: relation.target,
sourceHandle,
targetHandle: relation.target + ":id"
targetHandle: relation.target + ":id",
};
});
}
const nodeTypes = {
entity: EntityTableNode.Component
entity: EntityTableNode.Component,
} as const;
export function DataSchemaCanvas() {
const {
config: { data }
config: { data },
} = useBknd();
const { theme } = useBkndSystemTheme();
const nodes = entitiesToNodes(data.entities);
const edges = relationsToEdges(data.relations).map((e) => ({
...e,
style: {
stroke: theme === "light" ? "#ccc" : "#666"
stroke: theme === "light" ? "#ccc" : "#666",
},
type: "smoothstep",
markerEnd: {
type: MarkerType.Arrow,
width: 20,
height: 20,
color: theme === "light" ? "#aaa" : "#777"
}
color: theme === "light" ? "#aaa" : "#777",
},
}));
console.log("-", data, { nodes, edges });
const nodeLayout = layoutWithDagre({
nodes: nodes.map((n) => ({
id: n.id,
...EntityTableNode.getSize(n.data)
...EntityTableNode.getSize(n.data),
})),
edges,
graph: {
rankdir: "LR",
marginx: 50,
marginy: 50
}
marginy: 50,
},
});
nodeLayout.nodes.forEach((node) => {
@@ -95,7 +117,7 @@ export function DataSchemaCanvas() {
maxZoom={2}
fitViewOptions={{
minZoom: 0.1,
maxZoom: 0.8
maxZoom: 0.8,
}}
>
<Panels zoom minimap />

View File

@@ -47,14 +47,14 @@ function NodeComponent(props: NodeProps<Node<TAppDataEntity & { label: string }>
const handleStyle = {
background: "transparent",
border: "none"
border: "none",
};
const TableRow = ({
field,
table,
index,
onHover,
last
last,
}: {
field: TableField;
table: string;
@@ -71,7 +71,7 @@ const TableRow = ({
className={twMerge(
"flex flex-row w-full justify-between font-mono py-1.5 px-2.5 border-b border-primary/15 border-l border-r cursor-auto",
last && "rounded-bl-lg rounded-br-lg",
"hover:bg-primary/5"
"hover:bg-primary/5",
)}
>
{handles && (
@@ -108,7 +108,7 @@ const TableRow = ({
export const HEIGHTS = {
header: 30,
row: 32.5
row: 32.5,
};
export const EntityTableNode = {
@@ -118,7 +118,7 @@ export const EntityTableNode = {
const field_count = Object.keys(fields).length;
return {
width: 320,
height: HEIGHTS.header + HEIGHTS.row * field_count
height: HEIGHTS.header + HEIGHTS.row * field_count,
};
}
},
};

View File

@@ -7,7 +7,7 @@ import {
TbPhoto,
TbSelector,
TbTextCaption,
TbToggleLeft
TbToggleLeft,
} from "react-icons/tb";
export type TFieldSpec = {
@@ -26,55 +26,55 @@ export const fieldSpecs: TFieldSpec[] = [
icon: TbTextCaption,
addable: false,
disabled: ["name"],
hidden: ["virtual"]
hidden: ["virtual"],
},
{
type: "text",
label: "Text",
icon: TbTextCaption
icon: TbTextCaption,
},
{
type: "number",
label: "Number",
icon: TbNumber123
icon: TbNumber123,
},
{
type: "boolean",
label: "Boolean",
icon: TbToggleLeft
icon: TbToggleLeft,
},
{
type: "date",
label: "Date",
icon: TbCalendar
icon: TbCalendar,
},
{
type: "enum",
label: "Enum",
icon: TbSelector
icon: TbSelector,
},
{
type: "json",
label: "JSON",
icon: TbBraces
icon: TbBraces,
},
{
type: "jsonschema",
label: "JSON Schema",
icon: TbCodePlus
icon: TbCodePlus,
},
{
type: "relation",
label: "Relation",
icon: TbCirclesRelation,
addable: false,
hidden: ["virtual"]
hidden: ["virtual"],
},
{
type: "media",
label: "Media",
icon: TbPhoto,
addable: false,
hidden: ["virtual"]
}
hidden: ["virtual"],
},
];

View File

@@ -1,8 +1,8 @@
import type { FieldApi } from "@tanstack/react-form";
import type { EntityData, JsonSchemaField } from "data";
import * as Formy from "ui/components/form/Formy";
import { FieldLabel } from "ui/components/form/Formy";
import { JsonSchemaForm } from "ui/components/form/json-schema";
import type { TFieldApi } from "ui/modules/data/components/EntityForm";
export function EntityJsonSchemaFormField({
fieldApi,
@@ -11,7 +11,7 @@ export function EntityJsonSchemaFormField({
disabled,
...props
}: {
fieldApi: FieldApi<any, any>;
fieldApi: TFieldApi;
field: JsonSchemaField;
data?: EntityData;
disabled?: boolean;
@@ -39,7 +39,7 @@ export function EntityJsonSchemaFormField({
formData={formData}
uiSchema={{
"ui:globalOptions": { flexDirection: "row" },
...field.getJsonUiSchema()
...field.getJsonUiSchema(),
}}
/>
</div>

View File

@@ -1,5 +1,4 @@
import { getHotkeyHandler, useHotkeys } from "@mantine/hooks";
import type { FieldApi } from "@tanstack/react-form";
import { ucFirst } from "core/utils";
import type { EntityData, RelationField } from "data";
import { useEffect, useRef, useState } from "react";
@@ -12,8 +11,11 @@ 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, type EntityTableProps } from "../EntityTable";
import type { EntityTableProps } from "../EntityTable";
import type { ResponseObject } from "modules/ModuleApi";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
import type { TFieldApi } from "ui/modules/data/components/EntityForm";
// @todo: allow clear if not required
export function EntityRelationalFormField({
@@ -23,7 +25,7 @@ export function EntityRelationalFormField({
disabled,
tabIndex,
}: {
fieldApi: FieldApi<any, any>;
fieldApi: TFieldApi;
field: RelationField;
data?: EntityData;
disabled?: boolean;
@@ -151,8 +153,13 @@ export function EntityRelationalFormField({
<span className="opacity-60 text-nowrap">
{field.getLabel()}:
</span>{" "}
{_value !== null && typeof value !== "undefined" ? (
<span className="text-nowrap truncate">{_value}</span>
{_value !== null && typeof _value !== "undefined" ? (
<ErrorBoundary
fallback={JSON.stringify(_value)}
suppressError
>
<span className="text-nowrap truncate">{_value}</span>
</ErrorBoundary>
) : (
<span className="opacity-30 text-nowrap font-mono mt-0.5">
null
@@ -190,8 +197,15 @@ type PropoverTableProps = Omit<EntityTableProps, "data"> & {
container: ResponseObject;
query: any;
toggle: () => void;
}
const PopoverTable = ({ container, entity, query, toggle, onClickRow, onClickPage }: PropoverTableProps) => {
};
const PopoverTable = ({
container,
entity,
query,
toggle,
onClickRow,
onClickPage,
}: PropoverTableProps) => {
function handleNext() {
if (query.limit * query.page < container.meta?.count) {
onClickPage?.(query.page + 1);
@@ -212,7 +226,7 @@ const PopoverTable = ({ container, entity, query, toggle, onClickRow, onClickPag
return (
<div>
<EntityTable
<EntityTable2
classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }}
data={container ?? []}
entity={entity}

View File

@@ -18,21 +18,21 @@ export const ModalActions = ["entity", "relation", "media"] as const;
export const entitySchema = Type.Composite([
Type.Object({
name: StringIdentifier
name: StringIdentifier,
}),
entitiesSchema
entitiesSchema,
]);
const schemaAction = Type.Union([
StringEnum(["entity", "relation", "media"]),
Type.String({ pattern: "^template-" })
Type.String({ pattern: "^template-" }),
]);
export type TSchemaAction = Static<typeof schemaAction>;
const createFieldSchema = Type.Object({
entity: StringIdentifier,
name: StringIdentifier,
field: Type.Array(fieldsSchema)
field: Type.Array(fieldsSchema),
});
export type TFieldCreate = Static<typeof createFieldSchema>;
@@ -42,30 +42,30 @@ const createModalSchema = Type.Object(
initial: Type.Optional(Type.Any()),
entities: Type.Optional(
Type.Object({
create: Type.Optional(Type.Array(entitySchema))
})
create: Type.Optional(Type.Array(entitySchema)),
}),
),
relations: Type.Optional(
Type.Object({
create: Type.Optional(Type.Array(Type.Union(relationsSchema)))
})
create: Type.Optional(Type.Array(Type.Union(relationsSchema))),
}),
),
fields: Type.Optional(
Type.Object({
create: Type.Optional(Type.Array(createFieldSchema))
})
)
create: Type.Optional(Type.Array(createFieldSchema)),
}),
),
},
{
additionalProperties: false
}
additionalProperties: false,
},
);
export type TCreateModalSchema = Static<typeof createModalSchema>;
export function CreateModal({
context,
id,
innerProps: { initialPath = [], initialState }
innerProps: { initialPath = [], initialState },
}: ContextModalProps<{ initialPath?: string[]; initialState?: TCreateModalSchema }>) {
const [path, setPath] = useState<string[]>(initialPath);
console.log("...", initialPath, initialState);
@@ -111,7 +111,7 @@ CreateModal.defaultTitle = undefined;
CreateModal.modalProps = {
withCloseButton: false,
size: "xl",
padding: 0
padding: 0,
} satisfies Partial<ModalProps>;
export { ModalBody, ModalFooter, ModalTitle, useStepContext, relationsSchema };

View File

@@ -4,7 +4,7 @@ import {
IconAugmentedReality,
IconBox,
IconCirclesRelation,
IconInfoCircle
IconInfoCircle,
} from "@tabler/icons-react";
import { ucFirst } from "core/utils";
import { useEffect, useState } from "react";
@@ -37,8 +37,8 @@ export function StepCreate() {
type: "Entity",
name: entity.name,
json: entity,
run: async () => await $data.actions.entity.add(entity.name, entity)
}))
run: async () => await $data.actions.entity.add(entity.name, entity),
})),
);
}
if (state.fields?.create) {
@@ -52,8 +52,8 @@ export function StepCreate() {
run: async () =>
await $data.actions.entity
.patch(field.entity)
.fields.add(field.name, field.field as any)
}))
.fields.add(field.name, field.field as any),
})),
);
}
if (state.relations?.create) {
@@ -64,8 +64,8 @@ export function StepCreate() {
type: "Relation",
name: `${rel.source} -> ${rel.target} (${rel.type})`,
json: rel,
run: async () => await $data.actions.relations.add(rel)
}))
run: async () => await $data.actions.relations.add(rel),
})),
);
}
@@ -92,7 +92,7 @@ export function StepCreate() {
items,
states.length,
items.length,
states.every((s) => s === true)
states.every((s) => s === true),
);
if (items.length === states.length && states.every((s) => s === true)) {
b.actions.reload().then(close);
@@ -116,7 +116,7 @@ export function StepCreate() {
nextLabel="Create"
next={{
onClick: handleCreate,
disabled: submitting
disabled: submitting,
}}
prev={{ onClick: stepBack, disabled: submitting }}
debug={{ state }}
@@ -142,7 +142,7 @@ const SummaryItem: React.FC<SummaryItemProps> = ({
json,
state,
action,
initialExpanded = false
initialExpanded = false,
}) => {
const [expanded, handlers] = useDisclosure(initialExpanded);
const error = typeof state !== "undefined" && state !== true;
@@ -153,7 +153,7 @@ const SummaryItem: React.FC<SummaryItemProps> = ({
className={twMerge(
"flex flex-col border border-muted rounded bg-background mb-2",
error && "bg-red-500/20",
done && "bg-green-500/20"
done && "bg-green-500/20",
)}
>
<div className="flex flex-row gap-4 px-2 py-2 items-center">

View File

@@ -8,7 +8,7 @@ import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelec
import { useEvent } from "ui/hooks/use-event";
import {
EntityFieldsForm,
type EntityFieldsFormRef
type EntityFieldsFormRef,
} from "ui/routes/data/forms/entity.fields.form";
import { ModalBody, ModalFooter, type TCreateModalSchema, useStepContext } from "./CreateModal";
@@ -25,9 +25,9 @@ export function StepEntityFields() {
fields: defaultFields,
config: {
sort_field: "id",
sort_dir: "asc"
}
})
sort_dir: "asc",
},
}),
);
const {
control,
@@ -35,17 +35,16 @@ export function StepEntityFields() {
getValues,
handleSubmit,
watch,
setValue
setValue,
} = useForm({
mode: "onTouched",
resolver: typeboxResolver(schema),
defaultValues: initial as NonNullable<Schema>
defaultValues: initial as NonNullable<Schema>,
});
const values = watch();
const updateListener = useEvent((data: TAppDataEntityFields) => {
console.log("updateListener", data);
setValue("fields", data as any);
});
@@ -58,14 +57,15 @@ export function StepEntityFields() {
return {
...prev,
entities: {
create: [getValues() as any]
}
create: [getValues() as any],
},
};
});
console.log("valid");
nextStep("create")();
} else {
console.warn("not valid");
console.warn("not valid", ref.current?.getErrors());
}
}
@@ -119,7 +119,7 @@ export function StepEntityFields() {
<ModalFooter
next={{
disabled: !isValid,
type: "submit"
type: "submit",
//onClick: handleNext
}}
prev={{ onClick: stepBack }}

View File

@@ -8,7 +8,7 @@ import {
ModalFooter,
type TCreateModalSchema,
entitySchema,
useStepContext
useStepContext,
} from "./CreateModal";
export function StepEntity() {
@@ -18,7 +18,7 @@ export function StepEntity() {
const { register, handleSubmit, formState, watch } = useForm({
mode: "onTouched",
resolver: typeboxResolver(entitySchema),
defaultValues: state.entities?.create?.[0] ?? {}
defaultValues: state.entities?.create?.[0] ?? {},
});
/*const data = watch();
console.log("state", { isValid });
@@ -83,7 +83,7 @@ export function StepEntity() {
<ModalFooter
next={{
type: "submit",
disabled: !formState.isValid
disabled: !formState.isValid,
//onClick:
}}
prev={{ onClick: stepBack }}

View File

@@ -7,7 +7,7 @@ import {
StringEnum,
StringIdentifier,
Type,
registerCustomTypeboxKinds
registerCustomTypeboxKinds,
} from "core/utils";
import { ManyToOneRelation, type RelationType, RelationTypes } from "data";
import { type ReactNode, startTransition, useEffect } from "react";
@@ -32,30 +32,30 @@ const Relations: {
{
type: RelationTypes.ManyToOne,
label: "Many to One",
component: ManyToOne
component: ManyToOne,
},
{
type: RelationTypes.OneToOne,
label: "One to One",
component: OneToOne
component: OneToOne,
},
{
type: RelationTypes.ManyToMany,
label: "Many to Many",
component: ManyToMany
component: ManyToMany,
},
{
type: RelationTypes.Polymorphic,
label: "Polymorphic",
component: Polymorphic
}
component: Polymorphic,
},
];
const schema = Type.Object({
type: StringEnum(Relations.map((r) => r.type)),
source: StringIdentifier,
target: StringIdentifier,
config: Type.Object({})
config: Type.Object({}),
});
type ComponentCtx<T extends FieldValues = FieldValues> = {
@@ -75,10 +75,10 @@ export function StepRelation() {
formState: { isValid },
setValue,
watch,
control
control,
} = useForm({
resolver: typeboxResolver(schema),
defaultValues: (state.relations?.create?.[0] ?? {}) as Static<typeof schema>
defaultValues: (state.relations?.create?.[0] ?? {}) as Static<typeof schema>,
});
const data = watch();
@@ -88,8 +88,8 @@ export function StepRelation() {
return {
...prev,
relations: {
create: [data]
}
create: [data],
},
};
});
console.log("data", data);
@@ -131,7 +131,7 @@ export function StepRelation() {
data={Object.entries(entities ?? {}).map(([name, entity]) => ({
value: name,
label: entity.config?.name ?? name,
disabled: data.target === name
disabled: data.target === name,
}))}
/>
<div className="flex flex-col gap-1">
@@ -159,7 +159,7 @@ export function StepRelation() {
data={Object.entries(entities ?? {}).map(([name, entity]) => ({
value: name,
label: entity.config?.name ?? name,
disabled: data.source === name
disabled: data.source === name,
}))}
/>
</div>
@@ -169,7 +169,7 @@ export function StepRelation() {
Relations.find((r) => r.type === data.type)?.component({
register,
control,
data
data,
})}
</div>
</ModalBody>
@@ -177,7 +177,7 @@ export function StepRelation() {
next={{
type: "submit",
disabled: !isValid,
onClick: handleNext
onClick: handleNext,
}}
prev={{ onClick: stepBack }}
debug={{ state, path, data }}
@@ -267,8 +267,8 @@ function OneToOne({
data: {
source,
target,
config: { mappedBy, required }
}
config: { mappedBy, required },
},
}: ComponentCtx) {
return (
<>

View File

@@ -7,7 +7,7 @@ import {
ModalFooter,
type TCreateModalSchema,
type TSchemaAction,
useStepContext
useStepContext,
} from "./CreateModal";
import Templates from "./templates/register";
@@ -70,7 +70,7 @@ export function StepSelect() {
<ModalFooter
next={{
onClick: selected && nextStep(selected),
disabled: !selected
disabled: !selected,
}}
prev={{ onClick: stepBack }}
prevLabel="Cancel"
@@ -87,7 +87,7 @@ const RadioCard = ({
onClick,
selected,
compact = false,
disabled = false
disabled = false,
}: {
Icon: IconType;
title: string;
@@ -104,7 +104,7 @@ const RadioCard = ({
"flex gap-3 border border-primary/10 rounded cursor-pointer",
compact ? "flex-row p-4 items-center" : "flex-col p-5",
selected ? "bg-primary/10 border-primary/50" : "hover:bg-primary/5",
disabled && "opacity-50"
disabled && "opacity-50",
)}
>
<Icon className="size-10" />

View File

@@ -6,7 +6,7 @@ import {
StringEnum,
StringIdentifier,
Type,
transformObject
transformObject,
} from "core/utils";
import type { MediaFieldConfig } from "media/MediaField";
import { useEffect, useState } from "react";
@@ -20,14 +20,14 @@ import {
ModalFooter,
type TCreateModalSchema,
type TFieldCreate,
useStepContext
useStepContext,
} from "../../CreateModal";
const schema = Type.Object({
entity: StringIdentifier,
cardinality_type: StringEnum(["single", "multiple"], { default: "multiple" }),
cardinality: Type.Optional(Type.Number({ minimum: 1 })),
name: StringIdentifier
name: StringIdentifier,
});
type TCreateModalMediaSchema = Static<typeof schema>;
@@ -38,11 +38,11 @@ export function TemplateMediaComponent() {
handleSubmit,
formState: { isValid, errors },
watch,
control
control,
} = useForm({
mode: "onChange",
resolver: typeboxResolver(schema),
defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema
defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema,
});
const [forbidden, setForbidden] = useState<boolean>(false);
@@ -50,7 +50,7 @@ export function TemplateMediaComponent() {
const media_enabled = config.media.enabled ?? false;
const media_entity = config.media.entity_name ?? "media";
const entities = transformObject(config.data.entities ?? {}, (entity, name) =>
name !== media_entity ? entity : undefined
name !== media_entity ? entity : undefined,
);
const data = watch();
const forbidden_field_names = Object.keys(config.data.entities?.[data.entity]?.fields ?? {});
@@ -66,7 +66,7 @@ export function TemplateMediaComponent() {
setState((prev) => ({
...prev,
fields: { create: [field] },
relations: { create: [relation] }
relations: { create: [relation] },
}));
nextStep("create")();
@@ -92,7 +92,7 @@ export function TemplateMediaComponent() {
required
data={Object.entries(entities).map(([name, entity]) => ({
value: name,
label: entity.config?.name ?? name
label: entity.config?.name ?? name,
}))}
/>
<MantineRadio.Group
@@ -141,10 +141,10 @@ export function TemplateMediaComponent() {
<ModalFooter
next={{
type: "submit",
disabled: !isValid || !media_enabled || forbidden
disabled: !isValid || !media_enabled || forbidden,
}}
prev={{
onClick: stepBack
onClick: stepBack,
}}
debug={{ state, path, data }}
/>
@@ -169,9 +169,9 @@ function convert(media_entity: string, data: TCreateModalMediaSchema) {
hidden: false,
mime_types: [],
virtual: true,
entity: data.entity
}
}
entity: data.entity,
},
},
};
const relation = {
@@ -180,8 +180,8 @@ function convert(media_entity: string, data: TCreateModalMediaSchema) {
target: media_entity,
config: {
mappedBy: data.name,
targetCardinality: data.cardinality_type === "single" ? 1 : undefined
}
targetCardinality: data.cardinality_type === "single" ? 1 : undefined,
},
};
if (data.cardinality_type === "multiple") {

View File

@@ -5,5 +5,5 @@ export const TemplateMediaMeta = {
id: "template-media",
title: "Attach Media",
description: "Attach media to an entity",
Icon: TbPhoto
Icon: TbPhoto,
} satisfies StepTemplate;

View File

@@ -1,5 +1,6 @@
import type { IconType } from "react-icons";
import { TemplateMediaComponent, TemplateMediaMeta } from "./media";
import type { ReactNode } from "react";
export type StepTemplate = {
id: string;
@@ -8,8 +9,6 @@ export type StepTemplate = {
Icon: IconType;
};
const Templates: [() => JSX.Element, StepTemplate][] = [
[TemplateMediaComponent, TemplateMediaMeta]
];
const Templates: [() => ReactNode, StepTemplate][] = [[TemplateMediaComponent, TemplateMediaMeta]];
export default Templates;

View File

@@ -13,7 +13,7 @@ export function useEntityForm({
action = "update",
entity,
initialData,
onSubmitted
onSubmitted,
}: EntityFormProps) {
const data = initialData ?? {};
// @todo: check if virtual must be filtered
@@ -32,14 +32,14 @@ export function useEntityForm({
entity.isValidData(value, action, {
explain: true,
// unknown will later be removed in getChangeSet
ignoreUnknown: true
ignoreUnknown: true,
});
return undefined;
} catch (e) {
//console.log("---validation error", e);
return (e as Error).message;
}
}
},
},
onSubmit: async ({ value, formApi }) => {
//console.log("onSubmit", value);
@@ -55,7 +55,7 @@ export function useEntityForm({
// only submit change set if there were changes
await onSubmitted?.(Object.keys(changeSet).length === 0 ? undefined : changeSet);
}
},
});
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {

View File

@@ -10,7 +10,7 @@ import {
addEdge,
useEdgesState,
useNodesState,
useStore
useStore,
} from "@xyflow/react";
import { type Execution, ExecutionEvent, ExecutionState, type Flow, type Task } from "flows";
import { transform } from "lodash-es";
@@ -24,8 +24,8 @@ export default function FlowCanvas({
flow,
execution,
options = {
theme: "dark"
}
theme: "dark",
},
}: {
flow: Flow;
execution: Execution | undefined;
@@ -70,9 +70,9 @@ function RenderedFlow({ nodes, edges, nodeTypes, execution, theme }: any) {
state: {
// @ts-ignore
...node.data.state,
event
}
}
event,
},
},
};
}
@@ -90,9 +90,9 @@ function RenderedFlow({ nodes, edges, nodeTypes, execution, theme }: any) {
state: {
// @ts-ignore
...node.data.state,
event: undefined
}
}
event: undefined,
},
},
}));
});
} else {
@@ -131,7 +131,7 @@ function RenderedFlow({ nodes, edges, nodeTypes, execution, theme }: any) {
fitView
fitViewOptions={{ maxZoom: 1 }}
proOptions={{
hideAttribution: true
hideAttribution: true,
}}
>
<Controls>

View File

@@ -15,11 +15,11 @@ const triggerSchemas = Object.values(
Type.Object(
{
type: Const(name),
config: trigger.cls.schema
config: trigger.cls.schema,
},
{ title: String(name), additionalProperties: false }
)
)
{ title: String(name), additionalProperties: false },
),
),
);
export function TriggerComponent({

View File

@@ -14,7 +14,7 @@ export function TaskForm({ task, onChange, ...props }: TaskFormProps) {
const uiSchema = Object.fromEntries(
Object.keys(schema.properties).map((key) => {
return [key, { "ui:field": "template", "ui:fieldReplacesAnyOrOneOf": true }];
})
}),
);
//console.log("uiSchema", uiSchema);

View File

@@ -9,14 +9,14 @@ type Mode = (typeof modes)[number];
export function TemplateField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
F extends FormContextType = any,
>(props: FieldProps<T, S, F>) {
const formData = props.formData;
const hasMarkup = SimpleRenderer.hasMarkup(formData!);
const [mode, setMode] = useState<Mode>(hasMarkup ? "code" : "field");
const [values, setValues] = useState<Record<Mode, any>>({
field: hasMarkup ? "" : formData,
code: hasMarkup ? formData : ""
code: hasMarkup ? formData : "",
});
//console.log("TemplateField", props);
const { SchemaField } = props.registry.fields;
@@ -35,7 +35,7 @@ export function TemplateField<
let _schema: any = schema;
if (!("anyOf" in schema)) {
_schema = {
anyOf: [schema, { type: "string" }]
anyOf: [schema, { type: "string" }],
};
}
@@ -48,7 +48,7 @@ export function TemplateField<
: {
"ui:label": false,
"ui:widget": "textarea",
"ui:options": { rows: 1 }
"ui:options": { rows: 1 },
};
return (

View File

@@ -10,8 +10,8 @@ export function RenderTaskComponent(props: TaskComponentProps) {
onChange={console.log}
uiSchema={{
render: {
"ui:field": "LiquidJsField"
}
"ui:field": "LiquidJsField",
},
}}
/>
</TaskComponent>

View File

@@ -29,7 +29,7 @@ export const KeyValueInput: React.FC<KeyValueInputProps> = ({
onChange,
error,
classNames,
mode = "object"
mode = "object",
}) => {
const [items, setItems] = useState(initialValue ? toItems(initialValue) : [ITEM]);

View File

@@ -34,7 +34,7 @@ const Wrapper = ({ children, className, ...props }: ElementProps<"div">) => (
{...props}
className={twMerge(
"flex flex-row bg-lightest border ring-2 ring-muted/5 border-muted rounded-full items-center p-1",
className
className,
)}
>
{children}
@@ -71,7 +71,7 @@ const Text = forwardRef<any, ElementProps<"span"> & { mono?: boolean }>(
>
{children}
</span>
)
),
);
FlowPanel.Wrapper = Wrapper;

View File

@@ -36,7 +36,7 @@ export function BaseNode({ children, className, tabs, Icon, isInvalid, ...props
"w-96",
//props.selected && "ring-4 ring-blue-500/15",
isInvalid && "ring-8 ring-red-500/15",
className
className,
)}
>
<Header
@@ -70,7 +70,7 @@ const BaseNodeTabs = ({ tabs }: { tabs: BaseNodeProps["tabs"] }) => {
onClick={handleClick(i)}
className={twMerge(
"text-sm leading-none",
i === active ? "font-bold opacity-80" : "font-medium opacity-50"
i === active ? "font-bold opacity-80" : "font-medium opacity-50",
)}
>
{tab.label}
@@ -90,7 +90,7 @@ const Header = ({
rightSection,
initialValue,
changable = false,
onChange
onChange,
}: {
Icon: React.FC<any>;
iconProps?: ElementProps<"svg">;
@@ -129,7 +129,7 @@ const Header = ({
disabled={!changable}
onChange={handleChange}
className={twMerge(
"font-mono font-semibold bg-transparent rounded-lg outline-none pl-1.5 w-full hover:bg-lightest/30 transition-colors focus:bg-lightest/60"
"font-mono font-semibold bg-transparent rounded-lg outline-none pl-1.5 w-full hover:bg-lightest/30 transition-colors focus:bg-lightest/60",
)}
/>
) : (

View File

@@ -6,18 +6,18 @@ export function Handle(props: Omit<HandleProps, "position">) {
width: 10,
height: 10,
background: "transparent",
border: "2px solid #999"
border: "2px solid #999",
};
const offset = -10;
const styles = {
target: {
...base,
left: offset
left: offset,
},
source: {
...base,
right: offset
}
right: offset,
},
};
//console.log("type", props.type, styles[props.type]);

View File

@@ -15,15 +15,15 @@ const nodes = [
params: {
method: "GET",
headers: [],
url: ""
}
}
url: "",
},
},
},
{
type: "render",
label: "Render",
description: "Render data using LiquidJS"
}
description: "Render data using LiquidJS",
},
];
export function SelectNode(props) {
@@ -46,13 +46,13 @@ export function SelectNode(props) {
type: "task",
data: {
...node.template,
label
}
label,
},
};
}
return n;
})
}),
);
setTimeout(() => {
reactflow.setEdges((prev) =>
@@ -62,12 +62,12 @@ export function SelectNode(props) {
return {
...e,
id: "task-" + label,
target: "task-" + label
target: "task-" + label,
};
}
return e;
})
}),
);
}, 100);
@@ -92,7 +92,7 @@ export function SelectNode(props) {
key={node.type}
className={twMerge(
"border border-primary/10 rounded-md py-2 px-4 hover:bg-primary/10",
selected === node.type && "bg-primary/10"
selected === node.type && "bg-primary/10",
)}
onClick={() => setSelected(node.type)}
>

View File

@@ -5,5 +5,5 @@ import { TriggerNode } from "./triggers/TriggerNode";
export const nodeTypes = {
select: SelectNode,
trigger: TriggerNode,
task: TaskNode
task: TaskNode,
};

View File

@@ -19,8 +19,8 @@ import { BaseNode } from "../BaseNode";
const schema = Type.Composite([
FetchTask.schema,
Type.Object({
query: Type.Optional(Type.Record(Type.String(), Type.String()))
})
query: Type.Optional(Type.Record(Type.String(), Type.String())),
}),
]);
type TFetchTaskSchema = Static<typeof FetchTask.schema>;
@@ -39,11 +39,11 @@ export function FetchTaskForm({ onChange, params, ...props }: FetchTaskFormProps
getValues,
formState: { isValid, errors },
watch,
control
control,
} = useForm({
resolver: typeboxResolver(schema),
defaultValues: params as Static<typeof schema>,
mode: "onChange"
mode: "onChange",
//defaultValues: (state.relations?.create?.[0] ?? {}) as Static<typeof schema>
});
@@ -130,11 +130,11 @@ const TaskNodeTabs = ({ watch }: any) => [
<div className="scroll-auto">
<JsonViewer json={watch()} expand={2} className="bg-white break-all" />
</div>
)
),
},
{
id: "test",
label: "test",
content: () => <div>test</div>
}
content: () => <div>test</div>,
},
];

View File

@@ -11,7 +11,7 @@ registerCustomTypeboxKinds(TypeRegistry);
const TaskComponents = {
fetch: FetchTaskForm,
render: RenderNode
render: RenderNode,
};
export const TaskNode = (
@@ -24,10 +24,10 @@ export const TaskNode = (
responding?: boolean;
}
>
>
>,
) => {
const {
data: { label, start, last, responding }
data: { label, start, last, responding },
} = props;
const task = useFlowSelector((s) => s.flow!.tasks![label])!;
const { actions } = useFlowCanvas();

View File

@@ -9,7 +9,7 @@ import {
StringEnum,
Type,
registerCustomTypeboxKinds,
transformObject
transformObject,
} from "core/utils";
import { TriggerMap } from "flows";
import type { TAppFlowTriggerSchema } from "flows/AppFlows";
@@ -33,18 +33,18 @@ const schema = Type.Object({
Type.Object(
{
type: Const(name),
config: trigger.cls.schema
config: trigger.cls.schema,
},
{ title: String(name), additionalProperties: false }
)
)
)
)
{ title: String(name), additionalProperties: false },
),
),
),
),
});
export const TriggerNode = (props: NodeProps<Node<TAppFlowTriggerSchema & { label: string }>>) => {
const {
data: { label, ...trigger }
data: { label, ...trigger },
} = props;
//console.log("TriggerNode");
const state = useFlowSelector((s) => s.flow!.trigger!);
@@ -57,11 +57,11 @@ export const TriggerNode = (props: NodeProps<Node<TAppFlowTriggerSchema & { labe
getValues,
formState: { isValid, errors },
watch,
control
control,
} = useForm({
resolver: typeboxResolver(schema),
defaultValues: { trigger: state } as Static<typeof schema>,
mode: "onChange"
mode: "onChange",
});
const data = watch("trigger");
@@ -91,7 +91,7 @@ export const TriggerNode = (props: NodeProps<Node<TAppFlowTriggerSchema & { labe
data={[
{ label: "Manual", value: "manual" },
{ label: "HTTP", value: "http" },
{ label: "Event", value: "event", disabled: true }
{ label: "Event", value: "event", disabled: true },
]}
name="trigger.type"
control={control}
@@ -101,7 +101,7 @@ export const TriggerNode = (props: NodeProps<Node<TAppFlowTriggerSchema & { labe
defaultValue="async"
data={[
{ label: "Async", value: "async" },
{ label: "Sync", value: "sync" }
{ label: "Sync", value: "sync" },
]}
name="trigger.config.mode"
control={control}
@@ -148,7 +148,7 @@ const Http = ({ form }) => {
data={[
{ label: "JSON", value: "json" },
{ label: "HTML", value: "html" },
{ label: "Text", value: "text" }
{ label: "Text", value: "text" },
]}
name="trigger.config.response_type"
control={form.control}
@@ -162,11 +162,11 @@ const TriggerNodeTabs = ({ data, ...props }) => [
{
id: "json",
label: "JSON",
content: () => <JsonViewer json={data} expand={2} className="" />
content: () => <JsonViewer json={data} expand={2} className="" />,
},
{
id: "test",
label: "test",
content: () => <div>test</div>
}
content: () => <div>test</div>,
},
];

View File

@@ -41,7 +41,7 @@ export type TFlowState = {
export const flowStateAtom = atom<TFlowState>({
dirty: false,
name: undefined,
flow: undefined
flow: undefined,
});
const FlowCanvasContext = createContext<FlowContextType>(undefined!);
@@ -65,7 +65,7 @@ export function FlowCanvasProvider({ children, name }: { children: any; name?: s
setName: async (name: string) => {
console.log("set name", name);
setFlowState((state) => ({ ...state, name, dirty: true }));
}
},
},
trigger: {
update: async (trigger: TAppFlowTriggerSchema | any) => {
@@ -75,7 +75,7 @@ export function FlowCanvasProvider({ children, name }: { children: any; name?: s
return { ...state, dirty: true, flow: { ...flow, trigger } };
});
//return s.actions.patch("flows", `flows.flows.${name}`, { trigger });
}
},
},
task: {
create: async (name: string, defaults: object = {}) => {
@@ -95,12 +95,12 @@ export function FlowCanvasProvider({ children, name }: { children: any; name?: s
return {
...state,
dirty: true,
flow: { ...flow, tasks: { ...flow.tasks, [name]: task } }
flow: { ...flow, tasks: { ...flow.tasks, [name]: task } },
};
});
//return s.actions.patch("flows", `flows.flows.${name}.tasks.${name}`, task);
}
}
},
},
};
return (
@@ -120,7 +120,7 @@ export function useFlowCanvasState() {
export function useFlowSelector<Reduced = TFlowState>(
selector: (state: TFlowState) => Reduced,
equalityFn: (a: any, b: any) => boolean = isEqual
equalityFn: (a: any, b: any) => boolean = isEqual,
) {
const selected = selectAtom(flowStateAtom, useCallback(selector, []), equalityFn);
return useAtom(selected)[0];
@@ -133,8 +133,8 @@ export function flowToNodes(flow: TAppFlowSchema, name: string): Node<TFlowNodeD
data: { label: name, type: flow.trigger.type },
type: "trigger",
dragHandle: ".drag-handle",
position: { x: 0, y: 0 }
}
position: { x: 0, y: 0 },
},
];
let i = 1;
@@ -150,7 +150,7 @@ export function flowToNodes(flow: TAppFlowSchema, name: string): Node<TFlowNodeD
type: "task",
dragHandle: ".drag-handle",
// @todo: this is currently static
position: { x: 450 * i + (i - 1) * 64, y: 0 }
position: { x: 450 * i + (i - 1) * 64, y: 0 },
});
i++;
}
@@ -178,14 +178,14 @@ export function flowToEdges(flow: TAppFlowSchema) {
target: `task-${tasks[0]?.[0]}`,
//type: "smoothstep",
style: {
strokeWidth: 2
strokeWidth: 2,
},
markerEnd: {
type: MarkerType.ArrowClosed,
width: 10,
height: 10
}
}
height: 10,
},
},
]
: [];
@@ -195,13 +195,13 @@ export function flowToEdges(flow: TAppFlowSchema) {
source: "task-" + connection.source,
target: "task-" + connection.target,
style: {
strokeWidth: 2
strokeWidth: 2,
},
markerEnd: {
type: MarkerType.ArrowClosed,
width: 10,
height: 10
}
height: 10,
},
});
}

View File

@@ -2,5 +2,5 @@ import { atom } from "jotai";
const flowStateAtom = atom({
dirty: false,
flow: undefined
flow: undefined,
});

View File

@@ -29,7 +29,7 @@ export function getFlowNodes(flow: Flow): Node[] {
type: "trigger",
position: { x: 0, y: 0 },
data: { trigger: flow.trigger },
dragHandle: ".drag-handle"
dragHandle: ".drag-handle",
});
console.log("adding node", { id: "trigger" });
@@ -47,7 +47,7 @@ export function getFlowNodes(flow: Flow): Node[] {
type: task.type,
position: { x: xs[j]!, y: (i + 1) * spacing.y },
data: { task, state: { i: 0, isRespondingTask, isStartTask, event: undefined } },
dragHandle: ".drag-handle"
dragHandle: ".drag-handle",
});
});
});
@@ -63,7 +63,7 @@ export function getFlowEdges(flow: Flow): Edge[] {
edges.push({
id: `trigger-${startTask.name}${new Date().getTime()}`,
source: "trigger",
target: startTask.name
target: startTask.name,
//type: "",
});
@@ -72,7 +72,7 @@ export function getFlowEdges(flow: Flow): Edge[] {
edges.push({
id: `${c.source.name}-${c.target.name}${new Date().getTime()}`,
source: c.source.name,
target: c.target.name
target: c.target.name,
//type: "",
});
});
@@ -84,6 +84,6 @@ export function getNodeTypes(flow: Flow) {
trigger: TriggerComponent,
render: RenderTaskComponent,
log: TaskComponent,
fetch: TaskComponent
fetch: TaskComponent,
};
}