From d4a6a9326f41acbde1dcc279f3472abc5e13d105 Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 26 Feb 2025 08:22:05 +0100 Subject: [PATCH] fix schema form --- .../ui/components/display/ErrorBoundary.tsx | 53 +++++++++++++++++++ .../form/json-schema-form/AnyOfField.tsx | 7 ++- .../form/json-schema-form/ArrayField.tsx | 12 ++--- .../form/json-schema-form/Field.tsx | 35 +++++++++--- .../components/form/json-schema-form/Form.tsx | 4 +- .../form/json-schema-form/ObjectField.tsx | 34 ++++++------ app/src/ui/main.tsx | 3 +- .../routes/test/tests/json-schema-form3.tsx | 37 +++++++++++-- 8 files changed, 139 insertions(+), 46 deletions(-) create mode 100644 app/src/ui/components/display/ErrorBoundary.tsx diff --git a/app/src/ui/components/display/ErrorBoundary.tsx b/app/src/ui/components/display/ErrorBoundary.tsx new file mode 100644 index 0000000..1148d29 --- /dev/null +++ b/app/src/ui/components/display/ErrorBoundary.tsx @@ -0,0 +1,53 @@ +import React, { Component, type ErrorInfo, type ReactNode } from "react"; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: + | (({ error, resetError }: { error: Error; resetError: () => void }) => ReactNode) + | ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error | undefined; +} + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: undefined }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + override componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, errorInfo); + } + + resetError = () => { + this.setState({ hasError: false, error: undefined }); + }; + + override render() { + if (this.state.hasError) { + return this.props.fallback ? ( + typeof this.props.fallback === "function" ? ( + this.props.fallback({ error: this.state.error!, resetError: this.resetError }) + ) : ( + this.props.fallback + ) + ) : ( +
+

Something went wrong.

+ +
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/app/src/ui/components/form/json-schema-form/AnyOfField.tsx b/app/src/ui/components/form/json-schema-form/AnyOfField.tsx index a51d107..fef96a7 100644 --- a/app/src/ui/components/form/json-schema-form/AnyOfField.tsx +++ b/app/src/ui/components/form/json-schema-form/AnyOfField.tsx @@ -10,7 +10,6 @@ import { getLabel, getMultiSchemaMatched } from "./utils"; export type AnyOfFieldRootProps = { path?: string; - schema?: JsonSchema; children: ReactNode; }; @@ -34,14 +33,14 @@ export const useAnyOfContext = () => { const selectedAtom = atom(null); -const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => { +const Root = ({ path = "", children }: AnyOfFieldRootProps) => { const { setValue, lib, pointer, value: { matchedIndex, schemas }, schema - } = useDerivedFieldContext(path, _schema, (ctx) => { + } = useDerivedFieldContext(path, (ctx) => { const [matchedIndex, schemas = []] = getMultiSchemaMatched(ctx.schema, ctx.value); return { matchedIndex, schemas }; }); @@ -115,7 +114,7 @@ const Select = () => { }; // @todo: add local validation for AnyOf fields -const Field = ({ name, label, schema, ...props }: Partial) => { +const Field = ({ name, label, ...props }: Partial) => { const { selected, selectedSchema, path, errors } = useAnyOfContext(); if (selected === null) return null; return ( diff --git a/app/src/ui/components/form/json-schema-form/ArrayField.tsx b/app/src/ui/components/form/json-schema-form/ArrayField.tsx index 1c95b0e..addc651 100644 --- a/app/src/ui/components/form/json-schema-form/ArrayField.tsx +++ b/app/src/ui/components/form/json-schema-form/ArrayField.tsx @@ -10,12 +10,8 @@ import { FieldWrapper } from "./FieldWrapper"; import { useDerivedFieldContext, useFormValue } from "./Form"; import { coerce, getMultiSchema, getMultiSchemaMatched, isEqual, suffixPath } from "./utils"; -export const ArrayField = ({ - path = "", - schema: _schema -}: { path?: string; schema?: JsonSchema }) => { - const { setValue, pointer, required, ...ctx } = useDerivedFieldContext(path, _schema); - const schema = _schema ?? ctx.schema; +export const ArrayField = ({ path = "" }: { path?: string }) => { + const { setValue, pointer, required, schema, ...ctx } = useDerivedFieldContext(path); if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`; // if unique items with enum @@ -55,7 +51,7 @@ export const ArrayField = ({ }; const ArrayItem = memo(({ path, index, schema }: any) => { - const { value, ...ctx } = useDerivedFieldContext(path, schema, (ctx) => { + const { value, ...ctx } = useDerivedFieldContext(path, (ctx) => { return ctx.value?.[index]; }); const itemPath = suffixPath(path, index); @@ -107,7 +103,7 @@ const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => { setValue, value: { currentIndex }, ...ctx - } = useDerivedFieldContext(path, schema, (ctx) => { + } = useDerivedFieldContext(path, (ctx) => { return { currentIndex: ctx.value?.length ?? 0 }; }); const itemsMultiSchema = getMultiSchema(schema.items); diff --git a/app/src/ui/components/form/json-schema-form/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx index 68435d0..355ef25 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -1,5 +1,6 @@ import type { JsonSchema } from "json-schema-library"; import type { ChangeEvent, ComponentPropsWithoutRef } from "react"; +import ErrorBoundary from "ui/components/display/ErrorBoundary"; import * as Formy from "ui/components/form/Formy"; import { useEvent } from "ui/hooks/use-event"; import { ArrayField } from "./ArrayField"; @@ -10,11 +11,28 @@ import { coerce, isType, isTypeSchema } from "./utils"; export type FieldProps = { onChange?: (e: ChangeEvent) => void; -} & Omit; + placeholder?: string; +} & Omit; -export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps) => { - const { path, setValue, required, ...ctx } = useDerivedFieldContext(name, _schema); - const schema = _schema ?? ctx.schema; +export const Field = (props: FieldProps) => { + return ( + + + + ); +}; + +const fieldErrorBoundary = + ({ name }: FieldProps) => + ({ error }: { error: Error }) => ( +
+         Field "{name}" error: {error.message}
+      
+ ); + +const FieldImpl = ({ name, onChange, placeholder, ...props }: FieldProps) => { + const { path, setValue, required, schema, ...ctx } = useDerivedFieldContext(name); + //console.log("Field", { name, path, schema }); if (!isTypeSchema(schema)) return (
@@ -23,11 +41,11 @@ export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps)
       );
 
    if (isType(schema.type, "object")) {
-      return ;
+      return ;
    }
 
    if (isType(schema.type, "array")) {
-      return ;
+      return ;
    }
 
    const disabled = schema.readOnly ?? "const" in schema ?? false;
@@ -48,6 +66,7 @@ export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps)
             name={name}
             required={required}
             disabled={disabled}
+            placeholder={placeholder}
             onChange={onChange ?? handleChange}
          />
       
@@ -69,7 +88,9 @@ export const FieldComponent = ({
    const props = {
       ..._props,
       // allow override
-      value: typeof _props.value !== "undefined" ? _props.value : value
+      value: typeof _props.value !== "undefined" ? _props.value : value,
+      placeholder:
+         (_props.placeholder ?? typeof schema.default !== "undefined") ? String(schema.default) : ""
    };
 
    if (schema.enum) {
diff --git a/app/src/ui/components/form/json-schema-form/Form.tsx b/app/src/ui/components/form/json-schema-form/Form.tsx
index 3c359b8..7e85917 100644
--- a/app/src/ui/components/form/json-schema-form/Form.tsx
+++ b/app/src/ui/components/form/json-schema-form/Form.tsx
@@ -298,7 +298,6 @@ type SelectorFn = (state: Ctx) => Refined;
 
 export function useDerivedFieldContext(
    path,
-   _schema?: LibJsonSchema,
    deriveFn?: SelectorFn<
       FormContext & {
          pointer: string;
@@ -314,8 +313,7 @@ export function useDerivedFieldContext(
    required: boolean;
    path: string;
 } {
-   const { _formStateAtom, root, lib, ...ctx } = useFormContext();
-   const schema = _schema ?? ctx.schema;
+   const { _formStateAtom, root, lib, schema, ...ctx } = useFormContext();
    const selected = selectAtom(
       _formStateAtom,
       useCallback(
diff --git a/app/src/ui/components/form/json-schema-form/ObjectField.tsx b/app/src/ui/components/form/json-schema-form/ObjectField.tsx
index d6d38b6..ff1b598 100644
--- a/app/src/ui/components/form/json-schema-form/ObjectField.tsx
+++ b/app/src/ui/components/form/json-schema-form/ObjectField.tsx
@@ -7,21 +7,14 @@ import { useDerivedFieldContext } from "./Form";
 
 export type ObjectFieldProps = {
    path?: string;
-   schema?: Exclude;
    label?: string | false;
    wrapperProps?: Partial;
 };
 
-export const ObjectField = ({
-   path = "",
-   schema: _schema,
-   label: _label,
-   wrapperProps = {}
-}: ObjectFieldProps) => {
-   const ctx = useDerivedFieldContext(path, _schema);
-   const schema = _schema ?? ctx.schema;
+export const ObjectField = ({ path = "", label: _label, wrapperProps = {} }: ObjectFieldProps) => {
+   const { schema } = useDerivedFieldContext(path);
    if (!isTypeSchema(schema)) return `ObjectField "${path}": no schema`;
-   const properties = schema.properties ?? {};
+   const properties = Object.entries(schema.properties ?? {}) as [string, JSONSchema][];
 
    return (
       
-         {Object.keys(properties).map((prop) => {
-            const schema = properties[prop];
-            const name = [path, prop].filter(Boolean).join(".");
-            if (typeof schema === "undefined" || typeof schema === "boolean") return;
+         {properties.length === 0 ? (
+            No properties
+         ) : (
+            properties.map(([prop, schema]) => {
+               const name = [path, prop].filter(Boolean).join(".");
+               if (typeof schema === "undefined" || typeof schema === "boolean") return;
 
-            if (schema.anyOf || schema.oneOf) {
-               return ;
-            }
+               if (schema.anyOf || schema.oneOf) {
+                  return ;
+               }
 
-            return ;
-         })}
+               return ;
+            })
+         )}
       
    );
 };
diff --git a/app/src/ui/main.tsx b/app/src/ui/main.tsx
index 74a358d..69a79a3 100644
--- a/app/src/ui/main.tsx
+++ b/app/src/ui/main.tsx
@@ -11,7 +11,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
 );
 
 // REGISTER ERROR OVERLAY
-if (process.env.NODE_ENV !== "production") {
+const showOverlay = true;
+if (process.env.NODE_ENV !== "production" && showOverlay) {
    const showErrorOverlay = (err) => {
       // must be within function call because that's when the element is defined for sure.
       const ErrorOverlay = customElements.get("vite-error-overlay");
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 eb0dc65..c9a7b7c 100644
--- a/app/src/ui/routes/test/tests/json-schema-form3.tsx
+++ b/app/src/ui/routes/test/tests/json-schema-form3.tsx
@@ -32,6 +32,33 @@ const schema2 = {
    required: ["age"]
 } as const satisfies JSONSchema;
 
+const authSchema = {
+   type: "object",
+   properties: {
+      what: {
+         type: "array",
+         items: {
+            type: "string"
+         }
+      },
+      jwt: {
+         type: "object",
+         properties: {
+            fields: {
+               type: "array",
+               items: {
+                  type: "string"
+               }
+            }
+         }
+      }
+   }
+} as const satisfies JSONSchema;
+
+const formOptions = {
+   debug: true
+};
+
 export default function JsonSchemaForm3() {
    const { schema: _schema, config } = useBknd();
    const schema = JSON.parse(JSON.stringify(_schema));
@@ -46,6 +73,8 @@ export default function JsonSchemaForm3() {
    return (
       
          
+
+ {/* console.log("change", data)} onSubmit={(data) => console.log("submit", data)} @@ -249,13 +278,13 @@ export default function JsonSchemaForm3() {
*/} {/**/} -
+ />*/} {/* - {/**/} + ); }