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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<DeliverableItem, 'docId' | 'isTemplateOnly'>
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user