mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 12:56:05 +00:00
fix schema form
This commit is contained in:
53
app/src/ui/components/display/ErrorBoundary.tsx
Normal file
53
app/src/ui/components/display/ErrorBoundary.tsx
Normal 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;
|
||||||
@@ -10,7 +10,6 @@ import { getLabel, getMultiSchemaMatched } from "./utils";
|
|||||||
|
|
||||||
export type AnyOfFieldRootProps = {
|
export type AnyOfFieldRootProps = {
|
||||||
path?: string;
|
path?: string;
|
||||||
schema?: JsonSchema;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,14 +33,14 @@ export const useAnyOfContext = () => {
|
|||||||
|
|
||||||
const selectedAtom = atom<number | null>(null);
|
const selectedAtom = atom<number | null>(null);
|
||||||
|
|
||||||
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
|
const Root = ({ path = "", children }: AnyOfFieldRootProps) => {
|
||||||
const {
|
const {
|
||||||
setValue,
|
setValue,
|
||||||
lib,
|
lib,
|
||||||
pointer,
|
pointer,
|
||||||
value: { matchedIndex, schemas },
|
value: { matchedIndex, schemas },
|
||||||
schema
|
schema
|
||||||
} = useDerivedFieldContext(path, _schema, (ctx) => {
|
} = useDerivedFieldContext(path, (ctx) => {
|
||||||
const [matchedIndex, schemas = []] = getMultiSchemaMatched(ctx.schema, ctx.value);
|
const [matchedIndex, schemas = []] = getMultiSchemaMatched(ctx.schema, ctx.value);
|
||||||
return { matchedIndex, schemas };
|
return { matchedIndex, schemas };
|
||||||
});
|
});
|
||||||
@@ -115,7 +114,7 @@ const Select = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @todo: add local validation for AnyOf fields
|
// @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();
|
const { selected, selectedSchema, path, errors } = useAnyOfContext();
|
||||||
if (selected === null) return null;
|
if (selected === null) return null;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,12 +10,8 @@ import { FieldWrapper } from "./FieldWrapper";
|
|||||||
import { useDerivedFieldContext, useFormValue } from "./Form";
|
import { useDerivedFieldContext, useFormValue } from "./Form";
|
||||||
import { coerce, getMultiSchema, getMultiSchemaMatched, isEqual, suffixPath } from "./utils";
|
import { coerce, getMultiSchema, getMultiSchemaMatched, isEqual, suffixPath } from "./utils";
|
||||||
|
|
||||||
export const ArrayField = ({
|
export const ArrayField = ({ path = "" }: { path?: string }) => {
|
||||||
path = "",
|
const { setValue, pointer, required, schema, ...ctx } = useDerivedFieldContext(path);
|
||||||
schema: _schema
|
|
||||||
}: { path?: string; schema?: JsonSchema }) => {
|
|
||||||
const { setValue, pointer, required, ...ctx } = useDerivedFieldContext(path, _schema);
|
|
||||||
const schema = _schema ?? ctx.schema;
|
|
||||||
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
|
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
|
||||||
|
|
||||||
// if unique items with enum
|
// if unique items with enum
|
||||||
@@ -55,7 +51,7 @@ export const ArrayField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ArrayItem = memo(({ path, index, schema }: any) => {
|
const ArrayItem = memo(({ path, index, schema }: any) => {
|
||||||
const { value, ...ctx } = useDerivedFieldContext(path, schema, (ctx) => {
|
const { value, ...ctx } = useDerivedFieldContext(path, (ctx) => {
|
||||||
return ctx.value?.[index];
|
return ctx.value?.[index];
|
||||||
});
|
});
|
||||||
const itemPath = suffixPath(path, index);
|
const itemPath = suffixPath(path, index);
|
||||||
@@ -107,7 +103,7 @@ const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
|
|||||||
setValue,
|
setValue,
|
||||||
value: { currentIndex },
|
value: { currentIndex },
|
||||||
...ctx
|
...ctx
|
||||||
} = useDerivedFieldContext(path, schema, (ctx) => {
|
} = useDerivedFieldContext(path, (ctx) => {
|
||||||
return { currentIndex: ctx.value?.length ?? 0 };
|
return { currentIndex: ctx.value?.length ?? 0 };
|
||||||
});
|
});
|
||||||
const itemsMultiSchema = getMultiSchema(schema.items);
|
const itemsMultiSchema = getMultiSchema(schema.items);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { JsonSchema } from "json-schema-library";
|
import type { JsonSchema } from "json-schema-library";
|
||||||
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
||||||
|
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { ArrayField } from "./ArrayField";
|
import { ArrayField } from "./ArrayField";
|
||||||
@@ -10,11 +11,28 @@ import { coerce, isType, isTypeSchema } from "./utils";
|
|||||||
|
|
||||||
export type FieldProps = {
|
export type FieldProps = {
|
||||||
onChange?: (e: ChangeEvent<any>) => void;
|
onChange?: (e: ChangeEvent<any>) => void;
|
||||||
} & Omit<FieldwrapperProps, "children">;
|
placeholder?: string;
|
||||||
|
} & Omit<FieldwrapperProps, "children" | "schema">;
|
||||||
|
|
||||||
export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps) => {
|
export const Field = (props: FieldProps) => {
|
||||||
const { path, setValue, required, ...ctx } = useDerivedFieldContext(name, _schema);
|
return (
|
||||||
const schema = _schema ?? ctx.schema;
|
<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))
|
if (!isTypeSchema(schema))
|
||||||
return (
|
return (
|
||||||
<Pre>
|
<Pre>
|
||||||
@@ -23,11 +41,11 @@ export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps)
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isType(schema.type, "object")) {
|
if (isType(schema.type, "object")) {
|
||||||
return <ObjectField path={name} schema={schema} />;
|
return <ObjectField path={name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isType(schema.type, "array")) {
|
if (isType(schema.type, "array")) {
|
||||||
return <ArrayField path={name} schema={schema} />;
|
return <ArrayField path={name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = schema.readOnly ?? "const" in schema ?? false;
|
const disabled = schema.readOnly ?? "const" in schema ?? false;
|
||||||
@@ -48,6 +66,7 @@ export const Field = ({ name, schema: _schema, onChange, ...props }: FieldProps)
|
|||||||
name={name}
|
name={name}
|
||||||
required={required}
|
required={required}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
placeholder={placeholder}
|
||||||
onChange={onChange ?? handleChange}
|
onChange={onChange ?? handleChange}
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
@@ -69,7 +88,9 @@ export const FieldComponent = ({
|
|||||||
const props = {
|
const props = {
|
||||||
..._props,
|
..._props,
|
||||||
// allow override
|
// 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) {
|
if (schema.enum) {
|
||||||
|
|||||||
@@ -298,7 +298,6 @@ type SelectorFn<Ctx = any, Refined = any> = (state: Ctx) => Refined;
|
|||||||
|
|
||||||
export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
||||||
path,
|
path,
|
||||||
_schema?: LibJsonSchema,
|
|
||||||
deriveFn?: SelectorFn<
|
deriveFn?: SelectorFn<
|
||||||
FormContext<Data> & {
|
FormContext<Data> & {
|
||||||
pointer: string;
|
pointer: string;
|
||||||
@@ -314,8 +313,7 @@ export function useDerivedFieldContext<Data = any, Reduced = undefined>(
|
|||||||
required: boolean;
|
required: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
} {
|
} {
|
||||||
const { _formStateAtom, root, lib, ...ctx } = useFormContext();
|
const { _formStateAtom, root, lib, schema, ...ctx } = useFormContext();
|
||||||
const schema = _schema ?? ctx.schema;
|
|
||||||
const selected = selectAtom(
|
const selected = selectAtom(
|
||||||
_formStateAtom,
|
_formStateAtom,
|
||||||
useCallback(
|
useCallback(
|
||||||
|
|||||||
@@ -7,21 +7,14 @@ import { useDerivedFieldContext } from "./Form";
|
|||||||
|
|
||||||
export type ObjectFieldProps = {
|
export type ObjectFieldProps = {
|
||||||
path?: string;
|
path?: string;
|
||||||
schema?: Exclude<JSONSchema, boolean>;
|
|
||||||
label?: string | false;
|
label?: string | false;
|
||||||
wrapperProps?: Partial<FieldwrapperProps>;
|
wrapperProps?: Partial<FieldwrapperProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ObjectField = ({
|
export const ObjectField = ({ path = "", label: _label, wrapperProps = {} }: ObjectFieldProps) => {
|
||||||
path = "",
|
const { schema } = useDerivedFieldContext(path);
|
||||||
schema: _schema,
|
|
||||||
label: _label,
|
|
||||||
wrapperProps = {}
|
|
||||||
}: ObjectFieldProps) => {
|
|
||||||
const ctx = useDerivedFieldContext(path, _schema);
|
|
||||||
const schema = _schema ?? ctx.schema;
|
|
||||||
if (!isTypeSchema(schema)) return `ObjectField "${path}": no schema`;
|
if (!isTypeSchema(schema)) return `ObjectField "${path}": no schema`;
|
||||||
const properties = schema.properties ?? {};
|
const properties = Object.entries(schema.properties ?? {}) as [string, JSONSchema][];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldWrapper
|
<FieldWrapper
|
||||||
@@ -31,17 +24,20 @@ export const ObjectField = ({
|
|||||||
errorPlacement="top"
|
errorPlacement="top"
|
||||||
{...wrapperProps}
|
{...wrapperProps}
|
||||||
>
|
>
|
||||||
{Object.keys(properties).map((prop) => {
|
{properties.length === 0 ? (
|
||||||
const schema = properties[prop];
|
<i className="opacity-50">No properties</i>
|
||||||
const name = [path, prop].filter(Boolean).join(".");
|
) : (
|
||||||
if (typeof schema === "undefined" || typeof schema === "boolean") return;
|
properties.map(([prop, schema]) => {
|
||||||
|
const name = [path, prop].filter(Boolean).join(".");
|
||||||
|
if (typeof schema === "undefined" || typeof schema === "boolean") return;
|
||||||
|
|
||||||
if (schema.anyOf || schema.oneOf) {
|
if (schema.anyOf || schema.oneOf) {
|
||||||
return <AnyOfField key={name} path={name} />;
|
return <AnyOfField key={name} path={name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Field key={name} name={name} />;
|
return <Field key={name} name={name} />;
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// REGISTER ERROR OVERLAY
|
// REGISTER ERROR OVERLAY
|
||||||
if (process.env.NODE_ENV !== "production") {
|
const showOverlay = true;
|
||||||
|
if (process.env.NODE_ENV !== "production" && showOverlay) {
|
||||||
const showErrorOverlay = (err) => {
|
const showErrorOverlay = (err) => {
|
||||||
// must be within function call because that's when the element is defined for sure.
|
// must be within function call because that's when the element is defined for sure.
|
||||||
const ErrorOverlay = customElements.get("vite-error-overlay");
|
const ErrorOverlay = customElements.get("vite-error-overlay");
|
||||||
|
|||||||
@@ -32,6 +32,33 @@ const schema2 = {
|
|||||||
required: ["age"]
|
required: ["age"]
|
||||||
} as const satisfies JSONSchema;
|
} 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() {
|
export default function JsonSchemaForm3() {
|
||||||
const { schema: _schema, config } = useBknd();
|
const { schema: _schema, config } = useBknd();
|
||||||
const schema = JSON.parse(JSON.stringify(_schema));
|
const schema = JSON.parse(JSON.stringify(_schema));
|
||||||
@@ -46,6 +73,8 @@ export default function JsonSchemaForm3() {
|
|||||||
return (
|
return (
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
<div className="flex flex-col p-3">
|
<div className="flex flex-col p-3">
|
||||||
|
<Form schema={_schema.auth} options={formOptions} />
|
||||||
|
|
||||||
{/*<Form
|
{/*<Form
|
||||||
onChange={(data) => console.log("change", data)}
|
onChange={(data) => console.log("change", data)}
|
||||||
onSubmit={(data) => console.log("submit", data)}
|
onSubmit={(data) => console.log("submit", data)}
|
||||||
@@ -249,13 +278,13 @@ export default function JsonSchemaForm3() {
|
|||||||
</Form>*/}
|
</Form>*/}
|
||||||
|
|
||||||
{/*<CustomMediaForm />*/}
|
{/*<CustomMediaForm />*/}
|
||||||
<Form
|
|
||||||
|
{/*<Form
|
||||||
schema={schema.media}
|
schema={schema.media}
|
||||||
initialValues={config.media as any}
|
initialValues={config.media as any}
|
||||||
onSubmit={console.log}
|
onSubmit={console.log}
|
||||||
options={{ debug: true }}
|
options={{ debug: true }}
|
||||||
/*validateOn="change"*/
|
/>*/}
|
||||||
/>
|
|
||||||
|
|
||||||
{/*<Form
|
{/*<Form
|
||||||
schema={removeKeyRecursively(schema.media, "pattern") as any}
|
schema={removeKeyRecursively(schema.media, "pattern") as any}
|
||||||
@@ -333,7 +362,7 @@ function CustomMediaForm() {
|
|||||||
<AnyOf.Root path="adapter">
|
<AnyOf.Root path="adapter">
|
||||||
<CustomMediaFormAdapter />
|
<CustomMediaFormAdapter />
|
||||||
</AnyOf.Root>
|
</AnyOf.Root>
|
||||||
{/*<FormDebug force />*/}
|
<FormDebug force />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user