Skip to content

Commit 3475670

Browse files
committed
Incorporate nested logic again
1 parent e5e2edc commit 3475670

File tree

2 files changed

+356
-5
lines changed

2 files changed

+356
-5
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,39 @@ interface DynamicJsonFormProps {
1616
const isSimpleObject = (schema: JsonSchemaType): boolean => {
1717
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
1818
if (supportedTypes.includes(schema.type)) return true;
19-
if (schema.type !== "object") return false;
20-
return Object.values(schema.properties ?? {}).every((prop) =>
21-
supportedTypes.includes(prop.type),
22-
);
19+
if (schema.type === "object") {
20+
return Object.values(schema.properties ?? {}).every((prop) =>
21+
supportedTypes.includes(prop.type),
22+
);
23+
}
24+
if (schema.type === "array") {
25+
return !!schema.items && isSimpleObject(schema.items);
26+
}
27+
return false;
28+
};
29+
30+
const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
31+
if ("default" in schema && schema.default !== undefined) {
32+
return schema.default;
33+
}
34+
35+
switch (schema.type) {
36+
case "string":
37+
return "";
38+
case "number":
39+
case "integer":
40+
return 0;
41+
case "boolean":
42+
return false;
43+
case "array":
44+
return [];
45+
case "object":
46+
return {};
47+
case "null":
48+
return null;
49+
default:
50+
return null;
51+
}
2352
};
2453

