fix schema form

This commit is contained in:
dswbx
2025-02-26 08:22:05 +01:00
parent de854eec3a
commit d4a6a9326f
8 changed files with 139 additions and 46 deletions

View File

@@ -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<ErrorBoundaryProps, ErrorBoundaryState> {
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
)
) : (
<div>
<h2>Something went wrong.</h2>
<button onClick={this.resetError}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -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<number | null>(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<FormFieldProps>) => {
const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
const { selected, selectedSchema, path, errors } = useAnyOfContext();
if (selected === null) return null;
return (

View File

@@ -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);

View File

@@ -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<any>) => void;
} & Omit<FieldwrapperProps, "children">;
placeholder?: string;
} & Omit<FieldwrapperProps, "children" | "schema">;
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 (
<ErrorBoundary fallback={fieldErrorBoundary(props)}>
<FieldImpl {...props} />
</ErrorBoundary>
);
};
const fieldErrorBoundary =
({ name }: FieldProps) =>
({ error }: { error: Error }) => (
<Pre>
Field "{name}" error: {error.message}
</Pre>
);
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 (
<Pre>
@@ -23,11 +41,11 @@ export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps)
);
if (isType(schema.type, "object")) {
return <ObjectField path={name} schema={schema} />;
return <ObjectField path={name} />;
}
if (isType(schema.type, "array")) {
return <ArrayField path={name} schema={schema} />;
return <ArrayField path={name} />;
}
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}
/>
</FieldWrapper>
@@ -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) {

View File

@@ -298,7 +298,6 @@ type SelectorFn<Ctx = any, Refined = any> = (state: Ctx) => Refined;
export function useDerivedFieldContext<Data = any, Reduced = undefined>(
path,
_schema?: LibJsonSchema,
deriveFn?: SelectorFn<
FormContext<Data> & {
pointer: string;
@@ -314,8 +313,7 @@ export function useDerivedFieldContext<Data = any, Reduced = undefined>(
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(

View File

@@ -7,21 +7,14 @@ import { useDerivedFieldContext } from "./Form";
export type ObjectFieldProps = {
path?: string;
schema?: Exclude<JSONSchema, boolean>;
label?: string | false;
wrapperProps?: Partial<FieldwrapperProps>;
};
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 (
<FieldWrapper
@@ -31,8 +24,10 @@ export const ObjectField = ({
errorPlacement="top"
{...wrapperProps}
>
{Object.keys(properties).map((prop) => {
const schema = properties[prop];
{properties.length === 0 ? (
<i className="opacity-50">No properties</i>
) : (
properties.map(([prop, schema]) => {
const name = [path, prop].filter(Boolean).join(".");
if (typeof schema === "undefined" || typeof schema === "boolean") return;
@@ -41,7 +36,8 @@ export const ObjectField = ({
}
return <Field key={name} name={name} />;
})}
})
)}
</FieldWrapper>
);
};

View File

@@ -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");

View File

@@ -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 (
<Scrollable>
<div className="flex flex-col p-3">
<Form schema={_schema.auth} options={formOptions} />
{/*<Form
onChange={(data) => console.log("change", data)}
onSubmit={(data) => console.log("submit", data)}
@@ -249,13 +278,13 @@ export default function JsonSchemaForm3() {
</Form>*/}
{/*<CustomMediaForm />*/}
<Form
{/*<Form
schema={schema.media}
initialValues={config.media as any}
onSubmit={console.log}
options={{ debug: true }}
/*validateOn="change"*/
/>
/>*/}
{/*<Form
schema={removeKeyRecursively(schema.media, "pattern") as any}
@@ -333,7 +362,7 @@ function CustomMediaForm() {
<AnyOf.Root path="adapter">
<CustomMediaFormAdapter />
</AnyOf.Root>
{/*<FormDebug force />*/}
<FormDebug force />
</Form>
);
}