From 3e95d9d42abe919d63673c42822449301616480b Mon Sep 17 00:00:00 2001 From: Maxwell Gerber Date: Fri, 11 Apr 2025 15:34:41 -0700 Subject: [PATCH] pr feedback: Only use form if object is simple --- client/src/components/DynamicJsonForm.tsx | 126 ++---------------- client/src/components/ToolsTab.tsx | 1 - .../__tests__/DynamicJsonForm.test.tsx | 46 ++++++- 3 files changed, 59 insertions(+), 114 deletions(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index a3fd847..70270e3 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -1,10 +1,9 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import JsonEditor from "./JsonEditor"; -import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils"; -import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils"; +import { updateValueAtPath } from "@/utils/jsonPathUtils"; +import { generateDefaultValue } from "@/utils/schemaUtils"; export type JsonValue = | string @@ -36,17 +35,25 @@ interface DynamicJsonFormProps { value: JsonValue; onChange: (value: JsonValue) => void; maxDepth?: number; - onlyJSON?: boolean; } +const isSimpleObject = (schema: JsonSchemaType): boolean => { + const supportedTypes = ["string", "number", "integer", "boolean", "null"]; + if (supportedTypes.includes(schema.type)) return true; + if (schema.type !== "object") return false; + return Object.values(schema.properties ?? {}).every((prop) => + supportedTypes.includes(prop.type), + ); +}; + const DynamicJsonForm = ({ schema, value, onChange, maxDepth = 3, - onlyJSON = false, }: DynamicJsonFormProps) => { - const [isJsonMode, setIsJsonMode] = useState(onlyJSON); + const isOnlyJSON = !isSimpleObject(schema); + const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [jsonError, setJsonError] = useState(); // Store the raw JSON string to allow immediate feedback during typing // while deferring parsing until the user stops typing @@ -233,111 +240,6 @@ const DynamicJsonForm = ({ required={propSchema.required} /> ); - case "object": { - // Handle case where we have a value but no schema properties - const objectValue = (currentValue as JsonObject) || {}; - - // If we have schema properties, use them to render fields - if (propSchema.properties) { - return ( -
- {Object.entries(propSchema.properties).map(([key, prop]) => ( -
- - {renderFormFields( - prop, - objectValue[key], - [...path, key], - depth + 1, - )} -
- ))} -
- ); - } - // If we have a value but no schema properties, render fields based on the value - else if (Object.keys(objectValue).length > 0) { - return ( -
- {Object.entries(objectValue).map(([key, value]) => ( -
- - - handleFieldChange([...path, key], e.target.value) - } - /> -
- ))} -
- ); - } - // If we have neither schema properties nor value, return null - return null; - } - case "array": { - const arrayValue = Array.isArray(currentValue) ? currentValue : []; - if (!propSchema.items) return null; - return ( -
- {propSchema.description && ( -

{propSchema.description}

- )} - - {propSchema.items?.description && ( -

- Items: {propSchema.items.description} -

- )} - -
- {arrayValue.map((item, index) => ( -
- {renderFormFields( - propSchema.items as JsonSchemaType, - item, - [...path, index.toString()], - depth + 1, - )} - -
- ))} - -
-
- ); - } default: return null; } @@ -376,7 +278,7 @@ const DynamicJsonForm = ({ Format JSON )} - {!onlyJSON && ( + {!isOnlyJSON && ( diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 462b3a9..a20e7da 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -234,7 +234,6 @@ const ToolsTab = ({ ) : (
{ }); }); }); + +describe("DynamicJsonForm Complex Fields", () => { + const renderForm = (props = {}) => { + const defaultProps = { + schema: { + type: "object", + properties: { + // The simplified JsonSchemaType does not accept oneOf fields + // But they exist in the more-complete JsonSchema7Type + nested: { oneOf: [{ type: "string" }, { type: "integer" }] }, + }, + } as unknown as JsonSchemaType, + value: undefined, + onChange: jest.fn(), + }; + return render(); + }; + + describe("Basic Operations", () => { + it("should render textbox and autoformat button, but no switch-to-form button", () => { + renderForm(); + const input = screen.getByRole("textbox"); + expect(input).toHaveProperty("type", "textarea"); + const buttons = screen.getAllByRole("button"); + expect(buttons).toHaveLength(1); + expect(buttons[0]).toHaveProperty("textContent", "Format JSON"); + }); + + it("should pass changed values to onChange", () => { + const onChange = jest.fn(); + renderForm({ onChange }); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { + target: { value: `{ "nested": "i am string" }` }, + }); + + // The onChange handler is debounced when using the JSON view, so we need to wait a little bit + waitFor(() => { + expect(onChange).toHaveBeenCalledWith(`{ "nested": "i am string" }`); + }); + }); + }); +});