From d037eca4ae5744d7b16f0f5c21247a35b3ce2a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Thu, 14 May 2026 18:19:01 +0800 Subject: [PATCH] feat(dashboard): recursive schema rendering with nested object, array, oneOf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nested object: expand properties with └─ indentation - object[]: show type as 'object[]', expand items.properties - string[]/number[]: show type directly, no expansion - oneOf/discriminatedUnion: variant headers with ├/└ connectors - Auto-detect discriminator field (const values) - Skip discriminator in variant field list - Recursive to arbitrary depth Phase 1+2 of #258 --- .../src/components/workflow-list.tsx | 156 ++++++++++++++++-- 1 file changed, 141 insertions(+), 15 deletions(-) diff --git a/packages/workflow-dashboard/src/components/workflow-list.tsx b/packages/workflow-dashboard/src/components/workflow-list.tsx index d6325fd..10ef1a7 100644 --- a/packages/workflow-dashboard/src/components/workflow-list.tsx +++ b/packages/workflow-dashboard/src/components/workflow-list.tsx @@ -17,21 +17,134 @@ function versionCount(detail: WorkflowDetail): number { return detail.history.length + 1; } -function schemaPropertiesTable(schema: Record): Array<{ +type SchemaRow = { + key: string; name: string; type: string; description: string; -}> { + depth: number; + prefix: string; + isVariantHeader: boolean; +}; + +function resolveType(prop: Record): string { + if (prop.type === "array") { + const items = prop.items as Record | undefined; + if (items !== undefined) { + const itemType = String(items.type ?? "unknown"); + return `${itemType}[]`; + } + return "array"; + } + return String(prop.type ?? "unknown"); +} + +function flattenSchema( + schema: Record, + depth: number, + parentPrefix: string, + keyPrefix: string, + parentRequired: Set, +): SchemaRow[] { + const rows: SchemaRow[] = []; + + // Handle oneOf / discriminatedUnion + const oneOf = schema.oneOf as Array> | undefined; + if (Array.isArray(oneOf) && oneOf.length > 0) { + for (let vi = 0; vi < oneOf.length; vi++) { + const variant = oneOf[vi]; + // Try to find a distinguishing literal (e.g. status: "approved") + const variantProps = (variant.properties ?? {}) as Record>; + let variantLabel = `Variant ${vi + 1}`; + for (const [pName, pDef] of Object.entries(variantProps)) { + if (pDef.const !== undefined) { + variantLabel = `${pName}: ${String(pDef.const)}`; + break; + } + } + const isLast = vi === oneOf.length - 1; + const connector = isLast ? "└" : "├"; + rows.push({ + key: `${keyPrefix}variant-${vi}`, + name: `${parentPrefix}${connector} ${variantLabel}`, + type: "", + description: "", + depth, + prefix: parentPrefix, + isVariantHeader: true, + }); + const childPrefix = `${parentPrefix}${isLast ? " " : "│ "}`; + const variantRequired = new Set( + Array.isArray(variant.required) ? (variant.required as string[]) : [], + ); + for (const [pName, pDef] of Object.entries(variantProps)) { + if (pDef.const !== undefined) continue; // skip discriminator field + const subRows = flattenProperty(pName, pDef, depth + 1, childPrefix, `${keyPrefix}v${vi}-`, variantRequired); + rows.push(...subRows); + } + } + return rows; + } + + // Handle regular object with properties const props = (schema.properties ?? {}) as Record>; const required = new Set( Array.isArray(schema.required) ? (schema.required as string[]) : [], ); - return Object.entries(props).map(([name, prop]) => { - let type = String(prop.type ?? "unknown"); - if (!required.has(name)) type += "?"; - const description = String(prop.description ?? ""); - return { name, type, description }; + for (const [name, prop] of Object.entries(props)) { + const subRows = flattenProperty(name, prop, depth, parentPrefix, keyPrefix, required); + rows.push(...subRows); + } + return rows; +} + +function flattenProperty( + name: string, + prop: Record, + depth: number, + parentPrefix: string, + keyPrefix: string, + required: Set, +): SchemaRow[] { + const rows: SchemaRow[] = []; + const hasOneOf = Array.isArray(prop.oneOf) && (prop.oneOf as unknown[]).length > 0; + let type = hasOneOf ? "⊕ oneOf" : resolveType(prop); + if (!required.has(name)) type += "?"; + const description = String(prop.description ?? ""); + const displayName = depth > 0 ? `${parentPrefix}└─ ${name}` : name; + + rows.push({ + key: `${keyPrefix}${name}`, + name: displayName, + type, + description, + depth, + prefix: parentPrefix, + isVariantHeader: false, }); + + // Recurse into nested object + if (prop.type === "object" && prop.properties !== undefined) { + const childPrefix = depth > 0 ? `${parentPrefix} ` : " "; + rows.push(...flattenSchema(prop as Record, depth + 1, childPrefix, `${keyPrefix}${name}-`, required)); + } + + // Recurse into array of objects + if (prop.type === "array") { + const items = prop.items as Record | undefined; + if (items !== undefined && items.type === "object" && items.properties !== undefined) { + const childPrefix = depth > 0 ? `${parentPrefix} ` : " "; + rows.push(...flattenSchema(items, depth + 1, childPrefix, `${keyPrefix}${name}-`, new Set())); + } + } + + // Recurse into oneOf + if (hasOneOf) { + const childPrefix = depth > 0 ? `${parentPrefix} ` : " "; + rows.push(...flattenSchema(prop as Record, depth + 1, childPrefix, `${keyPrefix}${name}-`, required)); + } + + return rows; } function RoleCard({ @@ -41,7 +154,7 @@ function RoleCard({ roleName: string; role: WorkflowRoleDescriptor; }) { - const fields = schemaPropertiesTable(role.schema); + const rows = flattenSchema(role.schema, 0, "", `${roleName}-`, new Set()); return (
)} - {fields.length > 0 && ( + {rows.length > 0 && (

- {fields.map((f) => ( - - {f.name} - {f.type} - {f.description || "—"} + {rows.map((r) => ( + + + {r.name} + + {r.type} + {r.description || (r.isVariantHeader ? "" : "—")} ))}

)} - {fields.length === 0 && Object.keys(role.schema).length > 0 && ( + {rows.length === 0 && Object.keys(role.schema).length > 0 && (