Skip to content

Commit 3e95d9d

Browse files
committed
pr feedback: Only use form if object is simple
1 parent f6ed09e commit 3e95d9d

File tree

3 files changed

+59
-114
lines changed

3 files changed

+59
-114
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 14 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { useState, useEffect, useCallback, useRef } from "react";
22
import { Button } from "@/components/ui/button";
33
import { Input } from "@/components/ui/input";
4-
import { Label } from "@/components/ui/label";
54
import JsonEditor from "./JsonEditor";
6-
import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils";
7-
import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils";
5+
import { updateValueAtPath } from "@/utils/jsonPathUtils";
6+
import { generateDefaultValue } from "@/utils/schemaUtils";
87

98
export type JsonValue =
109
| string
@@ -36,17 +35,25 @@ interface DynamicJsonFormProps {
3635
value: JsonValue;
3736
onChange: (value: JsonValue) => void;
3837
maxDepth?: number;
39-
onlyJSON?: boolean;
4038
}
4139

40+
const isSimpleObject = (schema: JsonSchemaType): boolean => {
41+
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
42+
if (supportedTypes.includes(schema.type)) return true;
43+
if (schema.type !== "object") return false;
44+
return Object.values(schema.properties ?? {}).every((prop) =>
45+
supportedTypes.includes(prop.type),
46+
);
47+
};
48+
4249
const DynamicJsonForm = ({
4350
schema,
4451
value,
4552
onChange,
4653
maxDepth = 3,
47-
onlyJSON = false,
4854
}: DynamicJsonFormProps) => {
49-
const [isJsonMode, setIsJsonMode] = useState(onlyJSON);
55+
const isOnlyJSON = !isSimpleObject(schema);
56+
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
5057
const [jsonError, setJsonError] = useState<string>();
5158
// Store the raw JSON string to allow immediate feedback during typing
5259
// while deferring parsing until the user stops typing
@@ -233,111 +240,6 @@ const DynamicJsonForm = ({
233240
required={propSchema.required}
234241
/>
235242
);
236-
case "object": {
237-
// Handle case where we have a value but no schema properties
238-
const objectValue = (currentValue as JsonObject) || {};
239-
240-
// If we have schema properties, use them to render fields
241-
if (propSchema.properties) {
242-
return (
243-
<div className="space-y-4 border rounded-md p-4">
244-
{Object.entries(propSchema.properties).map(([key, prop]) => (
245-
<div key={key} className="space-y-2">
246-
<Label>{formatFieldLabel(key)}</Label>
247-
{renderFormFields(
248-
prop,
249-
objectValue[key],
250-
[...path, key],
251-
depth + 1,
252-
)}
253-
</div>
254-
))}
255-
</div>
256-
);
257-
}
258-
// If we have a value but no schema properties, render fields based on the value
259-
else if (Object.keys(objectValue).length > 0) {
260-
return (
261-
<div className="space-y-4 border rounded-md p-4">
262-
{Object.entries(objectValue).map(([key, value]) => (
263-
<div key={key} className="space-y-2">
264-
<Label>{formatFieldLabel(key)}</Label>
265-
<Input
266-
type="text"
267-
value={String(value)}
268-
onChange={(e) =>
269-
handleFieldChange([...path, key], e.target.value)
270-
}
271-
/>
272-
</div>
273-
))}
274-
</div>
275-
);
276-
}
277-
// If we have neither schema properties nor value, return null
278-
return null;
279-
}
280-
case "array": {
281-
const arrayValue = Array.isArray(currentValue) ? currentValue : [];
282-
if (!propSchema.items) return null;
283-
return (
284-
<div className="space-y-4">
285-
{propSchema.description && (
286-
<p className="text-sm text-gray-600">{propSchema.description}</p>
287-
)}
288-
289-
{propSchema.items?.description && (
290-
<p className="text-sm text-gray-500">
291-
Items: {propSchema.items.description}
292-
</p>
293-
)}
294-
295-
<div className="space-y-2">
296-
{arrayValue.map((item, index) => (
297-
<div key={index} className="flex items-center gap-2">
298-
{renderFormFields(
299-
propSchema.items as JsonSchemaType,
300-
item,
301-
[...path, index.toString()],
302-
depth + 1,
303-
)}
304-
<Button
305-
variant="outline"
306-
size="sm"
307-
onClick={() => {
308-
const newArray = [...arrayValue];
309-
newArray.splice(index, 1);
310-
handleFieldChange(path, newArray);
311-
}}
312-
>
313-
Remove
314-
</Button>
315-
</div>
316-
))}
317-
<Button
318-
variant="outline"
319-
size="sm"
320-
onClick={() => {
321-
const defaultValue = generateDefaultValue(
322-
propSchema.items as JsonSchemaType,
323-
);
324-
handleFieldChange(path, [
325-
...arrayValue,
326-
defaultValue ?? null,
327-
]);
328-
}}
329-
title={
330-
propSchema.items?.description
331-
? `Add new ${propSchema.items.description}`
332-
: "Add new item"
333-
}
334-
>
335-
Add Item
336-
</Button>
337-
</div>
338-
</div>
339-
);
340-
}
341243
default:
342244
return null;
343245
}
@@ -376,7 +278,7 @@ const DynamicJsonForm = ({
376278
Format JSON
377279
</Button>
378280
)}
379-
{!onlyJSON && (
281+
{!isOnlyJSON && (
380282
<Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
381283
{isJsonMode ? "Switch to Form" : "Switch to JSON"}
382284
</Button>

client/src/components/ToolsTab.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ const ToolsTab = ({
234234
) : (
235235
<div className="mt-1">
236236
<DynamicJsonForm
237-
onlyJSON
238237
schema={{
239238
type: prop.type,
240239
properties: prop.properties,

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, fireEvent } from "@testing-library/react";
1+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
22
import { describe, it, expect, jest } from "@jest/globals";
33
import DynamicJsonForm from "../DynamicJsonForm";
44
import type { JsonSchemaType } from "../DynamicJsonForm";
@@ -93,3 +93,47 @@ describe("DynamicJsonForm Integer Fields", () => {
9393
});
9494
});
9595
});
96+
97+
describe("DynamicJsonForm Complex Fields", () => {
98+
const renderForm = (props = {}) => {
99+
const defaultProps = {
100+
schema: {
101+
type: "object",
102+
properties: {
103+
// The simplified JsonSchemaType does not accept oneOf fields
104+
// But they exist in the more-complete JsonSchema7Type
105+
nested: { oneOf: [{ type: "string" }, { type: "integer" }] },
106+
},
107+
} as unknown as JsonSchemaType,
108+
value: undefined,
109+
onChange: jest.fn(),
110+
};
111+
return render(<DynamicJsonForm {...defaultProps} {...props} />);
112+
};
113+
114+
describe("Basic Operations", () => {
115+
it("should render textbox and autoformat button, but no switch-to-form button", () => {
116+
renderForm();
117+
const input = screen.getByRole("textbox");
118+
expect(input).toHaveProperty("type", "textarea");
119+
const buttons = screen.getAllByRole("button");
120+
expect(buttons).toHaveLength(1);
121+
expect(buttons[0]).toHaveProperty("textContent", "Format JSON");
122+
});
123+
124+
it("should pass changed values to onChange", () => {
125+
const onChange = jest.fn();
126+
renderForm({ onChange });
127+
128+
const input = screen.getByRole("textbox");
129+
fireEvent.change(input, {
130+
target: { value: `{ "nested": "i am string" }` },
131+
});
132+
133+
// The onChange handler is debounced when using the JSON view, so we need to wait a little bit
134+
waitFor(() => {
135+
expect(onChange).toHaveBeenCalledWith(`{ "nested": "i am string" }`);
136+
});
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)