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