diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index b988b457..775fefcf 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -15,10 +15,10 @@ interface DynamicJsonFormProps { const isSimpleObject = (schema: JsonSchemaType): boolean => { const supportedTypes = ["string", "number", "integer", "boolean", "null"]; - if (supportedTypes.includes(schema.type)) return true; + if (schema.type && supportedTypes.includes(schema.type)) return true; if (schema.type === "object") { - return Object.values(schema.properties ?? {}).every((prop) => - supportedTypes.includes(prop.type), + return Object.values(schema.properties ?? {}).every( + (prop) => prop.type && supportedTypes.includes(prop.type), ); } if (schema.type === "array") { @@ -178,6 +178,41 @@ const DynamicJsonForm = ({ switch (propSchema.type) { case "string": { + if ( + propSchema.oneOf && + propSchema.oneOf.every( + (option) => + typeof option.const === "string" && + typeof option.title === "string", + ) + ) { + return ( + + ); + } + if (propSchema.enum) { return ( @@ -233,6 +268,9 @@ const DynamicJsonForm = ({ }} placeholder={propSchema.description} required={isRequired} + minLength={propSchema.minLength} + maxLength={propSchema.maxLength} + pattern={propSchema.pattern} /> ); } @@ -255,6 +293,8 @@ const DynamicJsonForm = ({ }} placeholder={propSchema.description} required={isRequired} + min={propSchema.minimum} + max={propSchema.maximum} /> ); @@ -277,6 +317,8 @@ const DynamicJsonForm = ({ }} placeholder={propSchema.description} required={isRequired} + min={propSchema.minimum} + max={propSchema.maximum} /> ); diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index 2a0da799..84886157 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -103,11 +103,13 @@ describe("DynamicJsonForm String Fields", () => { expect(options).toHaveLength(4); }); - it("should use enumNames for option labels", () => { + it("should use oneOf with const and title for labeled options", () => { const schema: JsonSchemaType = { type: "string", - enum: ["val1", "val2"], - enumNames: ["Label 1", "Label 2"], + oneOf: [ + { const: "val1", title: "Label 1" }, + { const: "val2", title: "Label 2" }, + ], description: "Select with labels", }; render(); @@ -117,6 +119,24 @@ describe("DynamicJsonForm String Fields", () => { expect(options[2]).toHaveProperty("textContent", "Label 2"); }); + it("should call onChange with selected oneOf value", () => { + const onChange = jest.fn(); + const schema: JsonSchemaType = { + type: "string", + oneOf: [ + { const: "option1", title: "Option 1" }, + { const: "option2", title: "Option 2" }, + ], + description: "Select an option", + }; + render(); + + const select = screen.getByRole("combobox"); + fireEvent.change(select, { target: { value: "option1" } }); + + expect(onChange).toHaveBeenCalledWith("option1"); + }); + it("should call onChange with selected enum value", () => { const onChange = jest.fn(); const schema: JsonSchemaType = { @@ -131,6 +151,44 @@ describe("DynamicJsonForm String Fields", () => { expect(onChange).toHaveBeenCalledWith("option1"); }); + + it("should render JSON Schema spec compliant oneOf with const for labeled enums", () => { + // Example from JSON Schema spec: labeled enums using oneOf with const + const onChange = jest.fn(); + const schema: JsonSchemaType = { + type: "string", + title: "Traffic Light", + description: "Select a traffic light color", + oneOf: [ + { const: "red", title: "Stop" }, + { const: "amber", title: "Caution" }, + { const: "green", title: "Go" }, + ], + }; + render(); + + // Should render as a select dropdown + const select = screen.getByRole("combobox"); + expect(select.tagName).toBe("SELECT"); + + // Should have options with proper labels + const options = screen.getAllByRole("option"); + expect(options).toHaveLength(4); // 3 options + 1 default "Select an option..." + + expect(options[0]).toHaveProperty("textContent", "Select an option..."); + expect(options[1]).toHaveProperty("textContent", "Stop"); + expect(options[2]).toHaveProperty("textContent", "Caution"); + expect(options[3]).toHaveProperty("textContent", "Go"); + + // Should have proper values + expect(options[1]).toHaveProperty("value", "red"); + expect(options[2]).toHaveProperty("value", "amber"); + expect(options[3]).toHaveProperty("value", "green"); + + // Test onChange behavior + fireEvent.change(select, { target: { value: "amber" } }); + expect(onChange).toHaveBeenCalledWith("amber"); + }); }); describe("Validation Attributes", () => { @@ -464,8 +522,8 @@ describe("DynamicJsonForm Object Fields", () => { , ); - const nameLabel = screen.getByText("Name"); - const optionalLabel = screen.getByText("Optional"); + const nameLabel = screen.getByText("name"); + const optionalLabel = screen.getByText("optional"); const nameInput = nameLabel.closest("div")?.querySelector("input"); const optionalInput = optionalLabel