@@ -16,10 +16,39 @@ interface DynamicJsonFormProps {
1616const 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
2554const DynamicJsonForm = ( {
@@ -113,6 +142,8 @@ const DynamicJsonForm = ({
113142 currentValue : JsonValue ,
114143 path : string [ ] = [ ] ,
115144 depth : number = 0 ,
145+ parentSchema ?: JsonSchemaType ,
146+ propertyName ?: string ,
116147 ) => {
117148 if (
118149 depth >= maxDepth &&
@@ -122,7 +153,8 @@ const DynamicJsonForm = ({
122153 return (
123154 < JsonEditor
124155 value = { JSON . stringify (
125- currentValue ?? generateDefaultValue ( propSchema ) ,
156+ currentValue ??
157+ generateDefaultValue ( propSchema , propertyName , parentSchema ) ,
126158 null ,
127159 2 ,
128160 ) }
@@ -140,6 +172,10 @@ const DynamicJsonForm = ({
140172 ) ;
141173 }
142174
175+ // Check if this property is required in the parent schema
176+ const isRequired =
177+ parentSchema ?. required ?. includes ( propertyName || "" ) ?? false ;
178+
143179 switch ( propSchema . type ) {
144180 case "string" :
145181 return (
@@ -148,16 +184,11 @@ const DynamicJsonForm = ({
148184 value = { ( currentValue as string ) ?? "" }
149185 onChange = { ( e ) => {
150186 const val = e . target . value ;
151- // Allow clearing non-required fields by setting undefined
152- // This preserves the distinction between empty string and unset
153- if ( ! val && ! propSchema . required ) {
154- handleFieldChange ( path , undefined ) ;
155- } else {
156- handleFieldChange ( path , val ) ;
157- }
187+ // Always allow setting string values, including empty strings
188+ handleFieldChange ( path , val ) ;
158189 } }
159190 placeholder = { propSchema . description }
160- required = { propSchema . required }
191+ required = { isRequired }
161192 />
162193 ) ;
163194 case "number" :
@@ -167,9 +198,7 @@ const DynamicJsonForm = ({
167198 value = { ( currentValue as number ) ?. toString ( ) ?? "" }
168199 onChange = { ( e ) => {
169200 const val = e . target . value ;
170- // Allow clearing non-required number fields
171- // This preserves the distinction between 0 and unset
172- if ( ! val && ! propSchema . required ) {
201+ if ( ! val && ! isRequired ) {
173202 handleFieldChange ( path , undefined ) ;
174203 } else {
175204 const num = Number ( val ) ;
@@ -179,7 +208,7 @@ const DynamicJsonForm = ({
179208 }
180209 } }
181210 placeholder = { propSchema . description }
182- required = { propSchema . required }
211+ required = { isRequired }
183212 />
184213 ) ;
185214 case "integer" :
@@ -190,9 +219,7 @@ const DynamicJsonForm = ({
190219 value = { ( currentValue as number ) ?. toString ( ) ?? "" }
191220 onChange = { ( e ) => {
192221 const val = e . target . value ;
193- // Allow clearing non-required integer fields
194- // This preserves the distinction between 0 and unset
195- if ( ! val && ! propSchema . required ) {
222+ if ( ! val && ! isRequired ) {
196223 handleFieldChange ( path , undefined ) ;
197224 } else {
198225 const num = Number ( val ) ;
@@ -203,7 +230,7 @@ const DynamicJsonForm = ({
203230 }
204231 } }
205232 placeholder = { propSchema . description }
206- required = { propSchema . required }
233+ required = { isRequired }
207234 />
208235 ) ;
209236 case "boolean" :
@@ -213,9 +240,135 @@ const DynamicJsonForm = ({
213240 checked = { ( currentValue as boolean ) ?? false }
214241 onChange = { ( e ) => handleFieldChange ( path , e . target . checked ) }
215242 className = "w-4 h-4"
216- required = { propSchema . required }
243+ required = { isRequired }
244+ />
245+ ) ;
246+ case "object" :
247+ if ( ! propSchema . properties ) {
248+ return (
249+ < JsonEditor
250+ value = { JSON . stringify ( currentValue ?? { } , null , 2 ) }
251+ onChange = { ( newValue ) => {
252+ try {
253+ const parsed = JSON . parse ( newValue ) ;
254+ handleFieldChange ( path , parsed ) ;
255+ setJsonError ( undefined ) ;
256+ } catch ( err ) {
257+ setJsonError (
258+ err instanceof Error ? err . message : "Invalid JSON" ,
259+ ) ;
260+ }
261+ } }
262+ error = { jsonError }
263+ />
264+ ) ;
265+ }
266+
267+ return (
268+ < div className = "space-y-2 border rounded p-3" >
269+ { Object . entries ( propSchema . properties ) . map ( ( [ key , subSchema ] ) => (
270+ < div key = { key } >
271+ < label className = "block text-sm font-medium mb-1" >
272+ { key }
273+ { propSchema . required ?. includes ( key ) && (
274+ < span className = "text-red-500 ml-1" > *</ span >
275+ ) }
276+ </ label >
277+ { renderFormFields (
278+ subSchema as JsonSchemaType ,
279+ ( currentValue as Record < string , JsonValue > ) ?. [ key ] ,
280+ [ ...path , key ] ,
281+ depth + 1 ,
282+ propSchema ,
283+ key ,
284+ ) }
285+ </ div >
286+ ) ) }
287+ </ div >
288+ ) ;
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" >
299+ { propSchema . description }
300+ </ p >
301+ ) }
302+
303+ { propSchema . items ?. description && (
304+ < p className = "text-sm text-gray-500" >
305+ Items: { propSchema . items . description }
306+ </ p >
307+ ) }
308+
309+ < div className = "space-y-2" >
310+ { arrayValue . map ( ( item , index ) => (
311+ < div key = { index } className = "flex items-center gap-2" >
312+ { renderFormFields (
313+ propSchema . items as JsonSchemaType ,
314+ item ,
315+ [ ...path , index . toString ( ) ] ,
316+ depth + 1 ,
317+ ) }
318+ < Button
319+ variant = "outline"
320+ size = "sm"
321+ onClick = { ( ) => {
322+ const newArray = [ ...arrayValue ] ;
323+ newArray . splice ( index , 1 ) ;
324+ handleFieldChange ( path , newArray ) ;
325+ } }
326+ >
327+ Remove
328+ </ Button >
329+ </ div >
330+ ) ) }
331+ < Button
332+ variant = "outline"
333+ size = "sm"
334+ onClick = { ( ) => {
335+ const defaultValue = getArrayItemDefault (
336+ propSchema . items as JsonSchemaType ,
337+ ) ;
338+ handleFieldChange ( path , [ ...arrayValue , defaultValue ] ) ;
339+ } }
340+ title = {
341+ propSchema . items ?. description
342+ ? `Add new ${ propSchema . items . description } `
343+ : "Add new item"
344+ }
345+ >
346+ Add Item
347+ </ Button >
348+ </ div >
349+ </ div >
350+ ) ;
351+ }
352+
353+ // For complex arrays, fall back to JSON editor
354+ return (
355+ < JsonEditor
356+ value = { JSON . stringify ( currentValue ?? [ ] , null , 2 ) }
357+ onChange = { ( newValue ) => {
358+ try {
359+ const parsed = JSON . parse ( newValue ) ;
360+ handleFieldChange ( path , parsed ) ;
361+ setJsonError ( undefined ) ;
362+ } catch ( err ) {
363+ setJsonError (
364+ err instanceof Error ? err . message : "Invalid JSON" ,
365+ ) ;
366+ }
367+ } }
368+ error = { jsonError }
217369 />
218370 ) ;
371+ }
219372 default :
220373 return null ;
221374 }
0 commit comments