mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
changed media settings to new form
This commit is contained in:
@@ -2,7 +2,7 @@ import type { JSONSchema } from "json-schema-to-ts";
|
|||||||
import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react";
|
import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field";
|
import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field";
|
||||||
import { useFieldContext } from "./Form";
|
import { FormContextOverride, useFieldContext } from "./Form";
|
||||||
import { getLabel, getMultiSchemaMatched } from "./utils";
|
import { getLabel, getMultiSchemaMatched } from "./utils";
|
||||||
|
|
||||||
export type AnyOfFieldRootProps = {
|
export type AnyOfFieldRootProps = {
|
||||||
@@ -30,7 +30,7 @@ export const useAnyOfContext = () => {
|
|||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
||||||
const { setValue, pointer, lib, value, ...ctx } = useFieldContext(path);
|
const { setValue, pointer, lib, value, ...ctx } = useFieldContext(path);
|
||||||
const schema = _schema ?? ctx.schema;
|
const schema = _schema ?? ctx.schema;
|
||||||
if (!schema) return `AnyOfField(${path}): no schema ${pointer}`;
|
if (!schema) return `AnyOfField(${path}): no schema ${pointer}`;
|
||||||
@@ -58,7 +58,7 @@ export const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootPro
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Select = () => {
|
const Select = () => {
|
||||||
const { selected, select, path, schema, selectSchema } = useAnyOfContext();
|
const { selected, select, path, schema, selectSchema } = useAnyOfContext();
|
||||||
|
|
||||||
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
|
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
|
||||||
@@ -80,17 +80,13 @@ export const Select = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
|
const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
|
||||||
const { selected, selectedSchema, path } = useAnyOfContext();
|
const { selected, selectedSchema, path } = useAnyOfContext();
|
||||||
if (selected === null) return null;
|
if (selected === null) return null;
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormContextOverride path={path} schema={selectedSchema} overrideData>
|
||||||
key={`${path}_${selected}`}
|
<FormField key={`${path}_${selected}`} name={""} label={false} {...props} />
|
||||||
schema={selectedSchema}
|
</FormContextOverride>
|
||||||
name={path}
|
|
||||||
label={false}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import { Draft2019, type JsonError } from "json-schema-library";
|
|||||||
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
|
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
|
||||||
import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types";
|
import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types";
|
||||||
import type { JSONSchema as $JSONSchema, FromSchema } from "json-schema-to-ts";
|
import type { JSONSchema as $JSONSchema, FromSchema } from "json-schema-to-ts";
|
||||||
import { get } from "lodash-es";
|
import { get, isEqual } from "lodash-es";
|
||||||
import * as immutable from "object-path-immutable";
|
import * as immutable from "object-path-immutable";
|
||||||
import {
|
import {
|
||||||
type ComponentPropsWithoutRef,
|
type ComponentPropsWithoutRef,
|
||||||
@@ -37,6 +37,7 @@ export type FormContext<Data> = {
|
|||||||
setValue: (pointer: string, value: any) => void;
|
setValue: (pointer: string, value: any) => void;
|
||||||
deleteValue: (pointer: string) => void;
|
deleteValue: (pointer: string) => void;
|
||||||
errors: JsonError[];
|
errors: JsonError[];
|
||||||
|
dirty: boolean;
|
||||||
schema: JSONSchema;
|
schema: JSONSchema;
|
||||||
lib: Draft2019;
|
lib: Draft2019;
|
||||||
};
|
};
|
||||||
@@ -48,7 +49,7 @@ export function Form<
|
|||||||
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
||||||
>({
|
>({
|
||||||
schema,
|
schema,
|
||||||
initialValues,
|
initialValues: _initialValues,
|
||||||
initialOpts,
|
initialOpts,
|
||||||
children,
|
children,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -57,9 +58,9 @@ export function Form<
|
|||||||
...props
|
...props
|
||||||
}: FormProps<Schema, Data>) {
|
}: FormProps<Schema, Data>) {
|
||||||
const lib = new Draft2019(schema);
|
const lib = new Draft2019(schema);
|
||||||
const [data, setData] = useState<Partial<Data>>(
|
const initialValues = _initialValues ?? lib.getTemplate(undefined, schema, initialOpts);
|
||||||
initialValues ?? lib.getTemplate(undefined, schema, initialOpts)
|
const [data, setData] = useState<Partial<Data>>(initialValues);
|
||||||
);
|
const [dirty, setDirty] = useState<boolean>(false);
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||||||
const [errors, setErrors] = useState<JsonError[]>([]);
|
const [errors, setErrors] = useState<JsonError[]>([]);
|
||||||
|
|
||||||
@@ -72,10 +73,11 @@ export function Form<
|
|||||||
|
|
||||||
function setValue(pointer: string, value: any) {
|
function setValue(pointer: string, value: any) {
|
||||||
const normalized = normalizePath(pointer);
|
const normalized = normalizePath(pointer);
|
||||||
console.log("setValue", { pointer, normalized, value });
|
//console.log("setValue", { pointer, normalized, value });
|
||||||
const key = normalized.substring(2).replace(/\//g, ".");
|
const key = normalized.substring(2).replace(/\//g, ".");
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
const changed = immutable.set(prev, key, value);
|
const changed = immutable.set(prev, key, value);
|
||||||
|
onChange?.(changed, key, value);
|
||||||
//console.log("changed", prev, changed, { key, value });
|
//console.log("changed", prev, changed, { key, value });
|
||||||
return changed;
|
return changed;
|
||||||
});
|
});
|
||||||
@@ -86,12 +88,15 @@ export function Form<
|
|||||||
const key = normalized.substring(2).replace(/\//g, ".");
|
const key = normalized.substring(2).replace(/\//g, ".");
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
const changed = immutable.del(prev, key);
|
const changed = immutable.del(prev, key);
|
||||||
|
onChange?.(changed, key, undefined);
|
||||||
//console.log("changed", prev, changed, { key });
|
//console.log("changed", prev, changed, { key });
|
||||||
return changed;
|
return changed;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setDirty(!isEqual(initialValues, data));
|
||||||
|
|
||||||
if (validateOn === "change") {
|
if (validateOn === "change") {
|
||||||
validate();
|
validate();
|
||||||
}
|
}
|
||||||
@@ -107,6 +112,7 @@ export function Form<
|
|||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
data: data ?? {},
|
data: data ?? {},
|
||||||
|
dirty,
|
||||||
setData,
|
setData,
|
||||||
setValue,
|
setValue,
|
||||||
deleteValue,
|
deleteValue,
|
||||||
@@ -194,3 +200,8 @@ export function useFieldContext(name: string) {
|
|||||||
required
|
required
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Subscribe({ children }: { children: (ctx: FormContext<any>) => ReactNode }) {
|
||||||
|
const ctx = useFormContext();
|
||||||
|
return children(ctx);
|
||||||
|
}
|
||||||
@@ -36,10 +36,10 @@ export const ObjectField = ({
|
|||||||
if (!schema) return;
|
if (!schema) return;
|
||||||
|
|
||||||
if (schema.anyOf || schema.oneOf) {
|
if (schema.anyOf || schema.oneOf) {
|
||||||
return <AnyOfField key={pointer} path={pointer} schema={schema} />;
|
return <AnyOfField key={pointer} path={pointer} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Field key={pointer} name={pointer} schema={schema} />;
|
return <Field key={pointer} name={pointer} />;
|
||||||
})}
|
})}
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
);
|
);
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { Switch } from "@mantine/core";
|
|
||||||
import { autoFormatString } from "core/utils";
|
|
||||||
import { type JSONSchema, useFieldContext, useFormContext } from "json-schema-form-react";
|
|
||||||
import type { ComponentPropsWithoutRef } from "react";
|
|
||||||
import * as Formy from "ui/components/form/Formy";
|
|
||||||
|
|
||||||
// make a local version of JSONSchema that is always an object
|
|
||||||
export type FieldProps = JSONSchema & {
|
|
||||||
name: string;
|
|
||||||
defaultValue?: any;
|
|
||||||
hidden?: boolean;
|
|
||||||
overrides?: ComponentPropsWithoutRef<"input">;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Field(p: FieldProps) {
|
|
||||||
const { schema, defaultValue, required } = useFieldContext(p.name);
|
|
||||||
const props = {
|
|
||||||
...(typeof schema === "object" ? schema : {}),
|
|
||||||
defaultValue,
|
|
||||||
required,
|
|
||||||
...p
|
|
||||||
} as FieldProps;
|
|
||||||
console.log("schema", p.name, schema, defaultValue);
|
|
||||||
|
|
||||||
const field = renderField(props);
|
|
||||||
const label = props.title
|
|
||||||
? props.title
|
|
||||||
: autoFormatString(
|
|
||||||
props.name?.includes(".") ? (props.name.split(".").pop() as string) : props.name
|
|
||||||
);
|
|
||||||
|
|
||||||
return p.hidden ? (
|
|
||||||
field
|
|
||||||
) : (
|
|
||||||
<Formy.Group>
|
|
||||||
<Formy.Label>
|
|
||||||
{label}
|
|
||||||
{props.required ? " *" : ""}
|
|
||||||
</Formy.Label>
|
|
||||||
{field}
|
|
||||||
{props.description ? <Formy.Help>{props.description}</Formy.Help> : null}
|
|
||||||
</Formy.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isType(_type: JSONSchema["type"], _compare: JSONSchema["type"]) {
|
|
||||||
if (!_type || !_compare) return false;
|
|
||||||
const type = Array.isArray(_type) ? _type : [_type];
|
|
||||||
const compare = Array.isArray(_compare) ? _compare : [_compare];
|
|
||||||
return compare.some((t) => type.includes(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderField(props: FieldProps) {
|
|
||||||
//console.log("renderField", props.name, props);
|
|
||||||
const common = {
|
|
||||||
name: props.name,
|
|
||||||
defaultValue: typeof props.defaultValue !== "undefined" ? props.defaultValue : props.default
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
if (props.hidden) {
|
|
||||||
common.type = "hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isType(props.type, "boolean")) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<Switch
|
|
||||||
disabled={props.disabled}
|
|
||||||
id={props.id}
|
|
||||||
defaultChecked={props.defaultValue}
|
|
||||||
name={props.name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (isType(props.type, ["number", "integer"])) {
|
|
||||||
return <Formy.Input type="number" {...common} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Formy.Input type="text" {...common} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AutoForm({ schema, prefix = "" }: { schema: JSONSchema; prefix?: string }) {
|
|
||||||
const required = schema.required ?? [];
|
|
||||||
const properties = schema.properties ?? {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/*<pre>{JSON.stringify(schema, null, 2)}</pre>;*/}
|
|
||||||
<div>
|
|
||||||
{Object.keys(properties).map((name) => {
|
|
||||||
const field = properties[name];
|
|
||||||
const _name = `${prefix ? prefix + "." : ""}${name}`;
|
|
||||||
return <Field key={_name} name={_name} {...(field as any)} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
export { TypeboxValidator, type ValueError } from "./validators/tb-validator";
|
export * from "./Field";
|
||||||
export { CfValidator, type OutputUnit } from "./validators/cf-validator";
|
export * from "./Form";
|
||||||
|
export * from "./ObjectField";
|
||||||
|
export * from "./ArrayField";
|
||||||
|
export * from "./AnyOfField";
|
||||||
|
export * from "./FieldWrapper";
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function normalizePath(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function prefixPointer(pointer: string, prefix: string) {
|
export function prefixPointer(pointer: string, prefix: string) {
|
||||||
return pointer.replace("#/", `#/${prefix}/`);
|
return pointer.replace("#/", `#/${prefix}/`).replace(/\/\//g, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getParentPointer(pointer: string) {
|
export function getParentPointer(pointer: string) {
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { type Schema as JsonSchema, type OutputUnit, Validator } from "@cfworker/json-schema";
|
|
||||||
import type { Validator as TValidator } from "json-schema-form-react";
|
|
||||||
|
|
||||||
export class CfValidator implements TValidator<OutputUnit> {
|
|
||||||
async validate(schema: JsonSchema, data: any) {
|
|
||||||
const result = new Validator(schema).validate(data);
|
|
||||||
return result.errors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { OutputUnit };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { ValueError } from "@sinclair/typebox/value";
|
|
||||||
import { type TSchema, Value } from "core/utils";
|
|
||||||
import type { Validator } from "json-schema-form-react";
|
|
||||||
|
|
||||||
export class TypeboxValidator implements Validator<ValueError> {
|
|
||||||
async validate(schema: TSchema, data: any) {
|
|
||||||
return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { ValueError };
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
import { Draft2019, type JsonError, type JsonSchema as LibJsonSchema } from "json-schema-library";
|
|
||||||
import type { JSONSchema as $JSONSchema, FromSchema } from "json-schema-to-ts";
|
|
||||||
import {
|
|
||||||
type ComponentPropsWithoutRef,
|
|
||||||
type FormEvent,
|
|
||||||
createContext,
|
|
||||||
startTransition,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from "react";
|
|
||||||
import { flatten, getFormTarget, isRequired, normalizePath, unflatten } from "./utils";
|
|
||||||
|
|
||||||
type JSONSchema = Exclude<$JSONSchema, boolean>;
|
|
||||||
type TFormData = Record<string, string>;
|
|
||||||
|
|
||||||
export type FormProps<
|
|
||||||
Schema extends JSONSchema = JSONSchema,
|
|
||||||
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
|
||||||
> = Omit<ComponentPropsWithoutRef<"form">, "onChange"> & {
|
|
||||||
schema: Schema;
|
|
||||||
validateOn?: "change" | "submit";
|
|
||||||
initialValues?: Partial<Data>;
|
|
||||||
onChange?: (data: Partial<Data>, name: string, value: any) => void;
|
|
||||||
hiddenSubmit?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormContext = {
|
|
||||||
data: TFormData;
|
|
||||||
setData: (data: TFormData) => void;
|
|
||||||
errors: JsonError[];
|
|
||||||
schema: JSONSchema;
|
|
||||||
lib: Draft2019;
|
|
||||||
select: (pointer: string, choice: number | undefined) => void;
|
|
||||||
selections: Record<string, number | undefined>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormContext = createContext<FormContext>(undefined!);
|
|
||||||
|
|
||||||
export function Form<
|
|
||||||
Schema extends JSONSchema = JSONSchema,
|
|
||||||
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
|
|
||||||
>({
|
|
||||||
schema: _schema,
|
|
||||||
initialValues: _initialValues,
|
|
||||||
children,
|
|
||||||
onChange,
|
|
||||||
validateOn = "submit",
|
|
||||||
hiddenSubmit = true,
|
|
||||||
...props
|
|
||||||
}: FormProps<Schema, Data>) {
|
|
||||||
const schema = useMemo(() => _schema, [JSON.stringify(_schema)]);
|
|
||||||
const initialValues = useMemo(
|
|
||||||
() => (_initialValues ? flatten(_initialValues) : {}),
|
|
||||||
[JSON.stringify(_initialValues)]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [data, setData] = useState<TFormData>(initialValues);
|
|
||||||
const [errors, setErrors] = useState<JsonError[]>([]);
|
|
||||||
const [selections, setSelections] = useState<Record<string, number | undefined>>({});
|
|
||||||
const lib = new Draft2019(schema);
|
|
||||||
const formRef = useRef<HTMLFormElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("setting", initialValues);
|
|
||||||
if (formRef.current) {
|
|
||||||
Object.entries(initialValues).forEach(([name, value]) => {
|
|
||||||
const pointer = normalizePath(name);
|
|
||||||
const input = formRef.current?.elements.namedItem(pointer);
|
|
||||||
if (input && "value" in input) {
|
|
||||||
input.value = value as any;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [initialValues]);
|
|
||||||
|
|
||||||
async function handleChange(e: FormEvent<HTMLFormElement>) {
|
|
||||||
const target = getFormTarget(e);
|
|
||||||
if (!target) return;
|
|
||||||
const name = normalizePath(target.name);
|
|
||||||
|
|
||||||
startTransition(() => {
|
|
||||||
const newData = { ...data, [name]: target.value };
|
|
||||||
setData(newData);
|
|
||||||
|
|
||||||
const actual = unflatten(newData, schema, selections);
|
|
||||||
if (validateOn === "change") {
|
|
||||||
validate(actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange?.(actual, name, target.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault();
|
|
||||||
const actual = unflatten(data, schema, selections);
|
|
||||||
const { data: newData, errors } = validate(actual);
|
|
||||||
setData(newData);
|
|
||||||
console.log("submit", newData, errors);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate(_data?: object) {
|
|
||||||
const actual = _data ?? unflatten(data, schema, selections);
|
|
||||||
const errors = lib.validate(actual);
|
|
||||||
console.log("validate", actual, errors);
|
|
||||||
setErrors(errors);
|
|
||||||
return {
|
|
||||||
data: actual,
|
|
||||||
errors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(pathOrPointer: string, choice: number | undefined) {
|
|
||||||
setSelections((prev) => ({ ...prev, [normalizePath(pathOrPointer)]: choice }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
data,
|
|
||||||
setData,
|
|
||||||
select,
|
|
||||||
selections,
|
|
||||||
errors,
|
|
||||||
schema,
|
|
||||||
lib
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<form {...props} ref={formRef} onChange={handleChange} onSubmit={handleSubmit}>
|
|
||||||
<FormContext.Provider value={context}>{children}</FormContext.Provider>
|
|
||||||
{hiddenSubmit && (
|
|
||||||
<button style={{ visibility: "hidden" }} type="submit">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
||||||
<pre>{JSON.stringify(unflatten(data, schema, selections), null, 2)}</pre>
|
|
||||||
<pre>{JSON.stringify(errors, null, 2)}</pre>
|
|
||||||
<pre>{JSON.stringify(selections, null, 2)}</pre>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFormContext() {
|
|
||||||
return useContext(FormContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFieldContext(name: string) {
|
|
||||||
const { data, setData, lib, schema, errors: formErrors, select, selections } = useFormContext();
|
|
||||||
const pointer = normalizePath(name);
|
|
||||||
//console.log("pointer", pointer);
|
|
||||||
const value = data[pointer];
|
|
||||||
const errors = formErrors.filter((error) => error.data.pointer === pointer);
|
|
||||||
const fieldSchema = pointer === "#/" ? (schema as LibJsonSchema) : lib.getSchema({ pointer });
|
|
||||||
const required = isRequired(pointer, schema);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
setValue: (value: any) => setData({ ...data, [name]: value }),
|
|
||||||
errors,
|
|
||||||
schema: fieldSchema,
|
|
||||||
pointer,
|
|
||||||
required,
|
|
||||||
select,
|
|
||||||
selections
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePrefixContext(prefix: string) {
|
|
||||||
const { data, setData, lib, schema, errors: formErrors, select, selections } = useFormContext();
|
|
||||||
const pointer = normalizePath(prefix);
|
|
||||||
const value = Object.fromEntries(Object.entries(data).filter(([key]) => key.startsWith(prefix)));
|
|
||||||
const errors = formErrors.filter((error) => error.data.pointer.startsWith(pointer));
|
|
||||||
const fieldSchema = pointer === "#/" ? (schema as LibJsonSchema) : lib.getSchema({ pointer });
|
|
||||||
const required = isRequired(pointer, schema);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
//setValue: (value: any) => setData({ ...data, [name]: value }),
|
|
||||||
errors,
|
|
||||||
schema: fieldSchema,
|
|
||||||
pointer,
|
|
||||||
required,
|
|
||||||
select,
|
|
||||||
selections
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { autoFormatString } from "core/utils";
|
|
||||||
import { Draft2019, type JsonSchema } from "json-schema-library";
|
|
||||||
import type { JSONSchema } from "json-schema-to-ts";
|
|
||||||
import type { JSONSchemaType } from "json-schema-to-ts/lib/types/definitions/jsonSchema";
|
|
||||||
import { set } from "lodash-es";
|
|
||||||
import type { FormEvent } from "react";
|
|
||||||
|
|
||||||
export function getFormTarget(e: FormEvent<HTMLFormElement>) {
|
|
||||||
const form = e.currentTarget;
|
|
||||||
const target = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
|
|
||||||
|
|
||||||
// check if target has attribute "data-ignore" set
|
|
||||||
// also check if target is within a "data-ignore" element
|
|
||||||
|
|
||||||
if (
|
|
||||||
!target ||
|
|
||||||
!form.contains(target) ||
|
|
||||||
!target.name ||
|
|
||||||
target.hasAttribute("data-ignore") ||
|
|
||||||
target.closest("[data-ignore]")
|
|
||||||
) {
|
|
||||||
return; // Ignore events from outside the form
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flatten(obj: any, parentKey = "", result: any = {}): any {
|
|
||||||
for (const key in obj) {
|
|
||||||
if (key in obj) {
|
|
||||||
const newKey = parentKey ? `${parentKey}/${key}` : "#/" + key;
|
|
||||||
if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
||||||
flatten(obj[key], newKey, result);
|
|
||||||
} else if (Array.isArray(obj[key])) {
|
|
||||||
obj[key].forEach((item, index) => {
|
|
||||||
const arrayKey = `${newKey}.${index}`;
|
|
||||||
if (typeof item === "object" && item !== null) {
|
|
||||||
flatten(item, arrayKey, result);
|
|
||||||
} else {
|
|
||||||
result[arrayKey] = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result[newKey] = obj[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @todo: make sure it's in the right order
|
|
||||||
export function unflatten(
|
|
||||||
obj: Record<string, string>,
|
|
||||||
schema: JSONSchema,
|
|
||||||
selections?: Record<string, number | undefined>
|
|
||||||
) {
|
|
||||||
const result = {};
|
|
||||||
const lib = new Draft2019(schema as any);
|
|
||||||
for (const pointer in obj) {
|
|
||||||
const required = isRequired(pointer, schema);
|
|
||||||
let subschema = lib.getSchema({ pointer });
|
|
||||||
console.log("subschema", pointer, subschema, selections);
|
|
||||||
if (!subschema) {
|
|
||||||
throw new Error(`"${pointer}" not found in schema`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if subschema starts with "anyOf" or "oneOf"
|
|
||||||
if (subschema.anyOf || subschema.oneOf) {
|
|
||||||
const selected = selections?.[pointer];
|
|
||||||
if (selected !== undefined) {
|
|
||||||
subschema = subschema.anyOf ? subschema.anyOf[selected] : subschema.oneOf![selected];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = coerce(obj[pointer], subschema as any, { required });
|
|
||||||
|
|
||||||
set(result, pointer.substring(2).replace(/\//g, "."), value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coerce(
|
|
||||||
value: any,
|
|
||||||
schema: Exclude<JSONSchema, boolean>,
|
|
||||||
opts?: { required?: boolean }
|
|
||||||
) {
|
|
||||||
if (!value && typeof opts?.required === "boolean" && !opts.required) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (schema.type) {
|
|
||||||
case "string":
|
|
||||||
return String(value);
|
|
||||||
case "integer":
|
|
||||||
case "number":
|
|
||||||
return Number(value);
|
|
||||||
case "boolean":
|
|
||||||
return ["true", "1", 1, "on"].includes(value);
|
|
||||||
case "null":
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* normalizes any path to a full json pointer
|
|
||||||
*
|
|
||||||
* examples: in -> out
|
|
||||||
* description -> #/description
|
|
||||||
* #/description -> #/description
|
|
||||||
* /description -> #/description
|
|
||||||
* nested/property -> #/nested/property
|
|
||||||
* nested.property -> #/nested/property
|
|
||||||
* nested.property[0] -> #/nested/property/0
|
|
||||||
* nested.property[0].name -> #/nested/property/0/name
|
|
||||||
* @param path
|
|
||||||
*/
|
|
||||||
export function normalizePath(path: string) {
|
|
||||||
return path.startsWith("#/")
|
|
||||||
? path
|
|
||||||
: `#/${path.replace(/#?\/?/, "").replace(/\./g, "/").replace(/\[/g, "/").replace(/\]/g, "")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getParentPointer(pointer: string) {
|
|
||||||
return pointer.substring(0, pointer.lastIndexOf("/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRequired(pointer: string, schema: JSONSchema, data?: any) {
|
|
||||||
if (pointer === "#/") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const lib = new Draft2019(schema as any);
|
|
||||||
|
|
||||||
const childSchema = lib.getSchema({ pointer, data });
|
|
||||||
if (typeof childSchema === "object" && ("const" in childSchema || "enum" in childSchema)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentPointer = getParentPointer(pointer);
|
|
||||||
const parentSchema = lib.getSchema({ pointer: parentPointer, data });
|
|
||||||
const required = parentSchema?.required?.includes(pointer.split("/").pop()!);
|
|
||||||
|
|
||||||
console.log("isRequired", {
|
|
||||||
pointer,
|
|
||||||
parentPointer,
|
|
||||||
parent: parentSchema ? JSON.parse(JSON.stringify(parentSchema)) : null,
|
|
||||||
required
|
|
||||||
});
|
|
||||||
|
|
||||||
return !!required;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TType = JSONSchemaType | JSONSchemaType[] | readonly JSONSchemaType[] | undefined;
|
|
||||||
export function isType(_type: TType, _compare: TType) {
|
|
||||||
if (!_type || !_compare) return false;
|
|
||||||
const type = Array.isArray(_type) ? _type : [_type];
|
|
||||||
const compare = Array.isArray(_compare) ? _compare : [_compare];
|
|
||||||
return compare.some((t) => type.includes(t));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLabel(name: string, schema: JSONSchema) {
|
|
||||||
if (typeof schema === "object" && "title" in schema) return schema.title;
|
|
||||||
const label = name.includes("/") ? (name.split("/").pop() ?? "") : name;
|
|
||||||
return autoFormatString(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMultiSchema(schema: JSONSchema): Exclude<JSONSchema, boolean>[] | undefined {
|
|
||||||
if (!schema || typeof schema !== "object") return;
|
|
||||||
return (schema.anyOf ?? schema.oneOf) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMultiSchemaMatched(
|
|
||||||
schema: JsonSchema,
|
|
||||||
data: any
|
|
||||||
): [number, Exclude<JSONSchema, boolean>[], Exclude<JSONSchema, boolean> | undefined] {
|
|
||||||
const multiSchema = getMultiSchema(schema);
|
|
||||||
if (!multiSchema) return [-1, [], undefined];
|
|
||||||
const index = multiSchema.findIndex((subschema) => {
|
|
||||||
const lib = new Draft2019(subschema as any);
|
|
||||||
return lib.validate(data, subschema).length === 0;
|
|
||||||
});
|
|
||||||
if (index === -1) return [-1, multiSchema, undefined];
|
|
||||||
|
|
||||||
return [index, multiSchema, multiSchema[index]];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeKeyRecursively<Given extends object>(obj: Given, keyToRemove: string): Given {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
return obj.map((item) => removeKeyRecursively(item, keyToRemove)) as any;
|
|
||||||
} else if (typeof obj === "object" && obj !== null) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(obj)
|
|
||||||
.filter(([key]) => key !== keyToRemove)
|
|
||||||
.map(([key, value]) => [key, removeKeyRecursively(value, keyToRemove)])
|
|
||||||
) as any;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
@@ -6,9 +6,18 @@ import { transform } from "lodash-es";
|
|||||||
import type { ComponentPropsWithoutRef } from "react";
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { Group, Input, Label } from "ui/components/form/Formy/components";
|
import { Group, Input, Label } from "ui/components/form/Formy/components";
|
||||||
import { TypeboxValidator } from "ui/components/form/json-schema-form";
|
|
||||||
import { SocialLink } from "./SocialLink";
|
import { SocialLink } from "./SocialLink";
|
||||||
|
|
||||||
|
import type { ValueError } from "@sinclair/typebox/value";
|
||||||
|
import { type TSchema, Value } from "core/utils";
|
||||||
|
import type { Validator } from "json-schema-form-react";
|
||||||
|
|
||||||
|
class TypeboxValidator implements Validator<ValueError> {
|
||||||
|
async validate(schema: TSchema, data: any) {
|
||||||
|
return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
|
||||||
className?: string;
|
className?: string;
|
||||||
formData?: any;
|
formData?: any;
|
||||||
@@ -75,7 +84,7 @@ export function AuthForm({
|
|||||||
<Form
|
<Form
|
||||||
method={method}
|
method={method}
|
||||||
action={password.action}
|
action={password.action}
|
||||||
{...props}
|
{...(props as any)}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
validator={validator}
|
validator={validator}
|
||||||
validationMode="change"
|
validationMode="change"
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Form, useFormContext } from "json-schema-form-react";
|
import {
|
||||||
import { omit } from "lodash-es";
|
AnyOf,
|
||||||
|
Field,
|
||||||
|
Form,
|
||||||
|
FormContextOverride,
|
||||||
|
ObjectField,
|
||||||
|
Subscribe
|
||||||
|
} from "ui/components/form/json-schema-form";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useBknd } from "ui/client/BkndProvider";
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
import { useBkndMedia } from "ui/client/schema/media/use-bknd-media";
|
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 { JsonViewer } from "ui/components/code/JsonViewer";
|
||||||
import { Message } from "ui/components/display/Message";
|
import { Message } from "ui/components/display/Message";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
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 type { ValueError } from "@sinclair/typebox/value";
|
||||||
import { TypeboxValidator } from "ui/components/form/json-schema-form";
|
import { type TSchema, Value } from "core/utils";
|
||||||
import { AutoForm, Field } from "ui/components/form/json-schema-form/components/Field";
|
|
||||||
import { Media } from "ui/elements";
|
import { Media } from "ui/elements";
|
||||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
|
|
||||||
const validator = new TypeboxValidator();
|
|
||||||
|
|
||||||
export function MediaSettings(props) {
|
export function MediaSettings(props) {
|
||||||
useBrowserTitle(["Media", "Settings"]);
|
useBrowserTitle(["Media", "Settings"]);
|
||||||
|
|
||||||
@@ -31,92 +35,70 @@ export function MediaSettings(props) {
|
|||||||
function MediaSettingsInternal() {
|
function MediaSettingsInternal() {
|
||||||
const { config, schema } = useBkndMedia();
|
const { config, schema } = useBkndMedia();
|
||||||
const [data, setData] = useState<any>(config);
|
const [data, setData] = useState<any>(config);
|
||||||
//console.log("schema", schema);
|
console.log("data", data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form
|
<Form schema={schema} onChange={setData} initialValues={config as any}>
|
||||||
schema={schema}
|
<Subscribe>
|
||||||
validator={validator}
|
{({ dirty }) => (
|
||||||
onChange={setData}
|
|
||||||
defaultValues={config as any}
|
|
||||||
>
|
|
||||||
{({ errors, submitting, dirty }) => (
|
|
||||||
<>
|
|
||||||
<AppShell.SectionHeader
|
<AppShell.SectionHeader
|
||||||
right={
|
right={
|
||||||
<Button variant="primary" disabled={!dirty || submitting}>
|
<Button variant="primary" disabled={!dirty /* || submitting*/}>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</AppShell.SectionHeader>
|
</AppShell.SectionHeader>
|
||||||
<AppShell.Scrollable>
|
)}
|
||||||
<div className="flex flex-col gap-3 p-3">
|
</Subscribe>
|
||||||
<Field name="enabled" />
|
<AppShell.Scrollable>
|
||||||
<div className="flex flex-col gap-3 relative">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<Overlay visible={!data.enabled} />
|
<Field name="enabled" />
|
||||||
<Field name="entity_name" />
|
<div className="flex flex-col gap-3 relative">
|
||||||
<Field name="storage.body_max_size" title="Storage Body Max Size" />
|
<Overlay visible={!data.enabled} />
|
||||||
</div>
|
<Field name="entity_name" />
|
||||||
</div>
|
<Field name="storage.body_max_size" label="Storage Body Max Size" />
|
||||||
<div className="flex flex-col gap-3 p-3 mt-3 border-t border-muted">
|
</div>
|
||||||
<Overlay visible={!data.enabled} />
|
</div>
|
||||||
<Adapters />
|
<div className="flex flex-col gap-3 p-3 mt-3 border-t border-muted">
|
||||||
</div>
|
<Overlay visible={!data.enabled} />
|
||||||
<JsonViewer json={data} expand={999} />
|
<AnyOf.Root path="adapter">
|
||||||
</AppShell.Scrollable>
|
<Adapters />
|
||||||
</>
|
</AnyOf.Root>
|
||||||
)}
|
</div>
|
||||||
|
<JsonViewer json={JSON.parse(JSON.stringify(data))} expand={999} />
|
||||||
|
</AppShell.Scrollable>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Adapters() {
|
function Adapters() {
|
||||||
const { config, schema } = useBkndMedia();
|
const ctx = AnyOf.useContext();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Formy.Select value={selected} onChange={onChangeSelect}>
|
<div className="flex flex-row gap-1">
|
||||||
{types.map((type) => (
|
{ctx.schemas?.map((schema: any, i) => (
|
||||||
<option key={type} value={type}>
|
<Button
|
||||||
{type}
|
key={i}
|
||||||
</option>
|
onClick={() => ctx.select(i)}
|
||||||
|
variant={ctx.selected === i ? "primary" : "default"}
|
||||||
|
>
|
||||||
|
{schema.title ?? `Option ${i + 1}`}
|
||||||
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Formy.Select>
|
</div>
|
||||||
<div>current: {selected}</div>
|
|
||||||
<div>options: {schemas.map((s) => s.title).join(", ")}</div>
|
{ctx.selected !== null && (
|
||||||
<Field name="adapter.type" defaultValue={selected} hidden />
|
<FormContextOverride schema={ctx.selectedSchema} path={ctx.path} overrideData>
|
||||||
{$schema && <AutoForm schema={$schema?.properties.config} prefix="adapter.config" />}
|
<Field name="type" hidden />
|
||||||
<hr />
|
<ObjectField path="config" wrapperProps={{ label: false, wrapper: "group" }} />
|
||||||
{/*{$schema && (
|
</FormContextOverride>
|
||||||
<div data-ignore>
|
)}
|
||||||
<JsonSchemaForm
|
</>
|
||||||
schema={omit($schema?.properties.config, "title")}
|
|
||||||
className="legacy hide-required-mark fieldset-alternative mute-root"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}*/}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import AppShellAccordionsTest from "ui/routes/test/tests/appshell-accordions-test";
|
import AppShellAccordionsTest from "ui/routes/test/tests/appshell-accordions-test";
|
||||||
import JsonSchemaFormReactTest from "ui/routes/test/tests/json-schema-form-react-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 SwaggerTest from "ui/routes/test/tests/swagger-test";
|
||||||
import SWRAndAPI from "ui/routes/test/tests/swr-and-api";
|
import SWRAndAPI from "ui/routes/test/tests/swr-and-api";
|
||||||
import SwrAndDataApi from "ui/routes/test/tests/swr-and-data-api";
|
import SwrAndDataApi from "ui/routes/test/tests/swr-and-data-api";
|
||||||
@@ -48,7 +48,6 @@ const tests = {
|
|||||||
SwrAndDataApi,
|
SwrAndDataApi,
|
||||||
DropzoneElementTest,
|
DropzoneElementTest,
|
||||||
JsonSchemaFormReactTest,
|
JsonSchemaFormReactTest,
|
||||||
JsonSchemaForm2,
|
|
||||||
JsonSchemaForm3
|
JsonSchemaForm3
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -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} />;
|
|
||||||
};
|
|
||||||
@@ -1,140 +1,14 @@
|
|||||||
import { useBknd } from "ui/client/bknd";
|
import { useBknd } from "ui/client/bknd";
|
||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { AnyOf, useAnyOfContext } from "ui/components/form/json-schema-form3/AnyOfField";
|
import {
|
||||||
import { Field } from "ui/components/form/json-schema-form3/Field";
|
AnyOf,
|
||||||
import { Form, FormContextOverride } from "ui/components/form/json-schema-form3/Form";
|
Field,
|
||||||
import { ObjectField } from "ui/components/form/json-schema-form3/ObjectField";
|
Form,
|
||||||
import { removeKeyRecursively } from "ui/components/form/json-schema-form3/utils";
|
FormContextOverride,
|
||||||
|
ObjectField
|
||||||
|
} from "ui/components/form/json-schema-form";
|
||||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
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() {
|
export default function JsonSchemaForm3() {
|
||||||
const { schema, config } = useBknd();
|
const { schema, config } = useBknd();
|
||||||
|
|
||||||
@@ -287,6 +161,7 @@ export default function JsonSchemaForm3() {
|
|||||||
|
|
||||||
function CustomMediaForm() {
|
function CustomMediaForm() {
|
||||||
const { schema, config } = useBknd();
|
const { schema, config } = useBknd();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form schema={schema.media} initialValues={config.media} className="flex flex-col gap-3">
|
<Form schema={schema.media} initialValues={config.media} className="flex flex-col gap-3">
|
||||||
<Field name="enabled" />
|
<Field name="enabled" />
|
||||||
@@ -301,7 +176,7 @@ function CustomMediaForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CustomMediaFormAdapter() {
|
function CustomMediaFormAdapter() {
|
||||||
const ctx = useAnyOfContext();
|
const ctx = AnyOf.useContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user