Name untitled template nodes clearly

Why: Recursive template nodes without titles were rendered as repeated generic project objects in the audit tree.

Evidence: Focused presentation tests pass, including a regression for stable readable fallback names.

Impact: Template-derived paragraphs, tables, rows, cells, fields, and attachments remain distinguishable in the document tree.

Refs: rdm-two-trees-latest-templates
This commit is contained in:
2026-10-14 17:38:42 +00:00
parent b52f9654a0
commit c9b55eb502
3 changed files with 52 additions and 5 deletions
+13 -4
View File
@@ -6,7 +6,7 @@ import { api } from '../api'
import { Chip, Collapsible, EmptyState, SectionLabel } from '@ui'
import { classifyRdmError } from '../rdmErrors'
import {
describeAuditEvent, humanizeKind, humanizeRelation, humanizeState, isTreeItemExpandable,
describeAuditEvent, describeTreeItem, humanizeKind, humanizeRelation, humanizeState, isTreeItemExpandable,
type AuditEventViewInput,
} from '../rdmAuditPresentation'
import { store, useStore } from '../store'
@@ -19,7 +19,15 @@ interface Capabilities { actor: string; projects: InspectProject[]; crossProject
interface CoverageItem { face: string; reason: string }
interface ListShell<T = any> { items: T[]; nextCursor?: string; hasMore: boolean; truncated: boolean; coverage?: CoverageItem[] }
interface SummaryData { projectId: string; counts: Record<string, number>; watermark: number; lastOpAt: string; coverage?: CoverageItem[] }
interface TreeItem { kind: string; id: string; title: string; state?: string; hasChildren?: boolean }
interface TreeItem {
kind: string
id: string
title: string
state?: string
hasChildren?: boolean
order?: number
templateBlockId?: string
}
interface RelationEntry { layer: string; edgeId?: string; kind?: string; type?: string; state: string; depth?: number; from: any; to: any; conf?: number }
interface EventDTO extends AuditEventViewInput {}
interface OpGroup { operationId: string; actor: string; at: string; events: EventDTO[] }
@@ -381,12 +389,13 @@ function TreeNodeRow({ item, selectedId, children, onSelect }: {
}) {
const expandable = isTreeItemExpandable(item)
const nested = children[item.id]
const readable = describeTreeItem(item)
return (
<div>
<button className={`rdm-audit-tree-node${selectedId === item.id ? ' sel' : ''}`} onClick={() => onSelect(item)}>
{expandable && <span className="rdm-audit-tree-caret">{nested ? '▾' : '▸'}</span>}
<span className="rdm-audit-tree-kind">{humanizeKind(item.kind)}</span>
<span className="rdm-audit-tree-title">{item.title}</span>
<span className="rdm-audit-tree-kind">{readable.kind}</span>
<span className="rdm-audit-tree-title">{readable.title}</span>
{item.state && <Chip tone={item.state === 'baselined' ? 'ok' : item.state === 'candidate' ? 'warn' : 'plain'}>{humanizeState(item.state)}</Chip>}
</button>
{nested && (
+15 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { describeAuditEvent, humanizeKind, humanizeRelation, humanizeState, isTreeItemExpandable } from './rdmAuditPresentation'
import { describeAuditEvent, describeTreeItem, humanizeKind, humanizeRelation, humanizeState, isTreeItemExpandable } from './rdmAuditPresentation'
describe('RDM audit presentation', () => {
it('turns a structured document event into a readable action and impact', () => {
@@ -60,4 +60,18 @@ describe('RDM audit presentation', () => {
expect(isTreeItemExpandable({ kind: 'section', id: 'sec-1', title: '章节', hasChildren: true })).toBe(true)
expect(isTreeItemExpandable({ kind: 'paragraph', id: 'p-1', title: '段落', hasChildren: false })).toBe(false)
})
it('gives untitled template nodes a distinct readable name', () => {
expect(describeTreeItem({
kind: 'paragraph',
id: 'dn-1',
title: '',
order: 6,
templateBlockId: 'dhf.dx.a7850d1881.3.placeholder',
})).toEqual({ kind: '段落', title: '模板内容 7(3)' })
expect(describeTreeItem({ kind: 'table', id: 'dn-2', title: '', order: 2 })).toEqual({
kind: '表格',
title: '模板内容 3',
})
})
})
+24
View File
@@ -26,6 +26,8 @@ interface AuditEventPresentation {
const KIND_LABELS: Record<string, string> = {
doc: '文档', section: '章节', node: '节点', note: '知识', slice: '资料切片',
paragraph: '段落', table: '表格', row: '表格行', cell: '表格单元格',
field: '填写字段', attachment: '附件',
requirement: '要求', risk: '风险', proposal: '变更提案', relation: '追溯关系',
template: '模板', project: '项目', conclusion: '结论', issue: '问题',
}
@@ -71,6 +73,28 @@ export function humanizeKind(kind: string): string {
return KIND_LABELS[kind] || '项目对象'
}
export function describeTreeItem(item: {
kind: string
id: string
title?: string
order?: number
templateBlockId?: string
}): { kind: string; title: string } {
const title = item.title?.trim()
if (title) return { kind: humanizeKind(item.kind), title }
const position = typeof item.order === 'number' ? item.order + 1 : undefined
const parts = (item.templateBlockId || '').split('.').filter(Boolean)
const semantic = new Set(['placeholder', 'example', 'fixed'])
while (parts.length > 0 && semantic.has(parts[parts.length - 1])) parts.pop()
const hint = parts.at(-1)
const numbered = position ? `模板内容 ${position}` : '未命名模板内容'
return {
kind: humanizeKind(item.kind),
title: hint ? `${numbered}(${hint})` : numbered,
}
}
export function humanizeState(state: string): string {
return STATE_LABELS[state] || '状态已记录'
}