Skip to content

Commit a4469f7

Browse files
committed
Add draft version of enhanced object input editing
1 parent 361f9d1 commit a4469f7

File tree

6 files changed

+363
-45
lines changed

6 files changed

+363
-45
lines changed

client/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
"@radix-ui/react-select": "^2.1.2",
2828
"@radix-ui/react-slot": "^1.1.0",
2929
"@radix-ui/react-tabs": "^1.1.1",
30+
"@types/prismjs": "^1.26.5",
3031
"class-variance-authority": "^0.7.0",
3132
"clsx": "^2.1.1",
3233
"lucide-react": "^0.447.0",
34+
"prismjs": "^1.29.0",
3335
"react": "^18.3.1",
3436
"react-dom": "^18.3.1",
37+
"react-simple-code-editor": "^0.14.1",
3538
"react-toastify": "^10.0.6",
3639
"serve-handler": "^6.1.6",
3740
"tailwind-merge": "^2.5.3",
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { useState } from "react";
2+
import { Button } from "@/components/ui/button";
3+
import { Input } from "@/components/ui/input";
4+
import { Label } from "@/components/ui/label";
5+
import JsonEditor from "./JsonEditor";
6+
7+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
8+
9+
export type JsonSchemaType = {
10+
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
11+
description?: string;
12+
properties?: Record<string, JsonSchemaType>;
13+
items?: JsonSchemaType;
14+
};
15+
16+
type JsonObject = { [key: string]: JsonValue };
17+
18+
interface DynamicJsonFormProps {
19+
schema: JsonSchemaType;
20+
value: JsonValue;
21+
onChange: (value: JsonValue) => void;
22+
maxDepth?: number;
23+
}
24+
25+
const formatFieldLabel = (key: string): string => {
26+
return key
27+
.replace(/([A-Z])/g, ' $1') // Insert space before capital letters
28+
.replace(/_/g, ' ') // Replace underscores with spaces
29+
.replace(/^\w/, c => c.toUpperCase()); // Capitalize first letter
30+
};
31+
32+
const DynamicJsonForm = ({
33+
schema,
34+
value,
35+
onChange,
36+
maxDepth = 3
37+
}: DynamicJsonFormProps) => {
38+
const [isJsonMode, setIsJsonMode] = useState(false);
39+
const [jsonError, setJsonError] = useState<string>();
40+
41+
const generateDefaultValue = (propSchema: JsonSchemaType): JsonValue => {
42+
switch (propSchema.type) {
43+
case 'string':
44+
return '';
45+
case 'number':
46+
case 'integer':
47+
return 0;
48+
case 'boolean':
49+
return false;
50+
case 'array':
51+
return [];
52+
case 'object': {
53+
const obj: JsonObject = {};
54+
if (propSchema.properties) {
55+
Object.entries(propSchema.properties).forEach(([key, prop]) => {
56+
obj[key] = generateDefaultValue(prop);
57+
});
58+
}
59+
return obj;
60+
}
61+
default:
62+
return null;
63+
}
64+
};
65+
66+
const renderFormFields = (
67+
propSchema: JsonSchemaType,
68+
currentValue: JsonValue,
69+
path: string[] = [],
70+
depth: number = 0
71+
) => {
72+
if (depth >= maxDepth && (propSchema.type === 'object' || propSchema.type === 'array')) {
73+
// Render as JSON editor when max depth is reached
74+
return (
75+
<JsonEditor
76+
value={JSON.stringify(currentValue ?? generateDefaultValue(propSchema), null, 2)}
77+
onChange={(newValue) => {
78+
try {
79+
const parsed = JSON.parse(newValue);
80+
handleFieldChange(path, parsed);
81+
setJsonError(undefined);
82+
} catch (err) {
83+
setJsonError(err instanceof Error ? err.message : 'Invalid JSON');
84+
}
85+
}}
86+
error={jsonError}
87+
/>
88+
);
89+
}
90+
91+
switch (propSchema.type) {
92+
case 'string':
93+
case 'number':
94+
case 'integer':
95+
return (
96+
<Input
97+
type={propSchema.type === 'string' ? 'text' : 'number'}
98+
value={(currentValue as string | number) ?? ''}
99+
onChange={(e) => handleFieldChange(path,
100+
propSchema.type === 'string' ? e.target.value : Number(e.target.value)
101+
)}
102+
placeholder={propSchema.description}
103+
/>
104+
);
105+
case 'boolean':
106+
return (
107+
<Input
108+
type="checkbox"
109+
checked={(currentValue as boolean) ?? false}
110+
onChange={(e) => handleFieldChange(path, e.target.checked)}
111+
className="w-4 h-4"
112+
/>
113+
);
114+
case 'object':
115+
if (!propSchema.properties) return null;
116+
return (
117+
<div className="space-y-4 border rounded-md p-4">
118+
{Object.entries(propSchema.properties).map(([key, prop]) => (
119+
<div key={key} className="space-y-2">
120+
<Label>{formatFieldLabel(key)}</Label>
121+
{renderFormFields(
122+
prop,
123+
(currentValue as JsonObject)?.[key],
124+
[...path, key],
125+
depth + 1
126+
)}
127+
</div>
128+
))}
129+
</div>
130+
);
131+
case 'array': {
132+
const arrayValue = Array.isArray(currentValue) ? currentValue : [];
133+
if (!propSchema.items) return null;
134+
return (
135+
<div className="space-y-2">
136+
{arrayValue.map((item, index) => (
137+
<div key={index} className="flex items-center gap-2">
138+
{renderFormFields(
139+
propSchema.items as JsonSchemaType,
140+
item,
141+
[...path, index.toString()],
142+
depth + 1
143+
)}
144+
<Button
145+
variant="outline"
146+
size="sm"
147+
onClick={() => {
148+
const newArray = [...arrayValue];
149+
newArray.splice(index, 1);
150+
handleFieldChange(path, newArray);
151+
}}
152+
>
153+
Remove
154+
</Button>
155+
</div>
156+
))}
157+
<Button
158+
variant="outline"
159+
size="sm"
160+
onClick={() => {
161+
handleFieldChange(
162+
path,
163+
[...arrayValue, generateDefaultValue(propSchema.items as JsonSchemaType)]
164+
);
165+
}}
166+
>
167+
Add Item
168+
</Button>
169+
</div>
170+
);
171+
}
172+
default:
173+
return null;
174+
}
175+
};
176+
177+
const handleFieldChange = (path: string[], fieldValue: JsonValue) => {
178+
if (path.length === 0) {
179+
onChange(fieldValue);
180+
return;
181+
}
182+
183+
const newValue = { ...(typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {}) } as JsonObject;
184+
let current: JsonObject = newValue;
185+
186+
for (let i = 0; i < path.length - 1; i++) {
187+
const key = path[i];
188+
if (!(key in current)) {
189+
current[key] = {};
190+
}
191+
current = current[key] as JsonObject;
192+
}
193+
194+
current[path[path.length - 1]] = fieldValue;
195+
onChange(newValue);
196+
};
197+
198+
return (
199+
<div className="space-y-4">
200+
<div className="flex justify-end">
201+
<Button
202+
variant="outline"
203+
size="sm"
204+
onClick={() => setIsJsonMode(!isJsonMode)}
205+
>
206+
{isJsonMode ? "Switch to Form" : "Switch to JSON"}
207+
</Button>
208+
</div>
209+
210+
{isJsonMode ? (
211+
<JsonEditor
212+
value={JSON.stringify(value ?? generateDefaultValue(schema), null, 2)}
213+
onChange={(newValue) => {
214+
try {
215+
onChange(JSON.parse(newValue));
216+
setJsonError(undefined);
217+
} catch (err) {
218+
setJsonError(err instanceof Error ? err.message : 'Invalid JSON');
219+
}
220+
}}
221+
error={jsonError}
222+
/>
223+
) : (
224+
renderFormFields(schema, value)
225+
)}
226+
</div>
227+
);
228+
};
229+
230+
export default DynamicJsonForm;

