From 15d8a4093b708fde9dd97029523fb5c579337d3a Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 11 Oct 2026 17:56:39 +0000 Subject: [PATCH] Expand the hierarchical docs tree and aggregate sections for tracing Why: rdm.inspect.tree docs is two-level (parent= returns sections), but the U4 workspace only fetched level one - the tree showed bare documents, clicking a doc hit unsupported inspect.object kind, and the trace tab always showed no sections. Evidence: tsc + design-guard green; scripts/e2e.sh full run in this task. Impact: tree browsing expands documents into sections; trace entry lists sections aggregated per document. Refs: U4/U3 read-service contract; task feature/rdm-e2e-acceptance. --- e2e/rdm-audit.playwright.ts | 7 ++-- src/components/RdmAuditWorkspace.tsx | 60 +++++++++++++++++++++++----- src/styles.css | 2 + 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/e2e/rdm-audit.playwright.ts b/e2e/rdm-audit.playwright.ts index b39f66b..36bc1dd 100644 --- a/e2e/rdm-audit.playwright.ts +++ b/e2e/rdm-audit.playwright.ts @@ -66,10 +66,11 @@ test.describe.serial('U4 项目追溯审计工作区', () => { test('三次主要点击内完成取证:章节来源依据与最近修改原因', async ({ page }) => { await login(page) await openAudit(page) - // 路径①:资料与文档(点击1)→ 选章节(点击2)→ 对象详情含最近事件(修改原因)。 - // 树首个节点是文档级(无事件史),取证对象选 section。 + // 路径①:资料与文档 → 展开文档(点击1)→ 选章节(点击2)→ 详情含最近事件(修改原因)。 + // docs 树是层级结构:第一层是文档,章节在展开后的第二层。 await page.getByRole('tab', { name: '资料与文档' }).click() - const sectionNode = page.locator('.rdm-audit-tree-node') + await page.locator('.rdm-audit-tree-node').first().click() + const sectionNode = page.locator('.rdm-audit-tree-children .rdm-audit-tree-node') .filter({ has: page.locator('.rdm-audit-tree-kind', { hasText: 'section' }) }).first() await expect(sectionNode).toBeVisible() await sectionNode.click() diff --git a/src/components/RdmAuditWorkspace.tsx b/src/components/RdmAuditWorkspace.tsx index 83605e0..07aaed3 100644 --- a/src/components/RdmAuditWorkspace.tsx +++ b/src/components/RdmAuditWorkspace.tsx @@ -144,11 +144,14 @@ function TreeList({ pid, kind }: { pid: string; kind: 'notes' | 'docs' }) { const [err, setErr] = useState('') const [selected, setSelected] = useState(null) const [detail, setDetail] = useState(null) + // docs 树是层级结构(rdm.inspect.tree parent= 才返回章节):文档节点 + // 点击展开子章节,不直接查对象详情(doc 不在 inspect.object 支持面)。 + const [children, setChildren] = useState>({}) const genRef = useRef(0) useEffect(() => { const gen = ++genRef.current - setShell(null); setErr(''); setSelected(null); setDetail(null) + setShell(null); setErr(''); setSelected(null); setDetail(null); setChildren({}) api.rdmCommand>('rdm.inspect.tree', { projectId: pid, kind, cursor: '' }) .then(s => { if (gen === genRef.current) setShell(s) }) .catch(e => { if (gen === genRef.current) setErr(e.message) }) @@ -162,7 +165,19 @@ function TreeList({ pid, kind }: { pid: string; kind: 'notes' | 'docs' }) { .catch(e => { if (gen === genRef.current) setErr(e.message) }) } + const toggleDoc = (item: TreeItem) => { + if (children[item.id]) { + setChildren(prev => { const next = { ...prev }; delete next[item.id]; return next }) + return + } + const gen = genRef.current + api.rdmCommand>('rdm.inspect.tree', { projectId: pid, kind: 'docs', parent: item.id, cursor: '' }) + .then(s => { if (gen === genRef.current) setChildren(prev => ({ ...prev, [item.id]: s.items })) }) + .catch(e => { if (gen === genRef.current) setErr(e.message) }) + } + const selectNode = (item: TreeItem) => { + if (item.kind === 'doc') { toggleDoc(item); return } setSelected(item); setDetail(null) const gen = genRef.current api.rdmCommand('rdm.inspect.object', { projectId: pid, kind: item.kind, id: item.id }) @@ -178,12 +193,28 @@ function TreeList({ pid, kind }: { pid: string; kind: 'notes' | 'docs' }) {
{shell.items.map(item => ( - +
+ + {item.kind === 'doc' && children[item.id] && ( +
+ {children[item.id].length === 0 &&
无章节
} + {children[item.id].map(sub => ( + + ))} +
+ )} +
))} {shell.hasMore && } {shell.truncated &&
已截断(深度或数量上限)
} @@ -216,14 +247,21 @@ function TraceTab({ pid }: { pid: string }) { .catch(e => { if (gen === genRef.current) setErr(e.message) }) }, [pid, sel]) - // 未选对象时列出章节供选择 + // 未选对象时列出章节供选择。docs 树是层级结构:章节挂在各文档下 + // (parent=),按文档聚合后合并展示。 const [sections, setSections] = useState([]) useEffect(() => { if (sel) return let c = false - api.rdmCommand>('rdm.inspect.tree', { projectId: pid, kind: 'docs' }) - .then(s => { if (!c) setSections(s.items.filter(i => i.kind === 'section')) }) - .catch(() => {}) + ;(async () => { + const top = await api.rdmCommand>('rdm.inspect.tree', { projectId: pid, kind: 'docs' }) + const secs: TreeItem[] = [] + for (const d of top.items.filter(i => i.kind === 'doc')) { + const sub = await api.rdmCommand>('rdm.inspect.tree', { projectId: pid, kind: 'docs', parent: d.id }) + secs.push(...sub.items.filter(i => i.kind === 'section')) + } + if (!c) setSections(secs) + })().catch(() => {}) return () => { c = true } }, [pid, sel]) diff --git a/src/styles.css b/src/styles.css index b1ea5bc..a9a9c54 100644 --- a/src/styles.css +++ b/src/styles.css @@ -596,6 +596,8 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .rdm-audit-tree-node { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: none; border: none; border-radius: var(--radius-sm); cursor: pointer; text-align: left; font-size: var(--fs-ui-sm); color: var(--text-dim); } .rdm-audit-tree-node:hover { background: var(--bg-hover); color: var(--text); } .rdm-audit-tree-node.sel { background: var(--bg-active); color: var(--text); } +.rdm-audit-tree-caret { width: 14px; flex: none; color: var(--text-dim); font-size: var(--fs-ui-sm); } +.rdm-audit-tree-children { padding-left: 18px; display: flex; flex-direction: column; gap: 0; } .rdm-audit-tree-kind { font-size: var(--fs-ui-xs); color: var(--text-faint); min-width: 3.2em; } .rdm-audit-tree-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rdm-audit-more { margin-top: 8px; }