From c9b55eb502b47beaea2183bc1f15f2c2916ab7fb Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 14 Oct 2026 17:38:42 +0000 Subject: [PATCH] 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 --- src/components/RdmAuditWorkspace.tsx | 17 +++++++++++++---- src/rdmAuditPresentation.test.ts | 16 +++++++++++++++- src/rdmAuditPresentation.ts | 24 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/components/RdmAuditWorkspace.tsx b/src/components/RdmAuditWorkspace.tsx index b446b7d..44860f5 100644 --- a/src/components/RdmAuditWorkspace.tsx +++ b/src/components/RdmAuditWorkspace.tsx @@ -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 { items: T[]; nextCursor?: string; hasMore: boolean; truncated: boolean; coverage?: CoverageItem[] } interface SummaryData { projectId: string; counts: Record; 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 (
{nested && ( diff --git a/src/rdmAuditPresentation.test.ts b/src/rdmAuditPresentation.test.ts index 8616370..0b7ac6a 100644 --- a/src/rdmAuditPresentation.test.ts +++ b/src/rdmAuditPresentation.test.ts @@ -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', + }) + }) }) diff --git a/src/rdmAuditPresentation.ts b/src/rdmAuditPresentation.ts index 2618676..2c2093e 100644 --- a/src/rdmAuditPresentation.ts +++ b/src/rdmAuditPresentation.ts @@ -26,6 +26,8 @@ interface AuditEventPresentation { const KIND_LABELS: Record = { 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] || '状态已记录' }