changed media settings to new form

This commit is contained in:
dswbx
2025-02-05 11:04:37 +01:00
parent f432473ed9
commit 4b3493a6f5
18 changed files with 110 additions and 1031 deletions

View File

@@ -1,5 +1,12 @@
import { Form, useFormContext } from "json-schema-form-react";
import { omit } from "lodash-es";
import {
AnyOf,
Field,
Form,
FormContextOverride,
ObjectField,
Subscribe
} from "ui/components/form/json-schema-form";
import { useState } from "react";
import { useBknd } from "ui/client/BkndProvider";
import { useBkndMedia } from "ui/client/schema/media/use-bknd-media";
@@ -7,16 +14,13 @@ import { Button } from "ui/components/buttons/Button";
import { JsonViewer } from "ui/components/code/JsonViewer";
import { Message } from "ui/components/display/Message";
import * as Formy from "ui/components/form/Formy";
import { BooleanInputMantine } from "ui/components/form/Formy/BooleanInputMantine";
import { JsonSchemaForm } from "ui/components/form/json-schema";
import { TypeboxValidator } from "ui/components/form/json-schema-form";
import { AutoForm, Field } from "ui/components/form/json-schema-form/components/Field";
import type { ValueError } from "@sinclair/typebox/value";
import { type TSchema, Value } from "core/utils";
import { Media } from "ui/elements";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell";
const validator = new TypeboxValidator();
export function MediaSettings(props) {
useBrowserTitle(["Media", "Settings"]);
@@ -31,92 +35,70 @@ export function MediaSettings(props) {
function MediaSettingsInternal() {
const { config, schema } = useBkndMedia();
const [data, setData] = useState<any>(config);
//console.log("schema", schema);
console.log("data", data);
return (
<>
<Form
schema={schema}
validator={validator}
onChange={setData}
defaultValues={config as any}
>
{({ errors, submitting, dirty }) => (
<>
<Form schema={schema} onChange={setData} initialValues={config as any}>
<Subscribe>
{({ dirty }) => (
<AppShell.SectionHeader
right={
<Button variant="primary" disabled={!dirty || submitting}>
<Button variant="primary" disabled={!dirty /* || submitting*/}>
Update
</Button>
}
>
Settings
</AppShell.SectionHeader>
<AppShell.Scrollable>
<div className="flex flex-col gap-3 p-3">
<Field name="enabled" />
<div className="flex flex-col gap-3 relative">
<Overlay visible={!data.enabled} />
<Field name="entity_name" />
<Field name="storage.body_max_size" title="Storage Body Max Size" />
</div>
</div>
<div className="flex flex-col gap-3 p-3 mt-3 border-t border-muted">
<Overlay visible={!data.enabled} />
<Adapters />
</div>
<JsonViewer json={data} expand={999} />
</AppShell.Scrollable>
</>
)}
)}
</Subscribe>
<AppShell.Scrollable>
<div className="flex flex-col gap-3 p-3">
<Field name="enabled" />
<div className="flex flex-col gap-3 relative">
<Overlay visible={!data.enabled} />
<Field name="entity_name" />
<Field name="storage.body_max_size" label="Storage Body Max Size" />
</div>
</div>
<div className="flex flex-col gap-3 p-3 mt-3 border-t border-muted">
<Overlay visible={!data.enabled} />
<AnyOf.Root path="adapter">
<Adapters />
</AnyOf.Root>
</div>
<JsonViewer json={JSON.parse(JSON.stringify(data))} expand={999} />
</AppShell.Scrollable>
</Form>
</>
);
}
function Adapters() {
const { config, schema } = useBkndMedia();
const ctx = useFormContext();
const current = config.adapter;
const schemas = schema.properties.adapter.anyOf;
const types = schemas.map((s) => s.properties.type.const) as string[];
const currentType = current?.type ?? (types[0] as string);
const [selected, setSelected] = useState<string>(currentType);
const $schema = schemas.find((s) => s.properties.type.const === selected);
console.log("$schema", $schema);
function onChangeSelect(e) {
setSelected(e.target.value);
// wait quickly for the form to update before triggering a change
setTimeout(() => {
ctx.setValue("adapter.type", e.target.value);
}, 10);
}
const ctx = AnyOf.useContext();
return (
<div>
<Formy.Select value={selected} onChange={onChangeSelect}>
{types.map((type) => (
<option key={type} value={type}>
{type}
</option>
<>
<div className="flex flex-row gap-1">
{ctx.schemas?.map((schema: any, i) => (
<Button
key={i}
onClick={() => ctx.select(i)}
variant={ctx.selected === i ? "primary" : "default"}
>
{schema.title ?? `Option ${i + 1}`}
</Button>
))}
</Formy.Select>
<div>current: {selected}</div>
<div>options: {schemas.map((s) => s.title).join(", ")}</div>
<Field name="adapter.type" defaultValue={selected} hidden />
{$schema && <AutoForm schema={$schema?.properties.config} prefix="adapter.config" />}
<hr />
{/*{$schema && (
<div data-ignore>
<JsonSchemaForm
schema={omit($schema?.properties.config, "title")}
className="legacy hide-required-mark fieldset-alternative mute-root"
/>
</div>
)}*/}
</div>
</div>
{ctx.selected !== null && (
<FormContextOverride schema={ctx.selectedSchema} path={ctx.path} overrideData>
<Field name="type" hidden />
<ObjectField path="config" wrapperProps={{ label: false, wrapper: "group" }} />
</FormContextOverride>
)}
</>
);
}

View File

@@ -1,6 +1,6 @@
import AppShellAccordionsTest from "ui/routes/test/tests/appshell-accordions-test";
import JsonSchemaFormReactTest from "ui/routes/test/tests/json-schema-form-react-test";
import JsonSchemaForm2 from "ui/routes/test/tests/json-schema-form2";
import SwaggerTest from "ui/routes/test/tests/swagger-test";
import SWRAndAPI from "ui/routes/test/tests/swr-and-api";
import SwrAndDataApi from "ui/routes/test/tests/swr-and-data-api";
@@ -48,7 +48,6 @@ const tests = {
SwrAndDataApi,
DropzoneElementTest,
JsonSchemaFormReactTest,
JsonSchemaForm2,
JsonSchemaForm3
} as const;

View File

@@ -1,287 +0,0 @@
import { Popover } from "@mantine/core";
import { IconBug } from "@tabler/icons-react";
import { autoFormatString } from "core/utils";
import type { JSONSchema } from "json-schema-to-ts";
import { type ChangeEvent, type ComponentPropsWithoutRef, useState } from "react";
import { IconButton } from "ui/components/buttons/IconButton";
import { JsonViewer } from "ui/components/code/JsonViewer";
import * as Formy from "ui/components/form/Formy";
import { ErrorMessage } from "ui/components/form/Formy";
import {
Form,
useFieldContext,
useFormContext,
usePrefixContext
} from "ui/components/form/json-schema-form2/Form";
import { isType } from "ui/components/form/json-schema-form2/utils";
const schema = {
type: "object",
properties: {
name: { type: "string", maxLength: 2 },
description: { type: "string", maxLength: 2 },
age: { type: "number", description: "Age of you" },
deep: {
type: "object",
properties: {
nested: { type: "string", maxLength: 2 }
}
}
}
//required: ["description"]
} as const satisfies JSONSchema;
const simpleSchema = {
type: "object",
properties: {
tags: {
type: "array",
items: {
type: "string"
}
}
}
} as const satisfies JSONSchema;
export default function JsonSchemaForm2() {
return (
<div className="flex flex-col p-3">
<h1>Form</h1>
{/*<Form
schema={schema}
onChange={console.log}
validateOn="change"
className="flex flex-col gap-2"
>
<input name="name" placeholder="name" />
<Field name="age" />
<Field name="description" />
<input name="deep/nested" placeholder="deep/nested" />
<div className="border border-red-500 p-3">
<ObjectField prefix="deep" />
</div>
</Form>
<hr />*/}
{/*<Form
schema={{
type: "object",
properties: {
name: { type: "string", maxLength: 2 },
description: { type: "string", maxLength: 2 },
age: { type: "number", description: "Age of you" },
deep: {
type: "object",
properties: {
nested: { type: "string", maxLength: 2 }
}
}
}
}}
onChange={console.log}
initialValues={{ age: 12, deep: { nested: "test" } }}
validateOn="change"
className="flex flex-col gap-2"
>
<AutoForm />
</Form>*/}
{/*<Form
schema={{
type: "object",
properties: {
bla: { type: "string" },
type: { type: "string", enum: ["a", "b"] }
}
}}
onChange={console.log}
validateOn="change"
className="flex flex-col gap-2"
>
<AutoForm />
</Form>*/}
<Form
schema={{
type: "object",
properties: {
bla: {
anyOf: [
{ type: "string", maxLength: 2, title: "String" },
{ type: "number", title: "Number" }
]
}
}
}}
onChange={console.log}
validateOn="change"
className="flex flex-col gap-2"
>
<AutoForm />
</Form>
{/*<Form schema={simpleSchema} validateOn="change" className="flex flex-col gap-2">
<AutoForm />
</Form>*/}
</div>
);
}
const Field = ({
name = "",
schema: _schema
}: { name?: string; schema?: Exclude<JSONSchema, boolean> }) => {
const { value, errors, pointer, required, ...ctx } = useFieldContext(name);
const schema = _schema ?? ctx.schema;
if (!schema) return `"${name}" (${pointer}) has no schema`;
if (isType(schema.type, ["object", "array"])) {
return null;
}
const label = schema.title ?? name; //autoFormatString(name.split("/").pop());
return (
<Formy.Group error={errors.length > 0}>
<Formy.Label>
{label} {required ? "*" : ""}
</Formy.Label>
<div className="flex flex-row gap-2">
<div className="flex flex-1 flex-col">
<FieldComponent
schema={schema}
name={pointer}
placeholder={pointer}
required={required}
/>
</div>
<Popover>
<Popover.Target>
<IconButton Icon={IconBug} />
</Popover.Target>
<Popover.Dropdown>
<JsonViewer
json={{ pointer, value, required, schema, errors }}
expand={6}
className="p-0"
/>
</Popover.Dropdown>
</Popover>
</div>
{schema.description && <Formy.Help>{schema.description}</Formy.Help>}
{errors.length > 0 && (
<Formy.ErrorMessage>{errors.map((e) => e.message).join(", ")}</Formy.ErrorMessage>
)}
</Formy.Group>
);
};
const FieldComponent = ({
schema,
...props
}: { schema: JSONSchema } & ComponentPropsWithoutRef<"input">) => {
if (!schema || typeof schema === "boolean") return null;
const common = {};
if (schema.enum) {
if (!Array.isArray(schema.enum)) return null;
return <Formy.Select {...(props as any)} options={schema.enum} />;
}
if (isType(schema.type, ["number", "integer"])) {
return <Formy.Input type="number" {...props} />;
}
return <Formy.Input {...props} />;
};
const ObjectField = ({ path = "" }: { path?: string }) => {
const { schema } = usePrefixContext(path);
if (!schema) return null;
const properties = schema.properties ?? {};
const label = schema.title ?? path;
console.log("object", { path, schema, properties });
return (
<fieldset className="border border-muted p-3 flex flex-col gap-2">
<legend>Object: {label}</legend>
{Object.keys(properties).map((prop) => {
const schema = properties[prop];
const pointer = `${path}/${prop}`;
console.log("--", prop, pointer, schema);
if (schema.anyOf || schema.oneOf) {
return <AnyOfField key={pointer} path={pointer} />;
}
if (isType(schema.type, "object")) {
console.log("object", { prop, pointer, schema });
return <ObjectField key={pointer} path={pointer} />;
}
if (isType(schema.type, "array")) {
return <ArrayField key={pointer} path={pointer} />;
}
return <Field key={pointer} name={pointer} />;
})}
</fieldset>
);
};
const AnyOfField = ({ path = "" }: { path?: string }) => {
const [selected, setSelected] = useState<number | null>(null);
const { schema, select } = usePrefixContext(path);
if (!schema) return null;
const schemas = schema.anyOf ?? schema.oneOf ?? [];
const options = schemas.map((s, i) => ({
value: i,
label: s.title ?? `Option ${i + 1}`
}));
const selectSchema = {
enum: options
};
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
const i = e.target.value ? Number(e.target.value) : null;
setSelected(i);
select(path, i !== null ? i : undefined);
}
console.log("options", options, schemas, selected !== null && schemas[selected]);
return (
<>
<div>
anyOf: {path} ({selected})
</div>
<FieldComponent schema={selectSchema as any} onChange={handleSelect} />
{selected !== null && (
<Field key={`${path}_${selected}`} schema={schemas[selected]} name={path} />
)}
</>
);
};
const ArrayField = ({ path = "" }: { path?: string }) => {
return "array: " + path;
};
const AutoForm = ({ prefix = "" }: { prefix?: string }) => {
const { schema } = usePrefixContext(prefix);
if (!schema) return null;
if (isType(schema.type, "object")) {
return <ObjectField path={prefix} />;
}
if (isType(schema.type, "array")) {
return <ArrayField path={prefix} />;
}
return <Field name={prefix} />;
};

View File

@@ -1,140 +1,14 @@
import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button";
import { AnyOf, useAnyOfContext } from "ui/components/form/json-schema-form3/AnyOfField";
import { Field } from "ui/components/form/json-schema-form3/Field";
import { Form, FormContextOverride } from "ui/components/form/json-schema-form3/Form";
import { ObjectField } from "ui/components/form/json-schema-form3/ObjectField";
import { removeKeyRecursively } from "ui/components/form/json-schema-form3/utils";
import {
AnyOf,
Field,
Form,
FormContextOverride,
ObjectField
} from "ui/components/form/json-schema-form";
import { Scrollable } from "ui/layouts/AppShell/AppShell";
const mediaSchema = {
additionalProperties: false,
type: "object",
properties: {
enabled: {
default: false,
type: "boolean"
},
basepath: {
default: "/api/media",
type: "string"
},
entity_name: {
default: "media",
type: "string"
},
storage: {
default: {},
type: "object",
properties: {
body_max_size: {
description: "Max size of the body in bytes. Leave blank for unlimited.",
type: "number"
}
}
},
adapter: {
anyOf: [
{
title: "s3",
additionalProperties: false,
type: "object",
properties: {
type: {
default: "s3",
const: "s3",
readOnly: true,
type: "string"
},
config: {
title: "S3",
type: "object",
properties: {
access_key: {
type: "string"
},
secret_access_key: {
type: "string"
},
url: {
pattern: "^https?://[^/]+",
description: "URL to S3 compatible endpoint without trailing slash",
examples: [
"https://{account_id}.r2.cloudflarestorage.com/{bucket}",
"https://{bucket}.s3.{region}.amazonaws.com"
],
type: "string"
}
},
required: ["access_key", "secret_access_key", "url"]
}
},
required: ["type", "config"]
},
{
title: "cloudinary",
additionalProperties: false,
type: "object",
properties: {
type: {
default: "cloudinary",
const: "cloudinary",
readOnly: true,
type: "string"
},
config: {
title: "Cloudinary",
type: "object",
properties: {
cloud_name: {
type: "string"
},
api_key: {
type: "string"
},
api_secret: {
type: "string"
},
upload_preset: {
type: "string"
}
},
required: ["cloud_name", "api_key", "api_secret"]
}
},
required: ["type", "config"]
},
{
title: "local",
additionalProperties: false,
type: "object",
properties: {
type: {
default: "local",
const: "local",
readOnly: true,
type: "string"
},
config: {
title: "Local",
type: "object",
properties: {
path: {
default: "./",
type: "string"
}
},
required: ["path"]
}
},
required: ["type", "config"]
}
]
}
},
required: ["enabled", "basepath", "entity_name", "storage"]
};
export default function JsonSchemaForm3() {
const { schema, config } = useBknd();
@@ -287,6 +161,7 @@ export default function JsonSchemaForm3() {
function CustomMediaForm() {
const { schema, config } = useBknd();
return (
<Form schema={schema.media} initialValues={config.media} className="flex flex-col gap-3">
<Field name="enabled" />
@@ -301,7 +176,7 @@ function CustomMediaForm() {
}
function CustomMediaFormAdapter() {
const ctx = useAnyOfContext();
const ctx = AnyOf.useContext();
return (
<>