diff --git a/app/src/ui/components/code/JsonEditor.tsx b/app/src/ui/components/code/JsonEditor.tsx index ec96811..c65e59a 100644 --- a/app/src/ui/components/code/JsonEditor.tsx +++ b/app/src/ui/components/code/JsonEditor.tsx @@ -1,9 +1,37 @@ -import { Suspense, lazy } from "react"; +import { Suspense, lazy, useState } from "react"; import { twMerge } from "tailwind-merge"; import type { CodeEditorProps } from "./CodeEditor"; const CodeEditor = lazy(() => import("./CodeEditor")); -export function JsonEditor({ editable, className, ...props }: CodeEditorProps) { +export type JsonEditorProps = Omit & { + value?: object; + onChange?: (value: object) => void; + emptyAs?: "null" | "undefined"; +}; + +export function JsonEditor({ + editable, + className, + value, + onChange, + onBlur, + emptyAs = "undefined", + ...props +}: JsonEditorProps) { + const [editorValue, setEditorValue] = useState( + JSON.stringify(value, null, 2), + ); + const handleChange = (given: string) => { + const value = given === "" ? (emptyAs === "null" ? null : undefined) : given; + try { + setEditorValue(value); + onChange?.(value ? JSON.parse(value) : value); + } catch (e) {} + }; + const handleBlur = (e) => { + setEditorValue(JSON.stringify(value, null, 2)); + onBlur?.(e); + }; return ( diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 502a844..3ad9146 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -28,8 +28,9 @@ export const Group = ({ return ( { +export type ArrayFieldProps = { + path?: string; + labelAdd?: string; + wrapperProps?: Omit; +}; + +export const ArrayField = ({ + path = "", + labelAdd = "Add", + wrapperProps = { wrapper: "fieldset" }, +}: ArrayFieldProps) => { const { setValue, pointer, required, schema, ...ctx } = useDerivedFieldContext(path); if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`; // if unique items with enum if (schema.uniqueItems && typeof schema.items === "object" && "enum" in schema.items) { return ( - + { } return ( - + {({ value }) => value?.map((v, index: number) => ( @@ -44,17 +54,21 @@ export const ArrayField = ({ path = "" }: { path?: string }) => { }
- +
); }; const ArrayItem = memo(({ path, index, schema }: any) => { - const { value, ...ctx } = useDerivedFieldContext(path, (ctx) => { + const { + value, + path: absolutePath, + ...ctx + } = useDerivedFieldContext(path, (ctx) => { return ctx.value?.[index]; }); - const itemPath = suffixPath(path, index); + const itemPath = suffixPath(absolutePath, index); let subschema = schema.items; const itemsMultiSchema = getMultiSchema(schema.items); if (itemsMultiSchema) { @@ -62,10 +76,6 @@ const ArrayItem = memo(({ path, index, schema }: any) => { subschema = _subschema; } - const handleUpdate = useEvent((pointer: string, value: any) => { - ctx.setValue(pointer, value); - }); - const handleDelete = useEvent((pointer: string) => { ctx.deleteValue(pointer); }); @@ -76,21 +86,26 @@ const ArrayItem = memo(({ path, index, schema }: any) => { ); return ( -
- { - handleUpdate(itemPath, coerce(e.target.value, subschema!)); - }} - className="w-full" - /> - {DeleteButton} -
+ +
+ {/* another wrap is required for primitive schemas */} + + {DeleteButton} +
+
); }, isEqual); +const AnotherField = (props: Partial) => { + const { value } = useFormValue(""); + + const inputProps = { + // @todo: check, potentially just provide value + value: ["string", "number", "boolean"].includes(typeof value) ? value : undefined, + }; + return ; +}; + const ArrayIterator = memo( ({ name, children }: any) => { return children(useFormValue(name)); @@ -98,19 +113,25 @@ const ArrayIterator = memo( (prev, next) => prev.value?.length === next.value?.length, ); -const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => { +const ArrayAdd = ({ + schema, + path: _path, + label = "Add", +}: { schema: JsonSchema; path: string; label?: string }) => { const { setValue, value: { currentIndex }, + path, ...ctx - } = useDerivedFieldContext(path, (ctx) => { + } = useDerivedFieldContext(_path, (ctx) => { return { currentIndex: ctx.value?.length ?? 0 }; }); const itemsMultiSchema = getMultiSchema(schema.items); + const options = { addOptionalProps: true }; function handleAdd(template?: any) { const newPath = suffixPath(path, currentIndex); - setValue(newPath, template ?? ctx.lib.getTemplate(undefined, schema!.items)); + setValue(newPath, template ?? ctx.lib.getTemplate(undefined, schema!.items, options)); } if (itemsMultiSchema) { @@ -121,14 +142,14 @@ const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => { }} items={itemsMultiSchema.map((s, i) => ({ label: s!.title ?? `Option ${i + 1}`, - onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!)), + onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!, options)), }))} onClickItem={console.log} > - + ); } - return ; + return ; }; 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 60351ca..bc81a83 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -72,7 +72,7 @@ const FieldImpl = ({ ); if (isType(schema.type, "object")) { - return ; + return ; } if (isType(schema.type, "array")) { diff --git a/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx b/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx index 784db35..af4607c 100644 --- a/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx +++ b/app/src/ui/components/form/json-schema-form/FieldWrapper.tsx @@ -11,6 +11,7 @@ import { } from "ui/components/form/json-schema-form/Form"; import { Popover } from "ui/components/overlay/Popover"; import { getLabel } from "./utils"; +import { twMerge } from "tailwind-merge"; export type FieldwrapperProps = { name: string; @@ -25,6 +26,7 @@ export type FieldwrapperProps = { description?: string; descriptionPlacement?: "top" | "bottom"; fieldId?: string; + className?: string; }; export function FieldWrapper({ @@ -38,6 +40,7 @@ export function FieldWrapper({ descriptionPlacement = "bottom", children, fieldId, + className, ...props }: FieldwrapperProps) { const errors = useFormError(name, { strict: true }); @@ -60,7 +63,7 @@ export function FieldWrapper({ 0} as={wrapper === "fieldset" ? "fieldset" : "div"} - className={hidden ? "hidden" : "relative"} + className={twMerge(hidden ? "hidden" : "relative", className)} > {errorPlacement === "top" && Errors} @@ -76,7 +79,7 @@ export function FieldWrapper({ )} {descriptionPlacement === "top" && Description} -
+
{Children.count(children) === 1 && isValidElement(children) ? cloneElement(children, { 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 59deceb..3cf920b 100644 --- a/app/src/ui/components/form/json-schema-form/ObjectField.tsx +++ b/app/src/ui/components/form/json-schema-form/ObjectField.tsx @@ -11,7 +11,7 @@ export type ObjectFieldProps = { }; export const ObjectField = ({ path = "", label: _label, wrapperProps = {} }: ObjectFieldProps) => { - const { schema, ...ctx } = useDerivedFieldContext(path); + const { schema } = useDerivedFieldContext(path); if (!isTypeSchema(schema)) return `ObjectField "${path}": no schema`; const properties = Object.entries(schema.properties ?? {}) as [string, JSONSchema][]; @@ -24,7 +24,7 @@ export const ObjectField = ({ path = "", label: _label, wrapperProps = {} }: Obj {...wrapperProps} > {properties.length === 0 ? ( - No properties + ) : ( properties.map(([prop, schema]) => { const name = [path, prop].filter(Boolean).join("."); @@ -40,3 +40,9 @@ export const ObjectField = ({ path = "", label: _label, wrapperProps = {} }: Obj ); }; + +export const ObjectJsonField = ({ path }: { path: string }) => { + const { value } = useFormValue(path); + const { setValue, path: absolutePath } = useDerivedFieldContext(path); + return setValue(absolutePath, value)} />; +}; diff --git a/app/src/ui/components/form/json-schema-form/utils.ts b/app/src/ui/components/form/json-schema-form/utils.ts index 333bba3..7b755cf 100644 --- a/app/src/ui/components/form/json-schema-form/utils.ts +++ b/app/src/ui/components/form/json-schema-form/utils.ts @@ -67,18 +67,23 @@ export function isRequired(lib: Draft, pointer: string, schema: JsonSchema, data return false; } - const childSchema = lib.getSchema({ pointer, data, schema }); - if (typeof childSchema === "object" && "const" in childSchema) { - return true; + try { + const childSchema = lib.getSchema({ pointer, data, schema }); + if (typeof childSchema === "object" && "const" in childSchema) { + return true; + } + } catch (e) { + return false; } const parentPointer = getParentPointer(pointer); const parentSchema = lib.getSchema({ pointer: parentPointer, data }); - const required = parentSchema?.required?.includes(pointer.split("/").pop()!); + const l = pointer.split("/").pop(); + const required = parentSchema?.required?.includes(l); return !!required; } catch (e) { - console.error("isRequired", { pointer, schema, data, e }); + console.warn("isRequired", { pointer, schema, data, e }); return false; } } diff --git a/app/src/ui/routes/test/tests/code-editor-test.tsx b/app/src/ui/routes/test/tests/code-editor-test.tsx new file mode 100644 index 0000000..99bcee1 --- /dev/null +++ b/app/src/ui/routes/test/tests/code-editor-test.tsx @@ -0,0 +1,13 @@ +import { useState } from "react"; +import { JsonEditor } from "ui/components/code/JsonEditor"; +import { JsonViewer } from "ui/components/code/JsonViewer"; + +export default function CodeEditorTest() { + const [value, setValue] = useState({}); + 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 401ab1f..f1d219c 100644 --- a/app/src/ui/routes/test/tests/json-schema-form3.tsx +++ b/app/src/ui/routes/test/tests/json-schema-form3.tsx @@ -56,6 +56,14 @@ const authSchema = { }, } as const satisfies JSONSchema; +const objectCodeSchema = { + type: "object", + properties: { + name: { type: "string" }, + config: { type: "object", properties: {} }, + }, +}; + const formOptions = { debug: true, }; @@ -77,6 +85,45 @@ export default function JsonSchemaForm3() { {/*
*/} + + + + + {/* + /> */} {/* console.log("change", data)}