From 4b3493a6f52bc2bfc20a2df5a4cbd2df68bbaace Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 5 Feb 2025 11:04:37 +0100 Subject: [PATCH] changed media settings to new form --- .../AnyOfField.tsx | 18 +- .../ArrayField.tsx | 0 .../Field.tsx | 0 .../FieldWrapper.tsx | 0 .../Form.tsx | 23 +- .../ObjectField.tsx | 4 +- .../json-schema-form/components/Field.tsx | 98 ------ .../components/form/json-schema-form/index.ts | 8 +- .../utils.ts | 2 +- .../validators/cf-validator.ts | 11 - .../validators/tb-validator.ts | 11 - .../form/json-schema-form2/Form.tsx | 192 ------------ .../form/json-schema-form2/utils.ts | 198 ------------ app/src/ui/elements/auth/AuthForm.tsx | 13 +- app/src/ui/routes/media/media.settings.tsx | 130 ++++---- app/src/ui/routes/test/index.tsx | 3 +- .../routes/test/tests/json-schema-form2.tsx | 287 ------------------ .../routes/test/tests/json-schema-form3.tsx | 143 +-------- 18 files changed, 110 insertions(+), 1031 deletions(-) rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/AnyOfField.tsx (87%) rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/ArrayField.tsx (100%) rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/Field.tsx (100%) rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/FieldWrapper.tsx (100%) rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/Form.tsx (89%) rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/ObjectField.tsx (88%) delete mode 100644 app/src/ui/components/form/json-schema-form/components/Field.tsx rename app/src/ui/components/form/{json-schema-form3 => json-schema-form}/utils.ts (98%) delete mode 100644 app/src/ui/components/form/json-schema-form/validators/cf-validator.ts delete mode 100644 app/src/ui/components/form/json-schema-form/validators/tb-validator.ts delete mode 100644 app/src/ui/components/form/json-schema-form2/Form.tsx delete mode 100644 app/src/ui/components/form/json-schema-form2/utils.ts delete mode 100644 app/src/ui/routes/test/tests/json-schema-form2.tsx diff --git a/app/src/ui/components/form/json-schema-form3/AnyOfField.tsx b/app/src/ui/components/form/json-schema-form/AnyOfField.tsx similarity index 87% rename from app/src/ui/components/form/json-schema-form3/AnyOfField.tsx rename to app/src/ui/components/form/json-schema-form/AnyOfField.tsx index 4844c46..87670cf 100644 --- a/app/src/ui/components/form/json-schema-form3/AnyOfField.tsx +++ b/app/src/ui/components/form/json-schema-form/AnyOfField.tsx @@ -2,7 +2,7 @@ import type { JSONSchema } from "json-schema-to-ts"; import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react"; import * as Formy from "ui/components/form/Formy"; 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"; export type AnyOfFieldRootProps = { @@ -30,7 +30,7 @@ export const useAnyOfContext = () => { 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 schema = _schema ?? ctx.schema; 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(); function handleSelect(e: ChangeEvent) { @@ -80,17 +80,13 @@ export const Select = () => { ); }; -export const Field = ({ name, label, ...props }: Partial) => { +const Field = ({ name, label, ...props }: Partial) => { const { selected, selectedSchema, path } = useAnyOfContext(); if (selected === null) return null; return ( - + + + ); }; diff --git a/app/src/ui/components/form/json-schema-form3/ArrayField.tsx b/app/src/ui/components/form/json-schema-form/ArrayField.tsx similarity index 100% rename from app/src/ui/components/form/json-schema-form3/ArrayField.tsx rename to app/src/ui/components/form/json-schema-form/ArrayField.tsx diff --git a/app/src/ui/components/form/json-schema-form3/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx similarity index 100% rename from app/src/ui/components/form/json-schema-form3/Field.tsx rename to app/src/ui/components/form/json-schema-form/Field.tsx diff --git a/app/src/ui/components/form/json-schema-form3/FieldWrapper.tsx b/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx similarity index 100% rename from app/src/ui/components/form/json-schema-form3/FieldWrapper.tsx rename to app/src/ui/components/form/json-schema-form/FieldWrapper.tsx diff --git a/app/src/ui/components/form/json-schema-form3/Form.tsx b/app/src/ui/components/form/json-schema-form/Form.tsx similarity index 89% rename from app/src/ui/components/form/json-schema-form3/Form.tsx rename to app/src/ui/components/form/json-schema-form/Form.tsx index d7816b2..07b3846 100644 --- a/app/src/ui/components/form/json-schema-form3/Form.tsx +++ b/app/src/ui/components/form/json-schema-form/Form.tsx @@ -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 { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types"; 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 { type ComponentPropsWithoutRef, @@ -37,6 +37,7 @@ export type FormContext = { setValue: (pointer: string, value: any) => void; deleteValue: (pointer: string) => void; errors: JsonError[]; + dirty: boolean; schema: JSONSchema; lib: Draft2019; }; @@ -48,7 +49,7 @@ export function Form< Data = Schema extends JSONSchema ? FromSchema : any >({ schema, - initialValues, + initialValues: _initialValues, initialOpts, children, onChange, @@ -57,9 +58,9 @@ export function Form< ...props }: FormProps) { const lib = new Draft2019(schema); - const [data, setData] = useState>( - initialValues ?? lib.getTemplate(undefined, schema, initialOpts) - ); + const initialValues = _initialValues ?? lib.getTemplate(undefined, schema, initialOpts); + const [data, setData] = useState>(initialValues); + const [dirty, setDirty] = useState(false); const formRef = useRef(null); const [errors, setErrors] = useState([]); @@ -72,10 +73,11 @@ export function Form< function setValue(pointer: string, value: any) { const normalized = normalizePath(pointer); - console.log("setValue", { pointer, normalized, value }); + //console.log("setValue", { pointer, normalized, value }); const key = normalized.substring(2).replace(/\//g, "."); setData((prev) => { const changed = immutable.set(prev, key, value); + onChange?.(changed, key, value); //console.log("changed", prev, changed, { key, value }); return changed; }); @@ -86,12 +88,15 @@ export function Form< const key = normalized.substring(2).replace(/\//g, "."); setData((prev) => { const changed = immutable.del(prev, key); + onChange?.(changed, key, undefined); //console.log("changed", prev, changed, { key }); return changed; }); } useEffect(() => { + setDirty(!isEqual(initialValues, data)); + if (validateOn === "change") { validate(); } @@ -107,6 +112,7 @@ export function Form< const context = { data: data ?? {}, + dirty, setData, setValue, deleteValue, @@ -194,3 +200,8 @@ export function useFieldContext(name: string) { required }; } + +export function Subscribe({ children }: { children: (ctx: FormContext) => ReactNode }) { + const ctx = useFormContext(); + return children(ctx); +} diff --git a/app/src/ui/components/form/json-schema-form3/ObjectField.tsx b/app/src/ui/components/form/json-schema-form/ObjectField.tsx similarity index 88% rename from app/src/ui/components/form/json-schema-form3/ObjectField.tsx rename to app/src/ui/components/form/json-schema-form/ObjectField.tsx index f363ff6..0113789 100644 --- a/app/src/ui/components/form/json-schema-form3/ObjectField.tsx +++ b/app/src/ui/components/form/json-schema-form/ObjectField.tsx @@ -36,10 +36,10 @@ export const ObjectField = ({ if (!schema) return; if (schema.anyOf || schema.oneOf) { - return ; + return ; } - return ; + return ; })} ); diff --git a/app/src/ui/components/form/json-schema-form/components/Field.tsx b/app/src/ui/components/form/json-schema-form/components/Field.tsx deleted file mode 100644 index 2e70f44..0000000 --- a/app/src/ui/components/form/json-schema-form/components/Field.tsx +++ /dev/null @@ -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 - ) : ( - - - {label} - {props.required ? " *" : ""} - - {field} - {props.description ? {props.description} : null} - - ); -} - -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 ( -
- -
- ); - } else if (isType(props.type, ["number", "integer"])) { - return ; - } - - return ; -} - -export function AutoForm({ schema, prefix = "" }: { schema: JSONSchema; prefix?: string }) { - const required = schema.required ?? []; - const properties = schema.properties ?? {}; - - return ( - <> - {/*
{JSON.stringify(schema, null, 2)}
;*/} -
- {Object.keys(properties).map((name) => { - const field = properties[name]; - const _name = `${prefix ? prefix + "." : ""}${name}`; - return ; - })} -
- - ); -} diff --git a/app/src/ui/components/form/json-schema-form/index.ts b/app/src/ui/components/form/json-schema-form/index.ts index 0a7fb1a..60907f6 100644 --- a/app/src/ui/components/form/json-schema-form/index.ts +++ b/app/src/ui/components/form/json-schema-form/index.ts @@ -1,2 +1,6 @@ -export { TypeboxValidator, type ValueError } from "./validators/tb-validator"; -export { CfValidator, type OutputUnit } from "./validators/cf-validator"; +export * from "./Field"; +export * from "./Form"; +export * from "./ObjectField"; +export * from "./ArrayField"; +export * from "./AnyOfField"; +export * from "./FieldWrapper"; diff --git a/app/src/ui/components/form/json-schema-form3/utils.ts b/app/src/ui/components/form/json-schema-form/utils.ts similarity index 98% rename from app/src/ui/components/form/json-schema-form3/utils.ts rename to app/src/ui/components/form/json-schema-form/utils.ts index 45c1563..532684b 100644 --- a/app/src/ui/components/form/json-schema-form3/utils.ts +++ b/app/src/ui/components/form/json-schema-form/utils.ts @@ -122,7 +122,7 @@ export function normalizePath(path: string) { } export function prefixPointer(pointer: string, prefix: string) { - return pointer.replace("#/", `#/${prefix}/`); + return pointer.replace("#/", `#/${prefix}/`).replace(/\/\//g, "/"); } export function getParentPointer(pointer: string) { diff --git a/app/src/ui/components/form/json-schema-form/validators/cf-validator.ts b/app/src/ui/components/form/json-schema-form/validators/cf-validator.ts deleted file mode 100644 index a25889e..0000000 --- a/app/src/ui/components/form/json-schema-form/validators/cf-validator.ts +++ /dev/null @@ -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 { - async validate(schema: JsonSchema, data: any) { - const result = new Validator(schema).validate(data); - return result.errors; - } -} - -export type { OutputUnit }; diff --git a/app/src/ui/components/form/json-schema-form/validators/tb-validator.ts b/app/src/ui/components/form/json-schema-form/validators/tb-validator.ts deleted file mode 100644 index 2ddef5b..0000000 --- a/app/src/ui/components/form/json-schema-form/validators/tb-validator.ts +++ /dev/null @@ -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 { - async validate(schema: TSchema, data: any) { - return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)]; - } -} - -export type { ValueError }; diff --git a/app/src/ui/components/form/json-schema-form2/Form.tsx b/app/src/ui/components/form/json-schema-form2/Form.tsx deleted file mode 100644 index bf0bf08..0000000 --- a/app/src/ui/components/form/json-schema-form2/Form.tsx +++ /dev/null @@ -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; - -export type FormProps< - Schema extends JSONSchema = JSONSchema, - Data = Schema extends JSONSchema ? FromSchema : any -> = Omit, "onChange"> & { - schema: Schema; - validateOn?: "change" | "submit"; - initialValues?: Partial; - onChange?: (data: Partial, 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; -}; - -const FormContext = createContext(undefined!); - -export function Form< - Schema extends JSONSchema = JSONSchema, - Data = Schema extends JSONSchema ? FromSchema : any ->({ - schema: _schema, - initialValues: _initialValues, - children, - onChange, - validateOn = "submit", - hiddenSubmit = true, - ...props -}: FormProps) { - const schema = useMemo(() => _schema, [JSON.stringify(_schema)]); - const initialValues = useMemo( - () => (_initialValues ? flatten(_initialValues) : {}), - [JSON.stringify(_initialValues)] - ); - - const [data, setData] = useState(initialValues); - const [errors, setErrors] = useState([]); - const [selections, setSelections] = useState>({}); - const lib = new Draft2019(schema); - const formRef = useRef(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) { - 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) { - 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 ( - <> -
- {children} - {hiddenSubmit && ( - - )} -
-
{JSON.stringify(data, null, 2)}
-
{JSON.stringify(unflatten(data, schema, selections), null, 2)}
-
{JSON.stringify(errors, null, 2)}
-
{JSON.stringify(selections, null, 2)}
- - ); -} - -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 - }; -} diff --git a/app/src/ui/components/form/json-schema-form2/utils.ts b/app/src/ui/components/form/json-schema-form2/utils.ts deleted file mode 100644 index 90bc89d..0000000 --- a/app/src/ui/components/form/json-schema-form2/utils.ts +++ /dev/null @@ -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) { - 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, - schema: JSONSchema, - selections?: Record -) { - 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, - 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[] | undefined { - if (!schema || typeof schema !== "object") return; - return (schema.anyOf ?? schema.oneOf) as any; -} - -export function getMultiSchemaMatched( - schema: JsonSchema, - data: any -): [number, Exclude[], Exclude | 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(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; -} diff --git a/app/src/ui/elements/auth/AuthForm.tsx b/app/src/ui/elements/auth/AuthForm.tsx index 1142188..fb5e806 100644 --- a/app/src/ui/elements/auth/AuthForm.tsx +++ b/app/src/ui/elements/auth/AuthForm.tsx @@ -6,9 +6,18 @@ import { transform } from "lodash-es"; import type { ComponentPropsWithoutRef } from "react"; import { Button } from "ui/components/buttons/Button"; import { Group, Input, Label } from "ui/components/form/Formy/components"; -import { TypeboxValidator } from "ui/components/form/json-schema-form"; 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 { + async validate(schema: TSchema, data: any) { + return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)]; + } +} + export type LoginFormProps = Omit, "onSubmit" | "action"> & { className?: string; formData?: any; @@ -75,7 +84,7 @@ export function AuthForm({
(config); - //console.log("schema", schema); + console.log("data", data); return ( <> - - {({ errors, submitting, dirty }) => ( - <> + + + {({ dirty }) => ( + } > Settings - -
- -
- - - -
-
-
- - -
- -
- - )} + )} +
+ +
+ +
+ + + +
+
+
+ + + + +
+ +
); } 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(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 ( -
- - {types.map((type) => ( - + <> +
+ {ctx.schemas?.map((schema: any, i) => ( + ))} - -
current: {selected}
-
options: {schemas.map((s) => s.title).join(", ")}
-
+
+ + {ctx.selected !== null && ( + + + )} + ); } diff --git a/app/src/ui/routes/test/index.tsx b/app/src/ui/routes/test/index.tsx index 9f1fd76..adaf354 100644 --- a/app/src/ui/routes/test/index.tsx +++ b/app/src/ui/routes/test/index.tsx @@ -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; diff --git a/app/src/ui/routes/test/tests/json-schema-form2.tsx b/app/src/ui/routes/test/tests/json-schema-form2.tsx deleted file mode 100644 index c137091..0000000 --- a/app/src/ui/routes/test/tests/json-schema-form2.tsx +++ /dev/null @@ -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 ( -
-

Form

- - {/*
- - - - - -
- -
- - -
*/} - - {/*
- - */} - {/*
- - */} - -
- - - - {/*
- - */} -
- ); -} - -const Field = ({ - name = "", - schema: _schema -}: { name?: string; schema?: Exclude }) => { - 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 ( - 0}> - - {label} {required ? "*" : ""} - -
-
- -
- - - - - - - - -
- {schema.description && {schema.description}} - {errors.length > 0 && ( - {errors.map((e) => e.message).join(", ")} - )} -
- ); -}; - -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 ; - } - - if (isType(schema.type, ["number", "integer"])) { - return ; - } - - return ; -}; - -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 ( -
- Object: {label} - {Object.keys(properties).map((prop) => { - const schema = properties[prop]; - const pointer = `${path}/${prop}`; - - console.log("--", prop, pointer, schema); - if (schema.anyOf || schema.oneOf) { - return ; - } - - if (isType(schema.type, "object")) { - console.log("object", { prop, pointer, schema }); - return ; - } - - if (isType(schema.type, "array")) { - return ; - } - - return ; - })} -
- ); -}; - -const AnyOfField = ({ path = "" }: { path?: string }) => { - const [selected, setSelected] = useState(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) { - 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 ( - <> -
- anyOf: {path} ({selected}) -
- - - {selected !== null && ( - - )} - - ); -}; - -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 ; - } - - if (isType(schema.type, "array")) { - return ; - } - - return ; -}; diff --git a/app/src/ui/routes/test/tests/json-schema-form3.tsx b/app/src/ui/routes/test/tests/json-schema-form3.tsx index cb5e2f9..9faf8b8 100644 --- a/app/src/ui/routes/test/tests/json-schema-form3.tsx +++ b/app/src/ui/routes/test/tests/json-schema-form3.tsx @@ -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 (
@@ -301,7 +176,7 @@ function CustomMediaForm() { } function CustomMediaFormAdapter() { - const ctx = useAnyOfContext(); + const ctx = AnyOf.useContext(); return ( <>