@@ -140,36 +140,144 @@ const DynamicJsonForm = ({
140
140
) ;
141
141
}
142
142
143
+ const isFieldRequired = ( fieldPath : string [ ] ) : boolean => {
144
+ if ( typeof schema . required === "boolean" ) {
145
+ return schema . required ;
146
+ }
147
+ if ( Array . isArray ( schema . required ) && fieldPath . length > 0 ) {
148
+ return schema . required . includes ( fieldPath [ fieldPath . length - 1 ] ) ;
149
+ }
150
+ return false ;
151
+ } ;
152
+
153
+ if ( propSchema . type === "object" && propSchema . properties ) {
154
+ const objectValue = ( currentValue as Record < string , JsonValue > ) || { } ;
155
+
156
+ return (
157
+ < div className = "space-y-4" >
158
+ { Object . entries ( propSchema . properties ) . map (
159
+ ( [ fieldName , fieldSchema ] ) => {
160
+ const fieldPath = [ ...path , fieldName ] ;
161
+ const fieldValue = objectValue [ fieldName ] ;
162
+ const fieldRequired = isFieldRequired ( [ fieldName ] ) ;
163
+
164
+ return (
165
+ < div key = { fieldName } className = "space-y-2" >
166
+ < label
167
+ htmlFor = { fieldName }
168
+ className = "block text-sm font-medium"
169
+ >
170
+ { fieldSchema . title || fieldName }
171
+ { fieldRequired && (
172
+ < span className = "text-red-500 ml-1" > *</ span >
173
+ ) }
174
+ </ label >
175
+ { fieldSchema . description && (
176
+ < p className = "text-xs text-gray-500" >
177
+ { fieldSchema . description }
178
+ </ p >
179
+ ) }
180
+ < div >
181
+ { renderFieldInput (
182
+ fieldSchema ,
183
+ fieldValue ,
184
+ fieldPath ,
185
+ fieldRequired ,
186
+ ) }
187
+ </ div >
188
+ </ div >
189
+ ) ;
190
+ } ,
191
+ ) }
192
+ </ div >
193
+ ) ;
194
+ }
195
+
196
+ const fieldRequired = isFieldRequired ( path ) ;
197
+ return renderFieldInput ( propSchema , currentValue , path , fieldRequired ) ;
198
+ } ;
199
+
200
+ const renderFieldInput = (
201
+ propSchema : JsonSchemaType ,
202
+ currentValue : JsonValue ,
203
+ path : string [ ] ,
204
+ fieldRequired : boolean ,
205
+ ) => {
143
206
switch ( propSchema . type ) {
144
- case "string" :
207
+ case "string" : {
208
+ if ( propSchema . enum ) {
209
+ return (
210
+ < select
211
+ value = { ( currentValue as string ) ?? "" }
212
+ onChange = { ( e ) => {
213
+ const val = e . target . value ;
214
+ if ( ! val && ! fieldRequired ) {
215
+ handleFieldChange ( path , undefined ) ;
216
+ } else {
217
+ handleFieldChange ( path , val ) ;
218
+ }
219
+ } }
220
+ required = { fieldRequired }
221
+ 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"
222
+ >
223
+ < option value = "" > Select an option...</ option >
224
+ { propSchema . enum . map ( ( option , index ) => (
225
+ < option key = { option } value = { option } >
226
+ { propSchema . enumNames ?. [ index ] || option }
227
+ </ option >
228
+ ) ) }
229
+ </ select >
230
+ ) ;
231
+ }
232
+
233
+ let inputType = "text" ;
234
+ switch ( propSchema . format ) {
235
+ case "email" :
236
+ inputType = "email" ;
237
+ break ;
238
+ case "uri" :
239
+ inputType = "url" ;
240
+ break ;
241
+ case "date" :
242
+ inputType = "date" ;
243
+ break ;
244
+ case "date-time" :
245
+ inputType = "datetime-local" ;
246
+ break ;
247
+ default :
248
+ inputType = "text" ;
249
+ break ;
250
+ }
251
+
145
252
return (
146
253
< Input
147
- type = "text"
254
+ type = { inputType }
148
255
value = { ( currentValue as string ) ?? "" }
149
256
onChange = { ( e ) => {
150
257
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 ) {
258
+ if ( ! val && ! fieldRequired ) {
154
259
handleFieldChange ( path , undefined ) ;
155
260
} else {
156
261
handleFieldChange ( path , val ) ;
157
262
}
158
263
} }
159
264
placeholder = { propSchema . description }
160
- required = { propSchema . required }
265
+ required = { fieldRequired }
266
+ minLength = { propSchema . minLength }
267
+ maxLength = { propSchema . maxLength }
268
+ pattern = { propSchema . pattern }
161
269
/>
162
270
) ;
271
+ }
272
+
163
273
case "number" :
164
274
return (
165
275
< Input
166
276
type = "number"
167
277
value = { ( currentValue as number ) ?. toString ( ) ?? "" }
168
278
onChange = { ( e ) => {
169
279
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 ) {
280
+ if ( ! val && ! fieldRequired ) {
173
281
handleFieldChange ( path , undefined ) ;
174
282
} else {
175
283
const num = Number ( val ) ;
@@ -179,9 +287,12 @@ const DynamicJsonForm = ({
179
287
}
180
288
} }
181
289
placeholder = { propSchema . description }
182
- required = { propSchema . required }
290
+ required = { fieldRequired }
291
+ min = { propSchema . minimum }
292
+ max = { propSchema . maximum }
183
293
/>
184
294
) ;
295
+
185
296
case "integer" :
186
297
return (
187
298
< Input
@@ -190,32 +301,38 @@ const DynamicJsonForm = ({
190
301
value = { ( currentValue as number ) ?. toString ( ) ?? "" }
191
302
onChange = { ( e ) => {
192
303
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 ) {
304
+ if ( ! val && ! fieldRequired ) {
196
305
handleFieldChange ( path , undefined ) ;
197
306
} else {
198
307
const num = Number ( val ) ;
199
- // Only update if it's a valid integer
200
308
if ( ! isNaN ( num ) && Number . isInteger ( num ) ) {
201
309
handleFieldChange ( path , num ) ;
202
310
}
203
311
}
204
312
} }
205
313
placeholder = { propSchema . description }
206
- required = { propSchema . required }
314
+ required = { fieldRequired }
315
+ min = { propSchema . minimum }
316
+ max = { propSchema . maximum }
207
317
/>
208
318
) ;
319
+
209
320
case "boolean" :
210
321
return (
211
- < Input
212
- type = "checkbox"
213
- checked = { ( currentValue as boolean ) ?? false }
214
- onChange = { ( e ) => handleFieldChange ( path , e . target . checked ) }
215
- className = "w-4 h-4"
216
- required = { propSchema . required }
217
- />
322
+ < div className = "flex items-center space-x-2" >
323
+ < Input
324
+ type = "checkbox"
325
+ checked = { ( currentValue as boolean ) ?? false }
326
+ onChange = { ( e ) => handleFieldChange ( path , e . target . checked ) }
327
+ className = "w-4 h-4"
328
+ required = { fieldRequired }
329
+ />
330
+ < span className = "text-sm" >
331
+ { propSchema . description || "Enable this option" }
332
+ </ span >
333
+ </ div >
218
334
) ;
335
+
219
336
default :
220
337
return null ;
221
338
}
0 commit comments