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:
@@ -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 && (
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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] || '状态已记录'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user