fix(editor): update card style after dragging it to note (#12660)

Close [BS-3148](https://linear.app/affine-design/issue/BS-3148/拖拽到note后,更新card样式)

### What Changes
- fix the style of card not updated after draggin it from canvas to note
- narrow type of specific card style by using `as const satisfies EmbedCardStyle[]`
- add type hint to the `props`, the second parameter  of `store.updateBlock`

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added middleware to automatically update card styles when dragging blocks into notes.
- **Bug Fixes**
  - Ensured that dragging a bookmark card into a note preserves its style.
- **Tests**
  - Introduced an end-to-end test to verify bookmark card style is retained after drag-and-drop.
- **Refactor**
  - Enhanced type safety and clarity for card style configurations and block properties.
- **Chores**
  - Refined type annotations and assertions across multiple block style constants and toolbar configurations.
  - Improved generic typing for block update methods to increase type precision.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-05-30 10:55:28 +00:00
parent 887a496f8b
commit 10da3ad28e
21 changed files with 213 additions and 98 deletions
@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let { style } = model.props;
let style: EmbedCardStyle = model.props.style;
if (!styles.includes(style)) {
style = styles[0];
@@ -482,24 +482,26 @@ const builtinSurfaceToolbarConfig = {
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'b.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => BookmarkStyles.includes(action.id)),
content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null;
@@ -259,18 +259,18 @@ const builtinToolbarConfig = {
conversionsActionGroup,
{
id: 'c.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
].filter(action =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
] as const
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;
@@ -368,26 +368,26 @@ const builtinSurfaceToolbarConfig = {
conversionsActionGroup,
{
id: 'c.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action =>
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
),
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
content(ctx) {
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
if (!model) return null;
@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
let { style } = model.props;
let style: EmbedCardStyle = model.props.style;
let flavour = 'affine:bookmark';
if (options?.viewType === 'card') {
@@ -227,7 +227,7 @@ function createBuiltinToolbarConfigForExternal(
if (options?.viewType !== 'embed') return;
const { flavour, styles } = options;
let { style } = model.props;
let style: EmbedCardStyle = model.props.style;
if (!styles.includes(style)) {
style =
@@ -441,7 +441,11 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
let { style } = model.props;
let flavour = 'affine:bookmark';
if (!BookmarkStyles.includes(style)) {
if (
!BookmarkStyles.includes(
style as (typeof BookmarkStyles)[number]
)
) {
style = BookmarkStyles[0];
}
@@ -517,26 +521,26 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
} satisfies ToolbarActionGroup<ToolbarAction>,
{
id: 'c.style',
actions: [
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
].filter(action =>
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
),
actions: (
[
{
id: 'horizontal',
label: 'Large horizontal style',
},
{
id: 'list',
label: 'Small horizontal style',
},
{
id: 'vertical',
label: 'Large vertical style',
},
{
id: 'cube',
label: 'Small vertical style',
},
] as const
).filter(action => EmbedGithubStyles.includes(action.id)),
when(ctx) {
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
},
@@ -9,7 +9,10 @@ import {
getSurfaceComponent,
} from '@blocksuite/affine-block-surface';
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import type {
EmbedCardStyle,
ShapeElementModel,
} from '@blocksuite/affine-model';
import {
BookmarkStyles,
DEFAULT_NOTE_HEIGHT,
@@ -236,7 +239,7 @@ export class EdgelessClipboardController extends PageClipboard {
const options: Record<string, unknown> = {};
let flavour = 'affine:bookmark';
let style = BookmarkStyles[0];
let style: EmbedCardStyle = BookmarkStyles[0];
let isInternalLink = false;
let isLinkedBlock = false;
@@ -30,11 +30,12 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
*/
type BackwardCompatibleUndefined = undefined;
export const AttachmentBlockStyles: EmbedCardStyle[] = [
export const AttachmentBlockStyles = [
'cubeThick',
'horizontalThin',
'pdf',
] as const;
'citation',
] as const satisfies EmbedCardStyle[];
export type AttachmentBlockProps = {
name: string;
@@ -15,13 +15,13 @@ import type {
LinkPreviewData,
} from '../../utils/index.js';
export const BookmarkStyles: EmbedCardStyle[] = [
export const BookmarkStyles = [
'vertical',
'horizontal',
'list',
'cube',
'citation',
] as const;
] as const satisfies EmbedCardStyle[];
export type BookmarkBlockProps = {
style: (typeof BookmarkStyles)[number];
@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
description: string | null;
};
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
export type EmbedFigmaBlockProps = {
style: (typeof EmbedFigmaStyles)[number];
@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
assignees: string[] | null;
};
export const EmbedGithubStyles: EmbedCardStyle[] = [
export const EmbedGithubStyles = [
'vertical',
'horizontal',
'list',
'cube',
] as const;
] as const satisfies EmbedCardStyle[];
export type EmbedGithubBlockProps = {
style: (typeof EmbedGithubStyles)[number];
@@ -3,7 +3,7 @@ import { BlockModel } from '@blocksuite/store';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
export const EmbedHtmlStyles = ['html'] as const satisfies EmbedCardStyle[];
export type EmbedHtmlBlockProps = {
style: (typeof EmbedHtmlStyles)[number];
@@ -7,7 +7,7 @@ import { BlockModel } from '@blocksuite/store';
import { type EmbedCardStyle } from '../../../utils/index.js';
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
export const EmbedIframeStyles = ['figma'] as const satisfies EmbedCardStyle[];
export type EmbedIframeBlockProps = {
url: string; // the original url that user input
@@ -4,17 +4,17 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
export const EmbedLinkedDocStyles = [
'vertical',
'horizontal',
'list',
'cube',
'horizontalThin',
'citation',
];
] as const satisfies EmbedCardStyle[];
export type EmbedLinkedDocBlockProps = {
style: EmbedCardStyle;
style: (typeof EmbedLinkedDocStyles)[number];
caption: string | null;
footnoteIdentifier: string | null;
} & ReferenceInfo;
@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
description: string | null;
};
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
export type EmbedLoomBlockProps = {
style: (typeof EmbedLoomStyles)[number];
@@ -5,7 +5,9 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
import type { EmbedCardStyle } from '../../../utils/index.js';
import { defineEmbedModel } from '../../../utils/index.js';
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
export const EmbedSyncedDocStyles = [
'syncedDoc',
] as const satisfies EmbedCardStyle[];
export type EmbedSyncedDocBlockProps = {
style: EmbedCardStyle;
@@ -1,6 +1,7 @@
import type { GfxModel } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { BookmarkBlockModel } from '../bookmark';
import { EmbedFigmaModel } from './figma';
import { EmbedGithubModel } from './github';
import type { EmbedHtmlModel } from './html';
@@ -30,7 +31,10 @@ export type EmbedCardModel = InstanceType<
ExternalEmbedModel | InternalEmbedModel
>;
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
export type LinkableEmbedModel =
| EmbedCardModel
| EmbedIframeBlockModel
| BookmarkBlockModel;
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
creatorImage: string | null;
};
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
export type EmbedYoutubeBlockProps = {
style: (typeof EmbedYoutubeStyles)[number];
@@ -0,0 +1,56 @@
import {
AttachmentBlockModel,
BookmarkBlockModel,
EmbedGithubModel,
EmbedLinkedDocModel,
NoteBlockModel,
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { TransformerMiddleware } from '@blocksuite/store';
export const cardStyleUpdater =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
slots.beforeImport.subscribe(payload => {
if (payload.type !== 'block' || !payload.parent) return;
const parentModel = std.store.getModelById(payload.parent);
if (!matchModels(parentModel, [NoteBlockModel])) return;
// TODO(@L-Sun): Refactor this after refactor `store.moveBlocks`
// Currently, drag a block will use store.moveBlocks to update the tree of blocks
// but the instance of it is not changed.
// So change the style of snapshot.props in the middleware is not working.
// Instead, we can change the style of the model instance in the middleware,
const model = std.store.getModelById(payload.snapshot.id);
if (!model) return;
if (model instanceof AttachmentBlockModel) {
std.store.updateBlock(model, {
style: 'horizontalThin',
});
return;
}
if (model instanceof BookmarkBlockModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
if (model instanceof EmbedGithubModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
if (model instanceof EmbedLinkedDocModel) {
std.store.updateBlock(model, {
style: 'horizontal',
});
return;
}
});
};
@@ -76,6 +76,7 @@ import last from 'lodash-es/last';
import type { AffineDragHandleWidget } from '../drag-handle.js';
import { PreviewHelper } from '../helpers/preview-helper.js';
import { gfxBlocksFilter } from '../middleware/blocks-filter.js';
import { cardStyleUpdater } from '../middleware/card-style-updater.js';
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
import { reorderList } from '../middleware/reorder-list';
import {
@@ -1433,6 +1434,7 @@ export class DragEventWatcher {
newIdCrossDoc(std),
reorderList(std),
surfaceRefToEmbed(std),
cardStyleUpdater(std),
];
if (selectedIds) {
@@ -566,23 +566,29 @@ Optional flag to insert before sibling
### updateBlock()
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
Updates a block's properties or executes a callback in a transaction
#### Type Parameters
##### T
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
#### Parameters
##### modelOrId
The block model or block ID to update
`string` | `BlockModel`\<`object`\>
`string` | `T`
##### callBackOrProps
Either a callback function to execute or properties to update
`Partial`\<`BlockProps`\> | () => `void`
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
#### Returns
@@ -19,3 +19,5 @@ export type BlockSysProps = {
children?: BlockModel[];
};
export type BlockProps = BlockSysProps & Record<string, unknown>;
export type PropsOfModel<T> = T extends BlockModel<infer P> ? P : never;
@@ -22,6 +22,8 @@ import {
type BlockModel,
type BlockOptions,
type BlockProps,
type BlockSysProps,
type PropsOfModel,
type YBlock,
} from '../block/index.js';
import { DocCRUD } from './crud.js';
@@ -852,9 +854,12 @@ export class Store {
*
* @category Block CRUD
*/
updateBlock(
modelOrId: BlockModel | string,
callBackOrProps: (() => void) | Partial<BlockProps>
updateBlock<T extends BlockModel = BlockModel>(
modelOrId: T | string,
callBackOrProps:
| (() => void)
| Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps>
) {
if (this.readonly) {
console.error('cannot modify data in readonly mode');
+28
View File
@@ -1,5 +1,6 @@
import './utils/declare-test-window.js';
import type { BookmarkBlockComponent } from '@blocksuite/affine/blocks/bookmark';
import type { BlockSnapshot } from '@blocksuite/store';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
@@ -8,7 +9,9 @@ import {
activeNoteInEdgeless,
clickView,
copyByKeyboard,
createNote,
dragBlockToPoint,
edgelessCommonSetup,
enterPlaygroundRoom,
expectConsoleMessage,
focusRichText,
@@ -500,3 +503,28 @@ test.describe('embed github card', () => {
await expect(banner).toBeVisible();
});
});
test('drag a card from canvas to note should not change the style of the card', async ({
page,
}) => {
const url = 'https://github.com/toeverything/AFFiNE/pull/12660';
await edgelessCommonSetup(page);
await createNote(page, [-100, -300]);
await page.locator('edgeless-link-tool-button').click();
await page.locator('.embed-card-modal-input').fill(url);
await pressEnter(page);
const edgelessBookmark = page.locator('affine-edgeless-bookmark');
await edgelessBookmark.click();
await waitNextFrame(page);
const dragHandle = page.locator('.affine-drag-handle-container');
await dragHandle.dragTo(page.locator('affine-edgeless-note'));
const noteBookmark = page.locator('affine-bookmark');
await expect(noteBookmark).toBeVisible();
const style = await noteBookmark.evaluate(
(el: BookmarkBlockComponent) => el.model.props.style
);
expect(style).toBe('horizontal');
});