2554
const DynamicJsonForm = ({
@@ -257,7 +286,72 @@ const DynamicJsonForm = ({
257286
))}
258287
</div>
259288
);
260-
case "array":
289+
case "array": {
290+
const arrayValue = Array.isArray(currentValue) ? currentValue : [];
291+
if (!propSchema.items) return null;
292+
293+
// If the array items are simple, render as form fields, otherwise use JSON editor
294+
if (isSimpleObject(propSchema.items)) {
295+
return (
296+
<div className="space-y-4">
297+
{propSchema.description && (
298+
<p className="text-sm text-gray-600">{propSchema.description}</p>
299+
)}
300+
301+
{propSchema.items?.description && (
302+
<p className="text-sm text-gray-500">
303+
Items: {propSchema.items.description}
304+
</p>
305+
)}
306+
307+
<div className="space-y-2">
308+
{arrayValue.map((item, index) => (
309+
<div key={index} className="flex items-center gap-2">
310+
{renderFormFields(
311+
propSchema.items as JsonSchemaType,
312+
item,
313+
[...path, index.toString()],
314+
depth + 1,
315+
)}
316+
<Button
317+
variant="outline"
318+
size="sm"
319+
onClick={() => {
320+
const newArray = [...arrayValue];
321+
newArray.splice(index, 1);
322+
handleFieldChange(path, newArray);
323+
}}
324+
>
325+
Remove
326+
</Button>
327+
</div>
328+
))}
329+
<Button
330+
variant="outline"
331+
size="sm"
332+
onClick={() => {
333+
const defaultValue = getArrayItemDefault(
334+
propSchema.items as JsonSchemaType,
335+
);
336+
handleFieldChange(path, [
337+
...arrayValue,
338+
defaultValue,
339+
]);
340+
}}
341+
title={
342+
propSchema.items?.description
343+
? `Add new ${propSchema.items.description}`
344+
: "Add new item"
345+
}
346+
>
347+
Add Item
348+
</Button>
349+
</div>
350+
</div>
351+
);
352+
}
353+
354+
// For complex arrays, fall back to JSON editor
261355
return (
262356
<JsonEditor
263357
value={JSON.stringify(currentValue ?? [], null, 2)}
@@ -275,6 +369,7 @@ const DynamicJsonForm = ({
275369
error={jsonError}
276370
/>
277371
);
372+
}
278373
default:
279374
return null;
280375
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { render, screen, fireEvent } from "@testing-library/react";
2+
import { describe, it, expect, jest } from "@jest/globals";
3+
import DynamicJsonForm from "../DynamicJsonForm";
4+
import type { JsonSchemaType } from "@/utils/jsonUtils";
5+
6+
describe("DynamicJsonForm Array Fields", () => {
7+
const renderSimpleArrayForm = (props = {}) => {
8+
const defaultProps = {
9+
schema: {
10+
type: "array" as const,
11+
description: "Test array field",
12+
items: {
13+
type: "string" as const,
14+
description: "Array item",
15+
},
16+
} satisfies JsonSchemaType,
17+
value: [],
18+
onChange: jest.fn(),
19+
};
20+
return render(<DynamicJsonForm {...defaultProps} {...props} />);
21+
};
22+
23+
const renderComplexArrayForm = (props = {}) => {
24+
const defaultProps = {
25+
schema: {
26+
type: "array" as const,
27+
description: "Test complex array field",
28+
items: {
29+
type: "object" as const,
30+
properties: {
31+
nested: { type: "object" as const },
32+
},
33+
},
34+
} satisfies JsonSchemaType,
35+
value: [],
36+
onChange: jest.fn(),
37+
};
38+
return render(<DynamicJsonForm {...defaultProps} {...props} />);
39+
};
40+
41+
describe("Simple Array Rendering", () => {
42+
it("should render form fields for simple array items", () => {
43+
renderSimpleArrayForm({ value: ["item1", "item2"] });
44+
45+
// Should show array description
46+
expect(screen.getByText("Test array field")).toBeDefined();
47+
expect(screen.getByText("Items: Array item")).toBeDefined();
48+
49+
// Should show input fields for each item
50+
const inputs = screen.getAllByRole("textbox");
51+
expect(inputs).toHaveLength(2);
52+
expect(inputs[0]).toHaveProperty("value", "item1");
53+
expect(inputs[1]).toHaveProperty("value", "item2");
54+
55+
// Should show remove buttons
56+
const removeButtons = screen.getAllByText("Remove");
57+
expect(removeButtons).toHaveLength(2);
58+
59+
// Should show add button
60+
expect(screen.getByText("Add Item")).toBeDefined();
61+
});
62+
63+
it("should add new items when Add Item button is clicked", () => {
64+
const onChange = jest.fn();
65+
renderSimpleArrayForm({ value: ["item1"], onChange });
66+
67+
const addButton = screen.getByText("Add Item");
68+
fireEvent.click(addButton);
69+
70+
expect(onChange).toHaveBeenCalledWith(["item1", ""]);
71+
});
72+
73+
it("should remove items when Remove button is clicked", () => {
74+
const onChange = jest.fn();
75+
renderSimpleArrayForm({ value: ["item1", "item2"], onChange });
76+
77+
const removeButtons = screen.getAllByText("Remove");
78+
fireEvent.click(removeButtons[0]);
79+
80+
expect(onChange).toHaveBeenCalledWith(["item2"]);
81+
});
82+
83+
it("should update item values when input changes", () => {
84+
const onChange = jest.fn();
85+
renderSimpleArrayForm({ value: ["item1"], onChange });
86+
87+
const input = screen.getByRole("textbox");
88+
fireEvent.change(input, { target: { value: "updated item" } });
89+
90+
expect(onChange).toHaveBeenCalledWith(["updated item"]);
91+
});
92+
93+
it("should handle empty arrays", () => {
94+
renderSimpleArrayForm({ value: [] });
95+
96+
// Should show description and add button but no items
97+
expect(screen.getByText("Test array field")).toBeDefined();
98+
expect(screen.getByText("Add Item")).toBeDefined();
99+
expect(screen.queryByText("Remove")).toBeNull();
100+
});
101+
});
102+
103+
describe("Complex Array Fallback", () => {
104+
it("should render JSON editor for complex arrays", () => {
105+
renderComplexArrayForm();
106+
107+
// Should render as JSON editor (textarea)
108+
const textarea = screen.getByRole("textbox");
109+
expect(textarea).toHaveProperty("type", "textarea");
110+
111+
// Should not show form-specific array controls
112+
expect(screen.queryByText("Add Item")).toBeNull();
113+
expect(screen.queryByText("Remove")).toBeNull();
114+
});
115+
});
116+
117+
describe("Array Type Detection", () => {
118+
it("should detect string arrays as simple", () => {
119+
const schema = {
120+
type: "array" as const,
121+
items: { type: "string" as const },
122+
};
123+
renderSimpleArrayForm({ schema, value: ["test"] });
124+
125+
// Should render form fields, not JSON editor
126+
expect(screen.getByRole("textbox")).not.toHaveProperty(
127+
"type",
128+
"textarea",
129+
);
130+
});
131+
132+
it("should detect number arrays as simple", () => {
133+
const schema = {
134+
type: "array" as const,
135+
items: { type: "number" as const },
136+
};
137+
renderSimpleArrayForm({ schema, value: [1, 2] });
138+
139+
// Should render form fields (number inputs)
140+
const inputs = screen.getAllByRole("spinbutton");
141+
expect(inputs).toHaveLength(2);
142+
});
143+
144+
it("should detect boolean arrays as simple", () => {
145+
const schema = {
146+
type: "array" as const,
147+
items: { type: "boolean" as const },
148+
};
149+
renderSimpleArrayForm({ schema, value: [true, false] });
150+
151+
// Should render form fields (checkboxes)
152+
const checkboxes = screen.getAllByRole("checkbox");
153+
expect(checkboxes).toHaveLength(2);
154+
});
155+
156+
it("should detect simple object arrays as simple", () => {
157+
const schema = {
158+
type: "array" as const,
159+
items: {
160+
type: "object" as const,
161+
properties: {
162+
name: { type: "string" as const },
163+
age: { type: "number" as const },
164+
},
165+
},
166+
};
167+
renderSimpleArrayForm({ schema, value: [{ name: "John", age: 30 }] });
168+
169+
// Should render form fields for simple objects
170+
expect(screen.getByText("Add Item")).toBeDefined();
171+
expect(screen.getByText("Remove")).toBeDefined();
172+
});
173+
});
174+
175+
describe("Array with Different Item Types", () => {
176+
it("should handle integer array items", () => {
177+
const schema = {
178+
type: "array" as const,
179+
items: { type: "integer" as const },
180+
};
181+
const onChange = jest.fn();
182+
renderSimpleArrayForm({ schema, value: [1, 2], onChange });
183+
184+
const inputs = screen.getAllByRole("spinbutton");
185+
expect(inputs).toHaveLength(2);
186+
expect(inputs[0]).toHaveProperty("value", "1");
187+
expect(inputs[1]).toHaveProperty("value", "2");
188+
189+
// Test adding new integer item
190+
const addButton = screen.getByText("Add Item");
191+
fireEvent.click(addButton);
192+
expect(onChange).toHaveBeenCalledWith([1, 2, 0]);
193+
});
194+
195+
it("should handle boolean array items", () => {
196+
const schema = {
197+
type: "array" as const,
198+
items: { type: "boolean" as const },
199+
};
200+
const onChange = jest.fn();
201+
renderSimpleArrayForm({ schema, value: [true, false], onChange });
202+
203+
const checkboxes = screen.getAllByRole("checkbox");
204+
expect(checkboxes).toHaveLength(2);
205+
expect(checkboxes[0]).toHaveProperty("checked", true);
206+
expect(checkboxes[1]).toHaveProperty("checked", false);
207+
208+
// Test adding new boolean item
209+
const addButton = screen.getByText("Add Item");
210+
fireEvent.click(addButton);
211+
expect(onChange).toHaveBeenCalledWith([true, false, false]);
212+
});
213+
});
214+
215+
describe("Array Item Descriptions", () => {
216+
it("should show item description when available", () => {
217+
const schema = {
218+
type: "array" as const,
219+
description: "List of names",
220+
items: {
221+
type: "string" as const,
222+
description: "Person name",
223+
},
224+
};
225+
renderSimpleArrayForm({ schema });
226+
227+
expect(screen.getByText("List of names")).toBeDefined();
228+
expect(screen.getByText("Items: Person name")).toBeDefined();
229+
});
230+
231+
it("should use item description in add button title", () => {
232+
const schema = {
233+
type: "array" as const,
234+
items: {
235+
type: "string" as const,
236+
description: "Email address",
237+
},
238+
};
239+
renderSimpleArrayForm({ schema });
240+
241+
const addButton = screen.getByText("Add Item");
242+
expect(addButton).toHaveProperty("title", "Add new Email address");
243+
});
244+
245+
it("should use default title when no item description", () => {
246+
const schema = {
247+
type: "array" as const,
248+
items: { type: "string" as const },
249+
};
250+
renderSimpleArrayForm({ schema });
251+
252+
const addButton = screen.getByText("Add Item");
253+
expect(addButton).toHaveProperty("title", "Add new item");
254+
});
255+
});
256+
});

0 commit comments

Comments
 (0)