From e457e2f8a8daef4b7b7f137502727105f62c8e43 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Tue, 22 Apr 2025 08:03:52 +0000 Subject: [PATCH] fix(editor): add reference after duplicate edgeless embed doc as note (#11877) - fix(editor): add reference in the copied note of embed doc - refactor(editor): add generics parameter `TextAttributes` into `Text` --- .../embed-synced-doc-block/configs/toolbar.ts | 28 +++++++++++++++++- .../api/@blocksuite/store/classes/Text.md | 18 ++++++++---- .../framework/store/src/reactive/text/text.ts | 9 ++++-- .../e2e/embed-synced-doc/edgeless.spec.ts | 29 ++++++++++++++----- 4 files changed, 67 insertions(+), 17 deletions(-) diff --git a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts index c271cb5624..d3ba26ef6b 100644 --- a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/configs/toolbar.ts @@ -7,11 +7,13 @@ import { NoteBlockModel, NoteDisplayMode, type NoteProps, + type ParagraphProps, } from '@blocksuite/affine-model'; import { draftSelectedModelsCommand, duplicateSelectedModelsCommand, } from '@blocksuite/affine-shared/commands'; +import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { ActionPlacement, EditorSettingProvider, @@ -24,6 +26,7 @@ import { type ToolbarModuleConfig, ToolbarModuleExtension, } from '@blocksuite/affine-shared/services'; +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { getBlockProps, matchModels } from '@blocksuite/affine-shared/utils'; import { Bound } from '@blocksuite/global/gfx'; import { @@ -36,7 +39,12 @@ import { OpenInNewIcon, } from '@blocksuite/icons/lit'; import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std'; -import { type BlockModel, type ExtensionType, Slice } from '@blocksuite/store'; +import { + type BlockModel, + type ExtensionType, + Slice, + Text, +} from '@blocksuite/store'; import { computed, signal } from '@preact/signals-core'; import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -372,6 +380,24 @@ const builtinSurfaceToolbarConfig = { ctx.store.root ); + std.store.addBlock( + 'affine:paragraph', + { + text: new Text([ + { + insert: REFERENCE_NODE, + attributes: { + reference: { + type: 'LinkedPage', + pageId: syncedDocModel.props.pageId, + }, + }, + }, + ]), + } satisfies Partial, + noteId + ); + await std.clipboard.duplicateSlice( Slice.fromModels(std.store, children), std.store, diff --git a/blocksuite/docs/api/@blocksuite/store/classes/Text.md b/blocksuite/docs/api/@blocksuite/store/classes/Text.md index 7f10a1843c..ab31c4e975 100644 --- a/blocksuite/docs/api/@blocksuite/store/classes/Text.md +++ b/blocksuite/docs/api/@blocksuite/store/classes/Text.md @@ -4,7 +4,7 @@ [BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / Text -# Class: Text +# Class: Text\ Text is an abstraction of Y.Text. It provides useful methods to manipulate the text content. @@ -22,11 +22,17 @@ text.split(7, 1); Text [delta](https://docs.yjs.dev/api/delta-format) is a format from Y.js. +## Type Parameters + +### TextAttributes + +`TextAttributes` *extends* `BaseTextAttributes` = `BaseTextAttributes` + ## Constructors ### Constructor -> **new Text**(`input?`): `Text` +> **new Text**\<`TextAttributes`\>(`input?`): `Text`\<`TextAttributes`\> #### Parameters @@ -34,11 +40,11 @@ Text [delta](https://docs.yjs.dev/api/delta-format) is a format from Y.js. The input can be a string, a Y.Text instance, or an array of DeltaInsert. -`string` | `YText` | `DeltaInsert`[] +`string` | `YText` | `DeltaInsert`\<`TextAttributes`\>[] #### Returns -`Text` +`Text`\<`TextAttributes`\> ## Accessors @@ -97,13 +103,13 @@ Clear the text content. ### clone() -> **clone**(): `Text` +> **clone**(): `Text`\<\{ `bold`: `null` \| `true`; `code`: `null` \| `true`; `italic`: `null` \| `true`; `link`: `null` \| `string`; `strike`: `null` \| `true`; `underline`: `null` \| `true`; \}\> Clone the text to a new Text instance. #### Returns -`Text` +`Text`\<\{ `bold`: `null` \| `true`; `code`: `null` \| `true`; `italic`: `null` \| `true`; `link`: `null` \| `string`; `strike`: `null` \| `true`; `underline`: `null` \| `true`; \}\> A new Text instance. diff --git a/blocksuite/framework/store/src/reactive/text/text.ts b/blocksuite/framework/store/src/reactive/text/text.ts index d1245051fa..a2b8acd68f 100644 --- a/blocksuite/framework/store/src/reactive/text/text.ts +++ b/blocksuite/framework/store/src/reactive/text/text.ts @@ -2,6 +2,7 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { type Signal, signal } from '@preact/signals-core'; import * as Y from 'yjs'; +import type { BaseTextAttributes } from './attributes'; import type { DeltaInsert, DeltaOperation, OnTextChange } from './types'; /** @@ -22,7 +23,9 @@ import type { DeltaInsert, DeltaOperation, OnTextChange } from './types'; * * @category Reactive */ -export class Text { +export class Text< + TextAttributes extends BaseTextAttributes = BaseTextAttributes, +> { private readonly _deltas$: Signal; private readonly _length$: Signal; @@ -49,7 +52,7 @@ export class Text { /** * @param input - The input can be a string, a Y.Text instance, or an array of DeltaInsert. */ - constructor(input?: Y.Text | string | DeltaInsert[]) { + constructor(input?: Y.Text | string | DeltaInsert[]) { let length = 0; if (typeof input === 'string') { const text = input.replaceAll('\r\n', '\n'); @@ -417,7 +420,7 @@ export class Text { ); } let tmpIndex = 0; - const rightDeltas: DeltaInsert[] = []; + const rightDeltas: DeltaInsert[] = []; for (let i = 0; i < deltas.length; i++) { const insert = deltas[i].insert; if (typeof insert === 'string') { diff --git a/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts b/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts index fc294806f8..0748cdc6c5 100644 --- a/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts +++ b/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts @@ -1,3 +1,4 @@ +import type { AffineReference } from '@blocksuite/affine/inlines/reference'; import { expect, type Page } from '@playwright/test'; import { clickView } from '../utils/actions/click.js'; @@ -95,6 +96,12 @@ test.describe('Embed synced doc in edgeless mode', () => { await edgelessEmbedSyncedBlock.click(); }); + const getDocIds = async (page: Page) => { + return page.evaluate(() => { + return [...window.collection.docs.keys()]; + }); + }; + const locateToolbar = (page: Page) => { return page.locator( // TODO(@L-Sun): simplify this selector after that toolbar widget are disabled in preview rendering is ready @@ -123,7 +130,7 @@ test.describe('Embed synced doc in edgeless mode', () => { await expect(embedSyncedBlock).toBeVisible(); }); - test('should using all content of embed-synced-doc to duplicate as a note', async ({ + test('should render a reference node and all content of embed-synced-doc after click "Duplicate as note" button', async ({ page, }) => { // switch doc @@ -158,13 +165,21 @@ test.describe('Embed synced doc in edgeless mode', () => { await expect(edgelessNotes).toHaveCount(2); await expect(edgelessNotes.last()).toBeVisible(); - const paragraphs = edgelessNotes - .last() - .locator('affine-paragraph [data-v-root="true"]'); + const blocks = edgelessNotes.last().locator('[data-block-id]'); + await expect(blocks).toHaveCount(3); + const reference = blocks.nth(0).locator('affine-reference'); + const paragraph1 = blocks.nth(1).locator('[data-v-text="true"]'); + const paragraph2 = blocks.nth(2).locator('[data-v-text="true"]'); + const refInfo = await reference.evaluate((reference: AffineReference) => { + return reference.delta.attributes?.reference; + }); - await expect(paragraphs).toHaveCount(2); - await expect(paragraphs.first()).toHaveText('hello page 1'); - await expect(paragraphs.last()).toHaveText('hello note'); + expect(refInfo).toEqual({ + type: 'LinkedPage', + pageId: (await getDocIds(page))[1], + }); + await expect(paragraph1).toHaveText('hello page 1'); + await expect(paragraph2).toHaveText('hello note'); }); test('should be selected and not overlay with the embed-synced-doc after duplicating as note', async ({