diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index bd77639..fa04467 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -1,11 +1,10 @@ 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 } from "@/utils/jsonUtils"; -import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils"; -import type { JsonValue, JsonSchemaType, JsonObject } from "@/utils/jsonUtils"; +import { generateDefaultValue } from "@/utils/schemaUtils"; +import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; interface DynamicJsonFormProps { schema: JsonSchemaType; @@ -14,13 +13,23 @@ interface DynamicJsonFormProps { maxDepth?: number; } +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, }: DynamicJsonFormProps) => { - const [isJsonMode, setIsJsonMode] = useState(false); + 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 @@ -207,111 +216,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; } @@ -350,9 +254,11 @@ const DynamicJsonForm = ({ Format JSON )} - + {!isOnlyJSON && ( + + )} {isJsonMode ? ( diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index df29e94..d9ff22a 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -43,7 +43,13 @@ const ToolsTab = ({ const [isToolRunning, setIsToolRunning] = useState(false); useEffect(() => { - setParams({}); + const params = Object.entries( + selectedTool?.inputSchema.properties ?? [], + ).map(([key, value]) => [ + key, + generateDefaultValue(value as JsonSchemaType), + ]); + setParams(Object.fromEntries(params)); }, [selectedTool]); const renderToolResult = () => { @@ -217,13 +223,10 @@ const ToolsTab = ({ }} /> - ) : ( + ) : prop.type === "number" || + prop.type === "integer" ? ( setParams({ ...params, - [key]: - prop.type === "number" || - prop.type === "integer" - ? Number(e.target.value) - : e.target.value, + [key]: Number(e.target.value), }) } className="mt-1" /> + ) : ( +
+ { + setParams({ + ...params, + [key]: newValue, + }); + }} + /> +
)} ); diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index 1ad4d04..afd435d 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { describe, it, expect, jest } from "@jest/globals"; import DynamicJsonForm from "../DynamicJsonForm"; import type { JsonSchemaType } from "@/utils/jsonUtils"; @@ -93,3 +93,47 @@ describe("DynamicJsonForm Integer Fields", () => { }); }); }); + +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" }`); + }); + }); + }); +});