Skip to content

Commit e6166a7

Browse files
committed
feat: add new schema requirements to DynamicJsonForm
1 parent e163aea commit e6166a7

File tree

2 files changed

+523
-22
lines changed

2 files changed

+523
-22
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 139 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -140,36 +140,144 @@ const DynamicJsonForm = ({
140140
);
141141
}
142142

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+
) => {
143206
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+
145252
return (
146253
<Input
147-
type="text"
254+
type={inputType}
148255
value={(currentValue as string) ?? ""}
149256
onChange={(e) => {
150257
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) {
154259
handleFieldChange(path, undefined);
155260
} else {
156261
handleFieldChange(path, val);
157262
}
158263
}}
159264
placeholder={propSchema.description}
160-
required={propSchema.required}
265+
required={fieldRequired}
266+
minLength={propSchema.minLength}
267+
maxLength={propSchema.maxLength}
268+
pattern={propSchema.pattern}
161269
/>
162270
);
271+
}
272+
163273
case "number":
164274
return (
165275
<Input
166276
type="number"
167277
value={(currentValue as number)?.toString() ?? ""}
168278
onChange={(e) => {
169279
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) {
173281
handleFieldChange(path, undefined);
174282
} else {
175283
const num = Number(val);
@@ -179,9 +287,12 @@ const DynamicJsonForm = ({
179287
}
180288
}}
181289
placeholder={propSchema.description}
182-
required={propSchema.required}
290+
required={fieldRequired}
291+
min={propSchema.minimum}
292+
max={propSchema.maximum}
183293
/>
184294
);
295+
185296
case "integer":
186297
return (
187298
<Input
@@ -190,32 +301,38 @@ const DynamicJsonForm = ({
190301
value={(currentValue as number)?.toString() ?? ""}
191302
onChange={(e) => {
192303
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) {
196305
handleFieldChange(path, undefined);
197306
} else {
198307
const num = Number(val);
199-
// Only update if it's a valid integer
200308
if (!isNaN(num) && Number.isInteger(num)) {
201309
handleFieldChange(path, num);
202310
}
203311
}
204312
}}
205313
placeholder={propSchema.description}
206-
required={propSchema.required}
314+
required={fieldRequired}
315+
min={propSchema.minimum}
316+
max={propSchema.maximum}
207317
/>
208318
);
319+
209320
case "boolean":
210321
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>
218334
);
335+
219336
default:
220337
return null;
221338
}

0 commit comments

Comments
 (0)