From 91de320b670ece4767847d3c9cabfdf71c4f0c7b Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 13 Sep 2026 11:12:03 +0000 Subject: [PATCH] fix(web): mount revision toolbar for registered uploads only Keep template/legacy gates; allow doc-free uploaded_file toolbars while blocking unregistered doc-upload orphans. Co-authored-by: Cursor --- .../revisionToolbarMount.test.ts | 70 ++++++++----------- .../documentRevision/revisionToolbarMount.ts | 57 +++++++-------- 2 files changed, 55 insertions(+), 72 deletions(-) diff --git a/web/src/features/documentRevision/revisionToolbarMount.test.ts b/web/src/features/documentRevision/revisionToolbarMount.test.ts index 9dbd472d..44b6cda3 100644 --- a/web/src/features/documentRevision/revisionToolbarMount.test.ts +++ b/web/src/features/documentRevision/revisionToolbarMount.test.ts @@ -13,50 +13,19 @@ describe('revisionToolbarMount', () => { assert.equal(TEMPLATE_REVISION_PREVIEW_HINT, '模板预览:首轮编制后启用版本账本'); }); - it('template-only slots do not mount toolbar and show template hint', () => { + it('template doc-p + isTemplateOnly=true does not mount', () => { const doc = { docId: 'doc-p6059-intake', isTemplateOnly: true as const }; assert.equal(shouldMountDocumentRevisionToolbar(doc), false); assert.equal(revisionToolbarMountMode(doc), 'template_hint'); assert.equal(documentRevisionInactiveHint(doc), TEMPLATE_REVISION_PREVIEW_HINT); }); - it('legacy local mock docs do not mount toolbar', () => { - for (const docId of ['doc-001', 'doc-002', 'doc-shared-001', 'doc-private-001', 'doc-cusa-intake']) { - assert.equal(shouldMountDocumentRevisionToolbar({ docId, isTemplateOnly: false }), false, docId); - assert.equal(revisionToolbarMountMode({ docId, isTemplateOnly: false }), 'local_hint', docId); - assert.equal( - documentRevisionInactiveHint({ docId, isTemplateOnly: false }), - LOCAL_REVISION_PREVIEW_HINT, - docId, - ); - } + it('legacy doc-001 does not mount', () => { + assert.equal(shouldMountDocumentRevisionToolbar({ docId: 'doc-001', isTemplateOnly: false }), false); + assert.equal(documentRevisionInactiveHint({ docId: 'doc-001', isTemplateOnly: false }), LOCAL_REVISION_PREVIEW_HINT); }); - it('generated controlled doc-p / doc_proj mount toolbar when not template-only', () => { - assert.equal( - shouldMountDocumentRevisionToolbar({ docId: 'doc-p6059-intake', isTemplateOnly: false }), - true, - ); - assert.equal( - shouldMountDocumentRevisionToolbar({ docId: 'doc_proj6059_intake', isTemplateOnly: false }), - true, - ); - assert.equal( - revisionToolbarMountMode({ docId: 'doc_proj56_srs', isTemplateOnly: false }), - 'toolbar', - ); - assert.equal(documentRevisionInactiveHint({ docId: 'doc_proj56_srs', isTemplateOnly: false }), null); - }); - - it('server freeform mounts toolbar; IndexedDB upload orphan does not', () => { - assert.equal( - shouldMountDocumentRevisionToolbar({ - docId: 'doc-free-1710000000000-2', - isTemplateOnly: false, - governanceMode: 'freeform', - }), - true, - ); + it('unregistered doc-upload orphan does not mount (must register first)', () => { assert.equal( shouldMountDocumentRevisionToolbar({ docId: 'doc-upload-1789263145197-0', @@ -66,13 +35,36 @@ describe('revisionToolbarMount', () => { }), false, ); + }); + + it('server-registered upload (doc-free) and freeform mount', () => { assert.equal( - revisionToolbarMountMode({ - docId: 'doc-upload-1789263145197-0', + shouldMountDocumentRevisionToolbar({ + docId: 'doc-free-1789263145197-2', isTemplateOnly: false, sourceType: 'uploaded_file', + governanceMode: 'freeform', }), - 'local_hint', + true, + ); + assert.equal( + shouldMountDocumentRevisionToolbar({ + docId: 'doc-free-1710000000000-2', + isTemplateOnly: false, + governanceMode: 'freeform', + }), + true, + ); + }); + + it('canonical doc_proj and generated non-template doc-p mount', () => { + assert.equal( + shouldMountDocumentRevisionToolbar({ docId: 'doc_proj6059_intake', isTemplateOnly: false }), + true, + ); + assert.equal( + shouldMountDocumentRevisionToolbar({ docId: 'doc-p6059-intake', isTemplateOnly: false }), + true, ); }); }); diff --git a/web/src/features/documentRevision/revisionToolbarMount.ts b/web/src/features/documentRevision/revisionToolbarMount.ts index 81b82b60..920cfd6a 100644 --- a/web/src/features/documentRevision/revisionToolbarMount.ts +++ b/web/src/features/documentRevision/revisionToolbarMount.ts @@ -1,11 +1,9 @@ /** * App 是否挂载 DocumentRevisionToolbar(会请求 revision-state)的可测判断。 * - * 根因:对 isTemplateOnly 新项目模板与 legacy local mock(如 doc-001)无条件挂工具条, - * 导致「版本账本未就绪」假错误。模板预览只显示非错误提示,不发 revision-state。 - * - * UI-16:IndexedDB 孤儿上传(doc-upload-*)不得挂工具条冒充 V1。 - * 受控生成后(isTemplateOnly=false 的 doc-p / doc_proj)与服务端 freeform 正常挂载。 + * - 模板预览 / legacy seed:不挂载、不请求,显示非错误提示 + * - 服务端 freeform / 已注册上传(doc-free-* + freeform 事实字段)/ 生成后受控:挂载 + * - 未注册的 doc-upload-* orphan:不挂载(上传路径必须先 createFreeform 再替换 ID) */ import type { DeliverableItem } from '../../types/workbench'; @@ -23,7 +21,6 @@ export type RevisionMountDoc = Pick export type RevisionToolbarMountMode = 'toolbar' | 'template_hint' | 'local_hint' | 'none'; -/** 前端种子/演示交付物 ID(无服务端账本)。 */ export function isLegacyLocalMockDocId(docId: string): boolean { const id = (docId || '').trim(); if (!id) return true; @@ -34,50 +31,45 @@ export function isLegacyLocalMockDocId(docId: string): boolean { return false; } -/** 仅 IndexedDB、从未服务端注册的上传预览(UI-16)。 */ -export function isIndexedDbUploadOrphanDocId(docId: string): boolean { +/** 未走服务端注册的本地上传 ID(禁止挂工具条冒充账本)。 */ +export function isUnregisteredUploadOrphanDocId(docId: string): boolean { return /^doc-upload-/.test((docId || '').trim()); } -/** 服务端受控 / freeform 事实源 ID。 */ -export function isServerBackedRevisionDocId(docId: string): boolean { - const id = (docId || '').trim(); - if (!id || isLegacyLocalMockDocId(id) || isIndexedDbUploadOrphanDocId(id)) return false; - if (/^doc_proj\d+_/.test(id)) return true; - if (/^doc-free-\d+/.test(id)) return true; +export function hasServerRevisionFactFields(doc: RevisionMountDoc): boolean { + if (doc.sourceType === 'uploaded_file' && !isUnregisteredUploadOrphanDocId(doc.docId)) return true; + if (doc.governanceMode === 'freeform' && !isUnregisteredUploadOrphanDocId(doc.docId)) return true; + if (doc.governanceMode === 'controlled') return true; + if ( + (doc.space === 'my_private' || doc.space === 'project_shared') && + !isUnregisteredUploadOrphanDocId(doc.docId) + ) { + return true; + } return false; } -/** - * 生成后的受控槽位(isTemplateOnly 已清)可用 doc-p / doc_proj 连字符形态挂载。 - * 模板槽位由 isTemplateOnly 拦截,不会走到这里。 - */ -export function isGeneratedControlledDocId(docId: string): boolean { +export function looksLikeServerOrGeneratedRevisionDocId(docId: string): boolean { const id = (docId || '').trim(); - if (!id || isLegacyLocalMockDocId(id)) return false; - if (/^doc_proj\d+_/.test(id)) return true; + if (!id || isLegacyLocalMockDocId(id) || isUnregisteredUploadOrphanDocId(id)) return false; + if (id.startsWith('doc_proj')) return true; + if (id.startsWith('doc-free-')) return true; if (/^doc-p\d+-/.test(id)) return true; if (/^doc-proj\d+-/.test(id)) return true; + if (!id.startsWith('doc-')) return true; // UUID / future server ids return false; } -/** 仅真实可接入账本且非模板槽位才挂工具条并发起 revision-state。 */ export function shouldMountDocumentRevisionToolbar( doc: RevisionMountDoc | null | undefined, ): boolean { if (!doc?.docId) return false; if (doc.isTemplateOnly) return false; if (isLegacyLocalMockDocId(doc.docId)) return false; - if (isIndexedDbUploadOrphanDocId(doc.docId)) return false; - - if (isServerBackedRevisionDocId(doc.docId)) return true; - if (isGeneratedControlledDocId(doc.docId)) return true; - - // 服务端 freeform:ID 可能非 doc-free 前缀,但带 freeform 事实字段且非本地孤儿 - if (doc.governanceMode === 'freeform' && doc.sourceType !== 'uploaded_file') { - return !isLegacyLocalMockDocId(doc.docId); - } + if (isUnregisteredUploadOrphanDocId(doc.docId)) return false; + if (hasServerRevisionFactFields(doc)) return true; + if (looksLikeServerOrGeneratedRevisionDocId(doc.docId)) return true; return false; } @@ -87,7 +79,7 @@ export function revisionToolbarMountMode( if (!doc?.docId) return 'none'; if (doc.isTemplateOnly) return 'template_hint'; if (shouldMountDocumentRevisionToolbar(doc)) return 'toolbar'; - if (isLegacyLocalMockDocId(doc.docId) || isIndexedDbUploadOrphanDocId(doc.docId)) { + if (isLegacyLocalMockDocId(doc.docId) || isUnregisteredUploadOrphanDocId(doc.docId)) { return 'local_hint'; } if (/^doc-p\d+-/.test(doc.docId) || /^doc-proj\d+-/.test(doc.docId)) { @@ -96,7 +88,6 @@ export function revisionToolbarMountMode( return 'local_hint'; } -/** 未挂载工具条时的非错误提示(不得写成账本未就绪)。 */ export function documentRevisionInactiveHint( doc: RevisionMountDoc | null | undefined, ): string | null {