diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 685401ab..af5f4def 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -76,7 +76,38 @@ const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => { const DynamicJsonForm = forwardRef( ({ schema, value, onChange, maxDepth = 3 }, ref) => { - const isOnlyJSON = !isSimpleObject(schema); + // Determine if we can render a form at the top level. + // This is more permissive than isSimpleObject(): + // - Objects with any properties are form-capable (individual complex fields may still fallback to JSON) + // - Arrays with defined items are form-capable + // - Primitive types are form-capable + const canRenderTopLevelForm = (s: JsonSchemaType): boolean => { + const primitiveTypes = ["string", "number", "integer", "boolean", "null"]; + + const hasType = Array.isArray(s.type) ? s.type.length > 0 : !!s.type; + if (!hasType) return false; + + const includesType = (t: string) => + Array.isArray(s.type) ? s.type.includes(t as any) : s.type === t; + + // Primitive at top-level + if (primitiveTypes.some(includesType)) return true; + + // Object with properties + if (includesType("object")) { + const keys = Object.keys(s.properties ?? {}); + return keys.length > 0; + } + + // Array with items + if (includesType("array")) { + return !!s.items; + } + + return false; + }; + + const isOnlyJSON = !canRenderTopLevelForm(schema); const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [jsonError, setJsonError] = useState(); const [copiedJson, setCopiedJson] = useState(false); @@ -267,63 +298,76 @@ const DynamicJsonForm = forwardRef( switch (fieldType) { case "string": { - if ( - propSchema.oneOf && - propSchema.oneOf.every( - (option) => - typeof option.const === "string" && - typeof option.title === "string", - ) - ) { + // Titled single-select using oneOf/anyOf with const/title pairs + const titledOptions = (propSchema.oneOf ?? propSchema.anyOf)?.filter( + (opt) => (opt as any).const !== undefined, + ) as { const: string; title?: string }[] | undefined; + + if (titledOptions && titledOptions.length > 0) { return ( - +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} + +
); } + // Untitled single-select using enum (with optional legacy enumNames for labels) if (propSchema.enum) { + const names = Array.isArray((propSchema as any).enumNames) + ? (propSchema as any).enumNames + : undefined; return ( - +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} + +
); } @@ -413,13 +457,20 @@ const DynamicJsonForm = forwardRef( case "boolean": return ( - handleFieldChange(path, e.target.checked)} - className="w-4 h-4" - required={isRequired} - /> +
+ {propSchema.description && ( +

+ {propSchema.description} +

+ )} + handleFieldChange(path, e.target.checked)} + className="w-4 h-4" + required={isRequired} + /> +
); case "null": return null; @@ -449,7 +500,7 @@ const DynamicJsonForm = forwardRef( {Object.entries(propSchema.properties).map(([key, subSchema]) => (