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`
This commit is contained in:
L-Sun
2025-04-22 08:03:52 +00:00
parent 6d6504e2af
commit e457e2f8a8
4 changed files with 67 additions and 17 deletions

View File

@@ -7,11 +7,13 @@ import {
NoteBlockModel, NoteBlockModel,
NoteDisplayMode, NoteDisplayMode,
type NoteProps, type NoteProps,
type ParagraphProps,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
draftSelectedModelsCommand, draftSelectedModelsCommand,
duplicateSelectedModelsCommand, duplicateSelectedModelsCommand,
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { import {
ActionPlacement, ActionPlacement,
EditorSettingProvider, EditorSettingProvider,
@@ -24,6 +26,7 @@ import {
type ToolbarModuleConfig, type ToolbarModuleConfig,
ToolbarModuleExtension, ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { getBlockProps, matchModels } from '@blocksuite/affine-shared/utils'; import { getBlockProps, matchModels } from '@blocksuite/affine-shared/utils';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { import {
@@ -36,7 +39,12 @@ import {
OpenInNewIcon, OpenInNewIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std'; 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 { computed, signal } from '@preact/signals-core';
import { html } from 'lit'; import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
@@ -372,6 +380,24 @@ const builtinSurfaceToolbarConfig = {
ctx.store.root ctx.store.root
); );
std.store.addBlock(
'affine:paragraph',
{
text: new Text<AffineTextAttributes>([
{
insert: REFERENCE_NODE,
attributes: {
reference: {
type: 'LinkedPage',
pageId: syncedDocModel.props.pageId,
},
},
},
]),
} satisfies Partial<ParagraphProps>,
noteId
);
await std.clipboard.duplicateSlice( await std.clipboard.duplicateSlice(
Slice.fromModels(std.store, children), Slice.fromModels(std.store, children),
std.store, std.store,

View File

@@ -4,7 +4,7 @@
[BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / Text [BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / Text
# Class: Text # Class: Text\<TextAttributes\>
Text is an abstraction of Y.Text. Text is an abstraction of Y.Text.
It provides useful methods to manipulate the text content. 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. Text [delta](https://docs.yjs.dev/api/delta-format) is a format from Y.js.
## Type Parameters
### TextAttributes
`TextAttributes` *extends* `BaseTextAttributes` = `BaseTextAttributes`
## Constructors ## Constructors
### Constructor ### Constructor
> **new Text**(`input?`): `Text` > **new Text**\<`TextAttributes`\>(`input?`): `Text`\<`TextAttributes`\>
#### Parameters #### 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. The input can be a string, a Y.Text instance, or an array of DeltaInsert.
`string` | `YText` | `DeltaInsert`[] `string` | `YText` | `DeltaInsert`\<`TextAttributes`\>[]
#### Returns #### Returns
`Text` `Text`\<`TextAttributes`\>
## Accessors ## Accessors
@@ -97,13 +103,13 @@ Clear the text content.
### clone() ### 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. Clone the text to a new Text instance.
#### Returns #### 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. A new Text instance.

View File

@@ -2,6 +2,7 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { type Signal, signal } from '@preact/signals-core'; import { type Signal, signal } from '@preact/signals-core';
import * as Y from 'yjs'; import * as Y from 'yjs';
import type { BaseTextAttributes } from './attributes';
import type { DeltaInsert, DeltaOperation, OnTextChange } from './types'; import type { DeltaInsert, DeltaOperation, OnTextChange } from './types';
/** /**
@@ -22,7 +23,9 @@ import type { DeltaInsert, DeltaOperation, OnTextChange } from './types';
* *
* @category Reactive * @category Reactive
*/ */
export class Text { export class Text<
TextAttributes extends BaseTextAttributes = BaseTextAttributes,
> {
private readonly _deltas$: Signal<DeltaOperation[]>; private readonly _deltas$: Signal<DeltaOperation[]>;
private readonly _length$: Signal<number>; private readonly _length$: Signal<number>;
@@ -49,7 +52,7 @@ export class Text {
/** /**
* @param input - The input can be a string, a Y.Text instance, or an array of DeltaInsert. * @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<TextAttributes>[]) {
let length = 0; let length = 0;
if (typeof input === 'string') { if (typeof input === 'string') {
const text = input.replaceAll('\r\n', '\n'); const text = input.replaceAll('\r\n', '\n');
@@ -417,7 +420,7 @@ export class Text {
); );
} }
let tmpIndex = 0; let tmpIndex = 0;
const rightDeltas: DeltaInsert[] = []; const rightDeltas: DeltaInsert<TextAttributes>[] = [];
for (let i = 0; i < deltas.length; i++) { for (let i = 0; i < deltas.length; i++) {
const insert = deltas[i].insert; const insert = deltas[i].insert;
if (typeof insert === 'string') { if (typeof insert === 'string') {

View File

@@ -1,3 +1,4 @@
import type { AffineReference } from '@blocksuite/affine/inlines/reference';
import { expect, type Page } from '@playwright/test'; import { expect, type Page } from '@playwright/test';
import { clickView } from '../utils/actions/click.js'; import { clickView } from '../utils/actions/click.js';
@@ -95,6 +96,12 @@ test.describe('Embed synced doc in edgeless mode', () => {
await edgelessEmbedSyncedBlock.click(); await edgelessEmbedSyncedBlock.click();
}); });
const getDocIds = async (page: Page) => {
return page.evaluate(() => {
return [...window.collection.docs.keys()];
});
};
const locateToolbar = (page: Page) => { const locateToolbar = (page: Page) => {
return page.locator( return page.locator(
// TODO(@L-Sun): simplify this selector after that toolbar widget are disabled in preview rendering is ready // 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(); 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, page,
}) => { }) => {
// switch doc // switch doc
@@ -158,13 +165,21 @@ test.describe('Embed synced doc in edgeless mode', () => {
await expect(edgelessNotes).toHaveCount(2); await expect(edgelessNotes).toHaveCount(2);
await expect(edgelessNotes.last()).toBeVisible(); await expect(edgelessNotes.last()).toBeVisible();
const paragraphs = edgelessNotes const blocks = edgelessNotes.last().locator('[data-block-id]');
.last() await expect(blocks).toHaveCount(3);
.locator('affine-paragraph [data-v-root="true"]'); 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); expect(refInfo).toEqual({
await expect(paragraphs.first()).toHaveText('hello page 1'); type: 'LinkedPage',
await expect(paragraphs.last()).toHaveText('hello note'); 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 ({ test('should be selected and not overlay with the embed-synced-doc after duplicating as note', async ({