diff --git a/client/package.json b/client/package.json index 4735ffa..038a02a 100644 --- a/client/package.json +++ b/client/package.json @@ -24,8 +24,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.4.1", - "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.3", @@ -37,8 +37,8 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "lucide-react": "^0.447.0", - "prismjs": "^1.29.0", "pkce-challenge": "^4.1.0", + "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-simple-code-editor": "^0.14.1", @@ -57,6 +57,7 @@ "@types/serve-handler": "^6.1.4", "@vitejs/plugin-react": "^4.3.2", "autoprefixer": "^10.4.20", + "co": "^4.6.0", "eslint": "^9.11.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.12", diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 9e865f9..5237513 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -11,12 +11,15 @@ export type JsonValue = | number | boolean | null + | undefined | JsonValue[] | { [key: string]: JsonValue }; export type JsonSchemaType = { - type: "string" | "number" | "integer" | "boolean" | "array" | "object"; + type: "string" | "number" | "integer" | "boolean" | "array" | "object" | "null"; description?: string; + required?: boolean; + default?: JsonValue; properties?: Record; items?: JsonSchemaType; }; @@ -105,21 +108,61 @@ const DynamicJsonForm = ({ switch (propSchema.type) { case "string": + return ( + { + const val = e.target.value; + if (!val && !propSchema.required) { + handleFieldChange(path, undefined); + } else { + handleFieldChange(path, val); + } + }} + placeholder={propSchema.description} + required={propSchema.required} + /> + ); case "number": + return ( + { + const val = e.target.value; + if (!val && !propSchema.required) { + handleFieldChange(path, undefined); + } else { + const num = Number(val); + if (!isNaN(num)) { + handleFieldChange(path, num); + } + } + }} + placeholder={propSchema.description} + required={propSchema.required} + /> + ); case "integer": return ( - handleFieldChange( - path, - propSchema.type === "string" - ? e.target.value - : Number(e.target.value), - ) - } + type="number" + step="1" + value={(currentValue as number)?.toString() ?? ""} + onChange={(e) => { + const val = e.target.value; + if (!val && !propSchema.required) { + handleFieldChange(path, undefined); + } else { + const num = Number(val); + if (!isNaN(num) && Number.isInteger(num)) { + handleFieldChange(path, num); + } + } + }} placeholder={propSchema.description} + required={propSchema.required} /> ); case "boolean": @@ -129,6 +172,7 @@ const DynamicJsonForm = ({ checked={(currentValue as boolean) ?? false} onChange={(e) => handleFieldChange(path, e.target.checked)} className="w-4 h-4" + required={propSchema.required} /> ); case "object": { @@ -216,9 +260,12 @@ const DynamicJsonForm = ({ variant="outline" size="sm" onClick={() => { + const defaultValue = generateDefaultValue( + propSchema.items as JsonSchemaType + ); handleFieldChange(path, [ ...arrayValue, - generateDefaultValue(propSchema.items as JsonSchemaType), + defaultValue ?? null ]); }} title={ diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts index 0a10af4..a238798 100644 --- a/client/src/utils/__tests__/schemaUtils.test.ts +++ b/client/src/utils/__tests__/schemaUtils.test.ts @@ -7,40 +7,42 @@ import { JsonSchemaType } from "../../components/DynamicJsonForm"; describe("generateDefaultValue", () => { test("generates default string", () => { - expect(generateDefaultValue({ type: "string" })).toBe(""); + expect(generateDefaultValue({ type: "string", required: true })).toBe(""); }); test("generates default number", () => { - expect(generateDefaultValue({ type: "number" })).toBe(0); + expect(generateDefaultValue({ type: "number", required: true })).toBe(0); }); test("generates default integer", () => { - expect(generateDefaultValue({ type: "integer" })).toBe(0); + expect(generateDefaultValue({ type: "integer", required: true })).toBe(0); }); test("generates default boolean", () => { - expect(generateDefaultValue({ type: "boolean" })).toBe(false); + expect(generateDefaultValue({ type: "boolean", required: true })).toBe(false); }); test("generates default array", () => { - expect(generateDefaultValue({ type: "array" })).toEqual([]); + expect(generateDefaultValue({ type: "array", required: true })).toEqual([]); }); test("generates default empty object", () => { - expect(generateDefaultValue({ type: "object" })).toEqual({}); + expect(generateDefaultValue({ type: "object", required: true })).toEqual({}); }); test("generates default null for unknown types", () => { - expect(generateDefaultValue({ type: "unknown" as any })).toBe(null); + // @ts-expect-error Testing with invalid type + expect(generateDefaultValue({ type: "unknown", required: true })).toBe(null); }); test("generates object with properties", () => { const schema: JsonSchemaType = { type: "object", + required: true, properties: { - name: { type: "string" }, - age: { type: "number" }, - isActive: { type: "boolean" }, + name: { type: "string", required: true }, + age: { type: "number", required: true }, + isActive: { type: "boolean", required: true }, }, }; expect(generateDefaultValue(schema)).toEqual({ @@ -53,15 +55,18 @@ describe("generateDefaultValue", () => { test("handles nested objects", () => { const schema: JsonSchemaType = { type: "object", + required: true, properties: { user: { type: "object", + required: true, properties: { - name: { type: "string" }, + name: { type: "string", required: true }, address: { type: "object", + required: true, properties: { - city: { type: "string" }, + city: { type: "string", required: true }, }, }, }, @@ -135,7 +140,8 @@ describe("validateValueAgainstSchema", () => { test("returns true for unknown types", () => { expect( - validateValueAgainstSchema("anything", { type: "unknown" as any }), + // @ts-expect-error Testing with invalid type + validateValueAgainstSchema("anything", { type: "unknown" }), ).toBe(true); }); }); diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index b3c8e48..9c438e7 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -4,9 +4,19 @@ import { JsonObject } from "./jsonPathUtils"; /** * Generates a default value based on a JSON schema type * @param schema The JSON schema definition - * @returns A default value matching the schema type + * @returns A default value matching the schema type, or null for non-required fields */ export function generateDefaultValue(schema: JsonSchemaType): JsonValue { + + if ("default" in schema) { + // Ensure we don't return undefined even if schema.default is undefined + return schema.default === undefined ? null : schema.default; + } + + if (!schema.required) { + return null; + } + switch (schema.type) { case "string": return ""; @@ -18,12 +28,15 @@ export function generateDefaultValue(schema: JsonSchemaType): JsonValue { case "array": return []; case "object": { + if (!schema.properties) return {}; + const obj: JsonObject = {}; - if (schema.properties) { - Object.entries(schema.properties).forEach(([key, prop]) => { - obj[key] = generateDefaultValue(prop); + Object.entries(schema.properties) + .filter(([, prop]) => prop.required) + .forEach(([key, prop]) => { + const value = generateDefaultValue(prop); + obj[key] = value; }); - } return obj; } default: @@ -53,13 +66,19 @@ export function validateValueAgainstSchema( value: JsonValue, schema: JsonSchemaType, ): boolean { + // Handle undefined values for non-required fields + if (value === undefined && !schema.required) { + return true; + } + // Basic type validation switch (schema.type) { case "string": return typeof value === "string"; case "number": - case "integer": return typeof value === "number"; + case "integer": + return typeof value === "number" && Number.isInteger(value); case "boolean": return typeof value === "boolean"; case "array": @@ -68,6 +87,8 @@ export function validateValueAgainstSchema( return ( typeof value === "object" && value !== null && !Array.isArray(value) ); + case "null": + return value === null; default: return true; } diff --git a/package-lock.json b/package-lock.json index 90af99a..a7df022 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "@types/serve-handler": "^6.1.4", "@vitejs/plugin-react": "^4.3.2", "autoprefixer": "^10.4.20", + "co": "^4.6.0", "eslint": "^9.11.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.12", @@ -4800,6 +4801,17 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",