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
@@ -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<AffineTextAttributes>([
{
insert: REFERENCE_NODE,
attributes: {
reference: {
type: 'LinkedPage',
pageId: syncedDocModel.props.pageId,
},
},
},
]),
} satisfies Partial<ParagraphProps>,
noteId
);
await std.clipboard.duplicateSlice(
Slice.fromModels(std.store, children),
std.store,
@@ -4,7 +4,7 @@
[BlockSuite API Documentation](../../../README.md) / [@blocksuite/store](../README.md) / Text
# Class: Text
# Class: Text\<TextAttributes\>
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.
@@ -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<DeltaOperation[]>;
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.
*/
constructor(input?: Y.Text | string | DeltaInsert[]) {
constructor(input?: Y.Text | string | DeltaInsert<TextAttributes>[]) {
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<TextAttributes>[] = [];
for (let i = 0; i < deltas.length; i++) {
const insert = deltas[i].insert;
if (typeof insert === 'string') {
@@ -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 ({