client/src/components/JsonEditor.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Editor from 'react-simple-code-editor';
2+
import Prism from 'prismjs';
3+
import 'prismjs/components/prism-json';
4+
import 'prismjs/themes/prism.css';
5+
import { Button } from "@/components/ui/button";
6+
7+
interface JsonEditorProps {
8+
value: string;
9+
onChange: (value: string) => void;
10+
error?: string;
11+
}
12+
13+
const JsonEditor = ({ value, onChange, error }: JsonEditorProps) => {
14+
const formatJson = (json: string): string => {
15+
try {
16+
return JSON.stringify(JSON.parse(json), null, 2);
17+
} catch {
18+
return json;
19+
}
20+
};
21+
22+
return (
23+
<div className="relative space-y-2">
24+
<div className="flex justify-end">
25+
<Button
26+
variant="outline"
27+
size="sm"
28+
onClick={() => onChange(formatJson(value))}
29+
>
30+
Format JSON
31+
</Button>
32+
</div>
33+
<div
34+
className={`border rounded-md ${
35+
error ? 'border-red-500' : 'border-gray-200 dark:border-gray-800'
36+
}`}
37+
>
38+
<Editor
39+
value={value}
40+
onValueChange={onChange}
41+
highlight={code =>
42+
Prism.highlight(code, Prism.languages.json, 'json')
43+
}
44+
padding={10}
45+
style={{
46+
fontFamily: '"Fira code", "Fira Mono", monospace',
47+
fontSize: 14,
48+
backgroundColor: 'transparent',
49+
minHeight: '100px',
50+
}}
51+
className="w-full"
52+
/>
53+
</div>
54+
{error && (
55+
<p className="text-sm text-red-500 mt-1">{error}</p>
56+
)}
57+
</div>
58+
);
59+
};
60+
61+
export default JsonEditor;

0 commit comments

Comments
 (0)