From 9c939da6b54e970d7b9dc6e9d731226376279a23 Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 27 Mar 2025 06:36:57 +0000 Subject: [PATCH] fix(core): fix duplicate in all page (#11229) --- .../affine/use-block-suite-meta-helper.ts | 49 +---------- .../core/src/modules/doc/services/docs.ts | 87 +++++++++++++++++++ 2 files changed, 91 insertions(+), 45 deletions(-) diff --git a/packages/frontend/core/src/components/hooks/affine/use-block-suite-meta-helper.ts b/packages/frontend/core/src/components/hooks/affine/use-block-suite-meta-helper.ts index 9ef813df35..36913ab577 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-block-suite-meta-helper.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-block-suite-meta-helper.ts @@ -1,20 +1,15 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; -import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta'; -import { useDocCollectionHelper } from '@affine/core/components/hooks/use-block-suite-workspace-helper'; import { DocsService } from '@affine/core/modules/doc'; import { WorkspaceService } from '@affine/core/modules/workspace'; -import type { DocMode } from '@blocksuite/affine/model'; import { useService } from '@toeverything/infra'; import { useCallback } from 'react'; -import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { useNavigateHelper } from '../use-navigate-helper'; export function useBlockSuiteMetaHelper() { const workspace = useService(WorkspaceService).workspace; - const { setDocMeta, getDocMeta, setDocTitle } = useDocMetaHelper(); - const { createDoc } = useDocCollectionHelper(workspace.docCollection); const { openPage } = useNavigateHelper(); + const docsService = useService(DocsService); const docRecordList = useService(DocsService).list; // TODO-Doma @@ -48,47 +43,11 @@ export function useBlockSuiteMetaHelper() { const duplicate = useAsyncCallback( async (pageId: string, openPageAfterDuplication: boolean = true) => { - const currentPagePrimaryMode = - docRecordList.doc$(pageId).value?.primaryMode$.value; - const currentPageMeta = getDocMeta(pageId); - const newPage = createDoc(); - const currentPage = workspace.docCollection.getDoc(pageId); - - newPage.load(); - if (!currentPageMeta || !currentPage) { - return; - } - - const update = encodeStateAsUpdate(currentPage.spaceDoc); - applyUpdate(newPage.spaceDoc, update); - - setDocMeta(newPage.id, { - tags: currentPageMeta.tags, - }); - - const lastDigitRegex = /\((\d+)\)$/; - const match = currentPageMeta?.title?.match(lastDigitRegex); - const newNumber = match ? parseInt(match[1], 10) + 1 : 1; - - const newPageTitle = - currentPageMeta?.title?.replace(lastDigitRegex, '') + `(${newNumber})`; - - docRecordList - .doc$(newPage.id) - .value?.setPrimaryMode(currentPagePrimaryMode || ('page' as DocMode)); - setDocTitle(newPage.id, newPageTitle); + const newPageId = await docsService.duplicate(pageId); openPageAfterDuplication && - openPage(workspace.docCollection.id, newPage.id); + openPage(workspace.docCollection.id, newPageId); }, - [ - docRecordList, - getDocMeta, - createDoc, - workspace.docCollection, - setDocMeta, - setDocTitle, - openPage, - ] + [docsService, openPage, workspace.docCollection.id] ); return { diff --git a/packages/frontend/core/src/modules/doc/services/docs.ts b/packages/frontend/core/src/modules/doc/services/docs.ts index 6599a8c7ec..0f30836f71 100644 --- a/packages/frontend/core/src/modules/doc/services/docs.ts +++ b/packages/frontend/core/src/modules/doc/services/docs.ts @@ -168,6 +168,93 @@ export class DocsService extends Service { release(); } + async duplicate(sourceDocId: string, _targetDocId?: string) { + const targetDocId = _targetDocId ?? this.createDoc().id; + + // check if source doc is removed + if (this.list.doc$(sourceDocId).value?.trash$.value) { + console.warn( + `Template doc(id: ${sourceDocId}) is removed, skip duplicate` + ); + return targetDocId; + } + + const { release: sourceRelease, doc: sourceDoc } = this.open(sourceDocId); + const { release: targetRelease, doc: targetDoc } = this.open(targetDocId); + await sourceDoc.waitForSyncReady(); + + // duplicate doc content + try { + const sourceBsDoc = this.store.getBlockSuiteDoc(sourceDocId); + const targetBsDoc = this.store.getBlockSuiteDoc(targetDocId); + if (!sourceBsDoc) throw new Error('Source doc not found'); + if (!targetBsDoc) throw new Error('Target doc not found'); + + // clear the target doc (both surface and note) + targetBsDoc.root?.children.forEach(child => + targetBsDoc.deleteBlock(child) + ); + + const collection = this.store.getBlocksuiteCollection(); + const transformer = new Transformer({ + schema: getAFFiNEWorkspaceSchema(), + blobCRUD: collection.blobSync, + docCRUD: { + create: (id: string) => collection.createDoc(id).getStore({ id }), + get: (id: string) => collection.getDoc(id)?.getStore({ id }) ?? null, + delete: (id: string) => collection.removeDoc(id), + }, + middlewares: [replaceIdMiddleware(collection.idGenerator)], + }); + const slice = Slice.fromModels(sourceBsDoc, [ + ...(sourceBsDoc.root?.children ?? []), + ]); + const snapshot = transformer.sliceToSnapshot(slice); + if (!snapshot) { + throw new Error('Failed to create snapshot'); + } + await transformer.snapshotToSlice( + snapshot, + targetBsDoc, + targetBsDoc.root?.id + ); + } catch (e) { + logger.error('Failed to duplicate doc', { + sourceDocId, + targetDocId, + originalTargetDocId: _targetDocId, + error: e, + }); + } finally { + sourceRelease(); + targetRelease(); + } + + // duplicate doc meta + targetDoc.record.setMeta({ + tags: sourceDoc.meta$.value.tags, + }); + + // duplicate doc title + const originalTitle = sourceDoc.title$.value; + const lastDigitRegex = /\((\d+)\)$/; + const match = originalTitle.match(lastDigitRegex); + const newNumber = match ? parseInt(match[1], 10) + 1 : 1; + const newPageTitle = + originalTitle.replace(lastDigitRegex, '') + `(${newNumber})`; + targetDoc.changeDocTitle(newPageTitle); + + // duplicate doc properties + const properties = sourceDoc.getProperties(); + const removedProperties = ['id', 'isTemplate', 'journal']; + removedProperties.forEach(key => { + delete properties[key]; + }); + targetDoc.updateProperties(properties); + + return targetDocId; + } + /** * Duplicate a doc from template * @param sourceDocId - the id of the source doc to be duplicated