Skip to content

Commit efd5f8a

Browse files
committed
feat: enum support in dynamic json form
1 parent 3a121bc commit efd5f8a

File tree

2 files changed

+110
-10
lines changed

2 files changed

+110
-10
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ interface DynamicJsonFormProps {
1515

1616
const isSimpleObject = (schema: JsonSchemaType): boolean => {
1717
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
18-
if (supportedTypes.includes(schema.type)) return true;
18+
if (schema.type && supportedTypes.includes(schema.type)) return true;
1919
if (schema.type === "object") {
20-
return Object.values(schema.properties ?? {}).every((prop) =>
21-
supportedTypes.includes(prop.type),
20+
return Object.values(schema.properties ?? {}).every(
21+
(prop) => prop.type && supportedTypes.includes(prop.type),
2222
);
2323
}
2424
if (schema.type === "array") {
@@ -178,6 +178,41 @@ const DynamicJsonForm = ({
178178

179179
switch (propSchema.type) {
180180
case "string": {
181+
if (
182+
propSchema.oneOf &&
183+
propSchema.oneOf.every(
184+
(option) =>
185+
typeof option.const === "string" &&
186+
typeof option.title === "string",
187+
)
188+
) {
189+
return (
190+
<select
191+
value={(currentValue as string) ?? ""}
192+
onChange={(e) => {
193+
const val = e.target.value;
194+
if (!val && !isRequired) {
195+
handleFieldChange(path, undefined);
196+
} else {
197+
handleFieldChange(path, val);
198+
}
199+
}}
200+
required={isRequired}
201+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
202+
>
203+
<option value="">Select an option...</option>
204+
{propSchema.oneOf.map((option) => (
205+
<option
206+
key={option.const as string}
207+
value={option.const as string}
208+
>
209+
{option.title as string}
210+
</option>
211+
))}
212+
</select>
213+
);
214+
}
215+
181216
if (propSchema.enum) {
182217
return (
183218
<select
@@ -194,9 +229,9 @@ const DynamicJsonForm = ({
194229
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
195230
>
196231
<option value="">Select an option...</option>
197-
{propSchema.enum.map((option, index) => (
232+
{propSchema.enum.map((option) => (
198233
<option key={option} value={option}>
199-
{propSchema.enumNames?.[index] || option}
234+
{option}
200235
</option>
201236
))}
202237
</select>
@@ -233,6 +268,9 @@ const DynamicJsonForm = ({
233268
}}
234269
placeholder={propSchema.description}
235270
required={isRequired}
271+
minLength={propSchema.minLength}
272+
maxLength={propSchema.maxLength}
273+
pattern={propSchema.pattern}
236274
/>
237275
);
238276
}
@@ -255,6 +293,8 @@ const DynamicJsonForm = ({
255293
}}
256294
placeholder={propSchema.description}
257295
required={isRequired}
296+
min={propSchema.minimum}
297+
max={propSchema.maximum}
258298
/>
259299
);
260300

@@ -277,6 +317,8 @@ const DynamicJsonForm = ({
277317
}}
278318
placeholder={propSchema.description}
279319
required={isRequired}
320+
min={propSchema.minimum}
321+
max={propSchema.maximum}
280322
/>
281323
);
282324

client/src/components/__tests__/DynamicJsonForm.test.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,13 @@ describe("DynamicJsonForm String Fields", () => {
103103
expect(options).toHaveLength(4);
104104
});
105105

106-
it("should use enumNames for option labels", () => {
106+
it("should use oneOf with const and title for labeled options", () => {
107107
const schema: JsonSchemaType = {
108108
type: "string",
109-
enum: ["val1", "val2"],
110-
enumNames: ["Label 1", "Label 2"],
109+
oneOf: [
110+
{ const: "val1", title: "Label 1" },
111+
{ const: "val2", title: "Label 2" },
112+
],
111113
description: "Select with labels",
112114
};
113115
render(<DynamicJsonForm schema={schema} value="" onChange={jest.fn()} />);
@@ -117,6 +119,24 @@ describe("DynamicJsonForm String Fields", () => {
117119
expect(options[2]).toHaveProperty("textContent", "Label 2");
118120
});
119121

122+
it("should call onChange with selected oneOf value", () => {
123+
const onChange = jest.fn();
124+
const schema: JsonSchemaType = {
125+
type: "string",
126+
oneOf: [
127+
{ const: "option1", title: "Option 1" },
128+
{ const: "option2", title: "Option 2" },
129+
],
130+
description: "Select an option",
131+
};
132+
render(<DynamicJsonForm schema={schema} value="" onChange={onChange} />);
133+
134+
const select = screen.getByRole("combobox");
135+
fireEvent.change(select, { target: { value: "option1" } });
136+
137+
expect(onChange).toHaveBeenCalledWith("option1");
138+
});
139+
120140
it("should call onChange with selected enum value", () => {
121141
const onChange = jest.fn();
122142
const schema: JsonSchemaType = {
@@ -131,6 +151,44 @@ describe("DynamicJsonForm String Fields", () => {
131151

132152
expect(onChange).toHaveBeenCalledWith("option1");
133153
});
154+
155+
it("should render JSON Schema spec compliant oneOf with const for labeled enums", () => {
156+
// Example from JSON Schema spec: labeled enums using oneOf with const
157+
const onChange = jest.fn();
158+
const schema: JsonSchemaType = {
159+
type: "string",
160+
title: "Traffic Light",
161+
description: "Select a traffic light color",
162+
oneOf: [
163+
{ const: "red", title: "Stop" },
164+
{ const: "amber", title: "Caution" },
165+
{ const: "green", title: "Go" },
166+
],
167+
};
168+
render(<DynamicJsonForm schema={schema} value="" onChange={onChange} />);
169+
170+
// Should render as a select dropdown
171+
const select = screen.getByRole("combobox");
172+
expect(select.tagName).toBe("SELECT");
173+
174+
// Should have options with proper labels
175+
const options = screen.getAllByRole("option");
176+
expect(options).toHaveLength(4); // 3 options + 1 default "Select an option..."
177+
178+
expect(options[0]).toHaveProperty("textContent", "Select an option...");
179+
expect(options[1]).toHaveProperty("textContent", "Stop");
180+
expect(options[2]).toHaveProperty("textContent", "Caution");
181+
expect(options[3]).toHaveProperty("textContent", "Go");
182+
183+
// Should have proper values
184+
expect(options[1]).toHaveProperty("value", "red");
185+
expect(options[2]).toHaveProperty("value", "amber");
186+
expect(options[3]).toHaveProperty("value", "green");
187+
188+
// Test onChange behavior
189+
fireEvent.change(select, { target: { value: "amber" } });
190+
expect(onChange).toHaveBeenCalledWith("amber");
191+
});
134192
});
135193

136194
describe("Validation Attributes", () => {
@@ -464,8 +522,8 @@ describe("DynamicJsonForm Object Fields", () => {
464522
<DynamicJsonForm schema={schema} value={{}} onChange={jest.fn()} />,
465523
);
466524

467-
const nameLabel = screen.getByText("Name");
468-
const optionalLabel = screen.getByText("Optional");
525+
const nameLabel = screen.getByText("name");
526+
const optionalLabel = screen.getByText("optional");
469527

470528
const nameInput = nameLabel.closest("div")?.querySelector("input");
471529
const optionalInput = optionalLabel

0 commit comments

Comments
 (0)