From 1d865f16fe0fca2caf91c9d89f2f909dfa4b526a Mon Sep 17 00:00:00 2001 From: L-Sun Date: Tue, 8 Jul 2025 18:33:09 +0800 Subject: [PATCH] feat(editor): comment for edgeless element (#13098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #13098** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **New Features** * Added support for comments on graphical elements, allowing users to comment on both blocks and graphical elements within surfaces. * Enhanced comment previews to include graphical elements in selection summaries. * Improved editor navigation to focus on commented graphical elements in addition to blocks and inline texts. * **Bug Fixes** * Updated comment highlighting and management to consistently use the new comment manager across all block and element types. * **Refactor** * Renamed and extended the comment manager to handle both block and element comments. * Streamlined toolbar configurations by removing outdated comment button entries and adding a consolidated comment button in the root toolbar. * **Tests** * Disabled the mock comment provider integration in the test editor environment to refine testing setup. --- .../blocks/attachment/src/attachment-block.ts | 4 +- .../blocks/attachment/src/configs/toolbar.ts | 5 - .../blocks/bookmark/src/bookmark-block.ts | 4 +- .../blocks/bookmark/src/configs/toolbar.ts | 5 - .../affine/blocks/code/src/code-block.ts | 4 +- .../blocks/database/src/database-block.ts | 4 +- .../embed-linked-doc-block/configs/toolbar.ts | 5 - .../embed-synced-doc-block/configs/toolbar.ts | 5 - .../embed/src/common/embed-block-element.ts | 4 +- .../blocks/embed/src/configs/toolbar.ts | 5 - .../blocks/image/src/configs/toolbar.ts | 9 - .../affine/blocks/image/src/image-block.ts | 4 +- .../blocks/paragraph/src/paragraph-block.ts | 4 +- .../root/src/edgeless/configs/toolbar/misc.ts | 7 + .../blocks/surface-ref/src/configs/toolbar.ts | 5 - .../surface-ref/src/surface-ref-block.ts | 4 +- blocksuite/affine/foundation/src/view.ts | 4 +- .../affine/gfx/text/src/toolbar/actions.ts | 6 +- .../comment/src/inline-comment-manager.ts | 4 +- .../comment-service/block-comment-manager.ts | 118 ------------ .../block-element-comment-manager.ts | 169 ++++++++++++++++++ .../src/services/comment-service/index.ts | 2 +- .../src/services/comment-service/utils.ts | 61 +++++-- .../src/gfx/model/surface/element-model.ts | 5 + .../apps/starter/utils/extensions.ts | 1 + .../comment/comment-provider.ts | 30 +++- .../src/modules/editor/entities/editor.ts | 7 + 27 files changed, 288 insertions(+), 197 deletions(-) delete mode 100644 blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts create mode 100644 blocksuite/affine/shared/src/services/comment-service/block-element-comment-manager.ts diff --git a/blocksuite/affine/blocks/attachment/src/attachment-block.ts b/blocksuite/affine/blocks/attachment/src/attachment-block.ts index 40f88f1a9d..36005d6f09 100644 --- a/blocksuite/affine/blocks/attachment/src/attachment-block.ts +++ b/blocksuite/affine/blocks/attachment/src/attachment-block.ts @@ -17,7 +17,7 @@ import { AttachmentBlockStyles, } from '@blocksuite/affine-model'; import { - BlockCommentManager, + BlockElementCommentManager, CitationProvider, DocModeProvider, FileSizeLimitProvider, @@ -96,7 +96,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent, captionAction, - { - id: 'e.comment', - ...blockCommentToolbarButton, - }, { placement: ActionPlacement.More, id: 'a.clipboard', diff --git a/blocksuite/affine/blocks/code/src/code-block.ts b/blocksuite/affine/blocks/code/src/code-block.ts index df140da6d5..ba386b88c3 100644 --- a/blocksuite/affine/blocks/code/src/code-block.ts +++ b/blocksuite/affine/blocks/code/src/code-block.ts @@ -6,7 +6,7 @@ import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR, } from '@blocksuite/affine-shared/consts'; import { - BlockCommentManager, + BlockElementCommentManager, DocModeProvider, NotificationProvider, } from '@blocksuite/affine-shared/services'; @@ -394,7 +394,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent get isCommentHighlighted() { return ( this.std - .getOptional(BlockCommentManager) + .getOptional(BlockElementCommentManager) ?.isBlockCommentHighlighted(this.model) ?? false ); } diff --git a/blocksuite/affine/blocks/database/src/database-block.ts b/blocksuite/affine/blocks/database/src/database-block.ts index 7fa7514f17..dce944c1fa 100644 --- a/blocksuite/affine/blocks/database/src/database-block.ts +++ b/blocksuite/affine/blocks/database/src/database-block.ts @@ -10,7 +10,7 @@ import { toast } from '@blocksuite/affine-components/toast'; import type { DatabaseBlockModel } from '@blocksuite/affine-model'; import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts'; import { - BlockCommentManager, + BlockElementCommentManager, CommentProviderIdentifier, DocModeProvider, NotificationProvider, @@ -316,7 +316,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent, captionAction, - { - id: 'e.comment', - ...blockCommentToolbarButton, - }, { placement: ActionPlacement.More, id: 'a.clipboard', diff --git a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/toolbar.ts b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/toolbar.ts index 4c80430c7f..80071ea64e 100644 --- a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/toolbar.ts +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/toolbar.ts @@ -16,7 +16,6 @@ import { import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { ActionPlacement, - blockCommentToolbarButton, EditorSettingProvider, type LinkEventType, type OpenDocMode, @@ -226,10 +225,6 @@ const builtinToolbarConfig = { openDocActionGroup, conversionsActionGroup, captionAction, - { - id: 'e.comment', - ...blockCommentToolbarButton, - }, { placement: ActionPlacement.More, id: 'a.clipboard', diff --git a/blocksuite/affine/blocks/embed/src/common/embed-block-element.ts b/blocksuite/affine/blocks/embed/src/common/embed-block-element.ts index 87d6d3cb6c..d0d3116598 100644 --- a/blocksuite/affine/blocks/embed/src/common/embed-block-element.ts +++ b/blocksuite/affine/blocks/embed/src/common/embed-block-element.ts @@ -9,7 +9,7 @@ import { EMBED_CARD_WIDTH, } from '@blocksuite/affine-shared/consts'; import { - BlockCommentManager, + BlockElementCommentManager, DocModeProvider, } from '@blocksuite/affine-shared/services'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; @@ -65,7 +65,7 @@ export class EmbedBlockComponent< get isCommentHighlighted() { return ( this.std - .getOptional(BlockCommentManager) + .getOptional(BlockElementCommentManager) ?.isBlockCommentHighlighted(this.model) ?? false ); } diff --git a/blocksuite/affine/blocks/embed/src/configs/toolbar.ts b/blocksuite/affine/blocks/embed/src/configs/toolbar.ts index c055a7b719..81ce40f61c 100644 --- a/blocksuite/affine/blocks/embed/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/embed/src/configs/toolbar.ts @@ -13,7 +13,6 @@ import { } from '@blocksuite/affine-shared/consts'; import { ActionPlacement, - blockCommentToolbarButton, EmbedOptionProvider, type LinkEventType, type ToolbarAction, @@ -349,10 +348,6 @@ function createBuiltinToolbarConfigForExternal( }); }, }, - { - id: 'e.comment', - ...blockCommentToolbarButton, - }, { placement: ActionPlacement.More, id: 'a.clipboard', diff --git a/blocksuite/affine/blocks/image/src/configs/toolbar.ts b/blocksuite/affine/blocks/image/src/configs/toolbar.ts index bce27aacc2..67b64152ae 100644 --- a/blocksuite/affine/blocks/image/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/image/src/configs/toolbar.ts @@ -1,7 +1,6 @@ import { ImageBlockModel } from '@blocksuite/affine-model'; import { ActionPlacement, - blockCommentToolbarButton, type ToolbarModuleConfig, ToolbarModuleExtension, } from '@blocksuite/affine-shared/services'; @@ -50,10 +49,6 @@ const builtinToolbarConfig = { }); }, }, - { - id: 'c.comment', - ...blockCommentToolbarButton, - }, { placement: ActionPlacement.More, id: 'a.clipboard', @@ -146,10 +141,6 @@ const builtinSurfaceToolbarConfig = { }); }, }, - { - id: 'c.comment', - ...blockCommentToolbarButton, - }, ], when: ctx => ctx.getSurfaceModelsByType(ImageBlockModel).length === 1, diff --git a/blocksuite/affine/blocks/image/src/image-block.ts b/blocksuite/affine/blocks/image/src/image-block.ts index f50bfbfc97..c1b77e7505 100644 --- a/blocksuite/affine/blocks/image/src/image-block.ts +++ b/blocksuite/affine/blocks/image/src/image-block.ts @@ -6,7 +6,7 @@ import { ResourceController } from '@blocksuite/affine-components/resource'; import type { ImageBlockModel } from '@blocksuite/affine-model'; import { ImageSelection } from '@blocksuite/affine-shared/selection'; import { - BlockCommentManager, + BlockElementCommentManager, ToolbarRegistryIdentifier, } from '@blocksuite/affine-shared/services'; import { formatSize } from '@blocksuite/affine-shared/utils'; @@ -71,7 +71,7 @@ export class ImageBlockComponent extends CaptionedBlockComponent ({ ...action, diff --git a/blocksuite/affine/blocks/surface-ref/src/configs/toolbar.ts b/blocksuite/affine/blocks/surface-ref/src/configs/toolbar.ts index 1cd84bae5a..6eb081cffd 100644 --- a/blocksuite/affine/blocks/surface-ref/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/surface-ref/src/configs/toolbar.ts @@ -5,7 +5,6 @@ import { } from '@blocksuite/affine-shared/commands'; import { ActionPlacement, - blockCommentToolbarButton, type ToolbarModuleConfig, } from '@blocksuite/affine-shared/services'; import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit'; @@ -62,10 +61,6 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = { surfaceRefBlock.captionElement.show(); }, }, - { - id: 'e.comment', - ...blockCommentToolbarButton, - }, { id: 'a.clipboard', placement: ActionPlacement.More, diff --git a/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts b/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts index 893f5e793a..2d4cb861af 100644 --- a/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts +++ b/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts @@ -13,7 +13,7 @@ import { type SurfaceRefBlockModel, } from '@blocksuite/affine-model'; import { - BlockCommentManager, + BlockElementCommentManager, DocModeProvider, EditPropsStore, type OpenDocMode, @@ -145,7 +145,7 @@ export class SurfaceRefBlockComponent extends BlockComponent { this._handleDeleteAndResolve(comment); - this.std.get(BlockCommentManager).handleDeleteAndResolve(comment); + this.std.get(BlockElementCommentManager).handleDeleteAndResolve(comment); }); } diff --git a/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts b/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts deleted file mode 100644 index b5e33b8cba..0000000000 --- a/blocksuite/affine/shared/src/services/comment-service/block-comment-manager.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { DividerBlockModel } from '@blocksuite/affine-model'; -import { DisposableGroup } from '@blocksuite/global/disposable'; -import { - BlockSelection, - LifeCycleWatcher, - TextSelection, -} from '@blocksuite/std'; -import type { BaseSelection, BlockModel } from '@blocksuite/store'; -import { signal } from '@preact/signals-core'; - -import { getSelectedBlocksCommand } from '../../commands'; -import { ImageSelection } from '../../selection'; -import { matchModels } from '../../utils'; -import { type CommentId, CommentProviderIdentifier } from './comment-provider'; -import { findCommentedBlocks } from './utils'; - -export class BlockCommentManager extends LifeCycleWatcher { - static override key = 'block-comment-manager'; - - private readonly _highlightedCommentId$ = signal(null); - - private readonly _disposables = new DisposableGroup(); - - private get _provider() { - return this.std.getOptional(CommentProviderIdentifier); - } - - isBlockCommentHighlighted( - block: BlockModel<{ comments?: Record }> - ) { - const comments = block.props.comments; - if (!comments) return false; - return ( - this._highlightedCommentId$.value !== null && - Object.keys(comments).includes(this._highlightedCommentId$.value) - ); - } - - override mounted() { - const provider = this._provider; - if (!provider) return; - - this._disposables.add(provider.onCommentAdded(this._handleAddComment)); - this._disposables.add( - provider.onCommentDeleted(this.handleDeleteAndResolve) - ); - this._disposables.add( - provider.onCommentResolved(this.handleDeleteAndResolve) - ); - this._disposables.add( - provider.onCommentHighlighted(this._handleHighlightComment) - ); - } - - override unmounted() { - this._disposables.dispose(); - } - - private readonly _handleAddComment = ( - id: CommentId, - selections: BaseSelection[] - ) => { - const blocksFromTextRange = selections - .filter((s): s is TextSelection => s.is(TextSelection)) - .map(s => { - const [_, { selectedBlocks }] = this.std.command.exec( - getSelectedBlocksCommand, - { - textSelection: s, - } - ); - if (!selectedBlocks) return []; - return selectedBlocks.map(b => b.model).filter(m => !m.text); - }); - - const needCommentBlocks = [ - ...blocksFromTextRange.flat(), - ...selections - .filter(s => s instanceof BlockSelection || s instanceof ImageSelection) - .map(({ blockId }) => this.std.store.getModelById(blockId)) - .filter( - (m): m is BlockModel => - m !== null && !matchModels(m, [DividerBlockModel]) - ), - ]; - - if (needCommentBlocks.length === 0) return; - - this.std.store.withoutTransact(() => { - needCommentBlocks.forEach(block => { - const comments = ( - 'comments' in block.props && - typeof block.props.comments === 'object' && - block.props.comments !== null - ? block.props.comments - : {} - ) as Record; - - this.std.store.updateBlock(block, { - comments: { [id]: true, ...comments }, - }); - }); - }); - }; - - readonly handleDeleteAndResolve = (id: CommentId) => { - const commentedBlocks = findCommentedBlocks(this.std.store, id); - this.std.store.withoutTransact(() => { - commentedBlocks.forEach(block => { - delete block.props.comments[id]; - }); - }); - }; - - private readonly _handleHighlightComment = (id: CommentId | null) => { - this._highlightedCommentId$.value = id; - }; -} diff --git a/blocksuite/affine/shared/src/services/comment-service/block-element-comment-manager.ts b/blocksuite/affine/shared/src/services/comment-service/block-element-comment-manager.ts new file mode 100644 index 0000000000..5a1623644a --- /dev/null +++ b/blocksuite/affine/shared/src/services/comment-service/block-element-comment-manager.ts @@ -0,0 +1,169 @@ +import { DividerBlockModel } from '@blocksuite/affine-model'; +import { DisposableGroup } from '@blocksuite/global/disposable'; +import { + BlockSelection, + LifeCycleWatcher, + SurfaceSelection, + TextSelection, +} from '@blocksuite/std'; +import { + GfxControllerIdentifier, + type GfxModel, + type GfxPrimitiveElementModel, +} from '@blocksuite/std/gfx'; +import type { BaseSelection, BlockModel } from '@blocksuite/store'; +import { signal } from '@preact/signals-core'; + +import { getSelectedBlocksCommand } from '../../commands'; +import { ImageSelection } from '../../selection'; +import { matchModels } from '../../utils'; +import { type CommentId, CommentProviderIdentifier } from './comment-provider'; +import { findCommentedBlocks, findCommentedElements } from './utils'; + +export class BlockElementCommentManager extends LifeCycleWatcher { + static override key = 'block-element-comment-manager'; + + private readonly _highlightedCommentId$ = signal(null); + + private readonly _disposables = new DisposableGroup(); + + private get _provider() { + return this.std.getOptional(CommentProviderIdentifier); + } + + isBlockCommentHighlighted( + block: BlockModel<{ comments?: Record }> + ) { + const comments = block.props.comments; + if (!comments) return false; + return ( + this._highlightedCommentId$.value !== null && + Object.keys(comments).includes(this._highlightedCommentId$.value) + ); + } + + isElementCommentHighlighted(element: GfxPrimitiveElementModel) { + const comments = element.comments; + if (!comments) return false; + return ( + this._highlightedCommentId$.value !== null && + Object.keys(comments).includes(this._highlightedCommentId$.value) + ); + } + + override mounted() { + const provider = this._provider; + if (!provider) return; + + this._disposables.add(provider.onCommentAdded(this._handleAddComment)); + this._disposables.add( + provider.onCommentDeleted(this.handleDeleteAndResolve) + ); + this._disposables.add( + provider.onCommentResolved(this.handleDeleteAndResolve) + ); + this._disposables.add( + provider.onCommentHighlighted(this._handleHighlightComment) + ); + } + + override unmounted() { + this._disposables.dispose(); + } + + private readonly _handleAddComment = ( + id: CommentId, + selections: BaseSelection[] + ) => { + // get blocks from text range that some no-text blocks are selected such as image, bookmark, etc. + const noTextBlocksFromTextRange = selections + .filter((s): s is TextSelection => s.is(TextSelection)) + .flatMap(s => { + const [_, { selectedBlocks }] = this.std.command.exec( + getSelectedBlocksCommand, + { + textSelection: s, + } + ); + if (!selectedBlocks) return []; + return selectedBlocks.map(b => b.model).filter(m => !m.text); + }); + + const blocksFromBlockSelection = selections + .filter(s => s instanceof BlockSelection || s instanceof ImageSelection) + .map(({ blockId }) => this.std.store.getModelById(blockId)) + .filter( + (m): m is BlockModel => + m !== null && !matchModels(m, [DividerBlockModel]) + ); + + const needCommentBlocks = [ + ...noTextBlocksFromTextRange, + ...blocksFromBlockSelection, + ]; + + if (needCommentBlocks.length !== 0) { + this.std.store.withoutTransact(() => { + needCommentBlocks.forEach(block => { + const comments = ( + 'comments' in block.props && + typeof block.props.comments === 'object' && + block.props.comments !== null + ? block.props.comments + : {} + ) as Record; + + this.std.store.updateBlock(block, { + comments: { [id]: true, ...comments }, + }); + }); + }); + } + + const gfx = this.std.get(GfxControllerIdentifier); + const elementsFromSurfaceSelection = selections + .filter(s => s instanceof SurfaceSelection) + .flatMap(({ blockId, elements }) => { + if (blockId !== gfx.surface?.id) return []; + return elements + .map(id => gfx.getElementById(id)) + .filter(m => m !== null); + }); + if (elementsFromSurfaceSelection.length !== 0) { + this.std.store.withoutTransact(() => { + elementsFromSurfaceSelection.forEach(element => { + const comments = + 'comments' in element && + typeof element.comments === 'object' && + element.comments !== null + ? element.comments + : {}; + + gfx.updateElement(element, { + comments: { [id]: true, ...comments }, + }); + }); + }); + } + }; + + readonly handleDeleteAndResolve = (id: CommentId) => { + const commentedBlocks = findCommentedBlocks(this.std.store, id); + this.std.store.withoutTransact(() => { + commentedBlocks.forEach(block => { + delete block.props.comments[id]; + }); + }); + + const commentedElements = findCommentedElements(this.std.store, id); + this.std.store.withoutTransact(() => { + commentedElements.forEach(element => { + delete element.comments[id]; + }); + }); + }; + + private readonly _handleHighlightComment = (id: CommentId | null) => { + this._highlightedCommentId$.value = id; + }; +} diff --git a/blocksuite/affine/shared/src/services/comment-service/index.ts b/blocksuite/affine/shared/src/services/comment-service/index.ts index f1eeb78bb1..b56be60956 100644 --- a/blocksuite/affine/shared/src/services/comment-service/index.ts +++ b/blocksuite/affine/shared/src/services/comment-service/index.ts @@ -1,3 +1,3 @@ -export * from './block-comment-manager'; +export * from './block-element-comment-manager'; export * from './comment-provider'; export * from './utils'; diff --git a/blocksuite/affine/shared/src/services/comment-service/utils.ts b/blocksuite/affine/shared/src/services/comment-service/utils.ts index 2aeeb5d9e2..9ee16b2bd9 100644 --- a/blocksuite/affine/shared/src/services/comment-service/utils.ts +++ b/blocksuite/affine/shared/src/services/comment-service/utils.ts @@ -1,6 +1,10 @@ import { CommentIcon } from '@blocksuite/icons/lit'; -import { BlockSelection } from '@blocksuite/std'; -import type { BlockModel, Store } from '@blocksuite/store'; +import { BlockSelection, SurfaceSelection } from '@blocksuite/std'; +import type { + GfxPrimitiveElementModel, + SurfaceBlockModel, +} from '@blocksuite/std/gfx'; +import { BlockModel, type Store } from '@blocksuite/store'; import type { ToolbarAction } from '../toolbar-service'; import { type CommentId, CommentProviderIdentifier } from './comment-provider'; @@ -22,6 +26,31 @@ export function findCommentedBlocks(store: Store, commentId: CommentId) { }); } +export function findAllCommentedElements(store: Store) { + type CommentedElement = GfxPrimitiveElementModel & { + comments: Record; + }; + const surface = store.getModelsByFlavour('affine:surface')[0] as + | SurfaceBlockModel + | undefined; + if (!surface) return []; + + return surface.elementModels.filter( + (element): element is CommentedElement => { + return ( + element.comments !== undefined && + Object.keys(element.comments).length > 0 + ); + } + ); +} + +export function findCommentedElements(store: Store, commentId: CommentId) { + return findAllCommentedElements(store).filter(element => { + return element.comments[commentId]; + }); +} + export const blockCommentToolbarButton: Omit = { tooltip: 'Comment', when: ({ std }) => !!std.getOptional(CommentProviderIdentifier), @@ -29,22 +58,26 @@ export const blockCommentToolbarButton: Omit = { run: ctx => { const commentProvider = ctx.std.getOptional(CommentProviderIdentifier); if (!commentProvider) return; - const selections = ctx.selection.value; + const selections = ctx.selection.value; const model = ctx.getCurrentModel(); - if (selections.length > 1) { + // may be hover on a block or element, in this case + // the selection is empty, so we need to get the current model + if (model && selections.length === 0) { + if (model instanceof BlockModel) { + commentProvider.addComment([ + new BlockSelection({ + blockId: model.id, + }), + ]); + } else if (ctx.gfx.surface?.id) { + commentProvider.addComment([ + new SurfaceSelection(ctx.gfx.surface.id, [model.id], false), + ]); + } + } else if (selections.length > 0) { commentProvider.addComment(selections); - } else if (model) { - commentProvider.addComment([ - new BlockSelection({ - blockId: model.id, - }), - ]); - } else if (selections.length === 1) { - commentProvider.addComment(selections); - } else { - return; } }, }; diff --git a/blocksuite/framework/std/src/gfx/model/surface/element-model.ts b/blocksuite/framework/std/src/gfx/model/surface/element-model.ts index 5ba9ffaa24..2f02613461 100644 --- a/blocksuite/framework/std/src/gfx/model/surface/element-model.ts +++ b/blocksuite/framework/std/src/gfx/model/surface/element-model.ts @@ -52,6 +52,7 @@ export type BaseElementProps = { index: string; seed: number; lockedBySelf?: boolean; + comments?: Record; }; export type SerializedElement = Record & { @@ -60,6 +61,7 @@ export type SerializedElement = Record & { id: string; index: string; lockedBySelf?: boolean; + comments?: Record; props: Record; }; export abstract class GfxPrimitiveElementModel< @@ -372,6 +374,9 @@ export abstract class GfxPrimitiveElementModel< @field() accessor seed!: number; + + @field() + accessor comments: Record | undefined = undefined; } export abstract class GfxGroupLikeElementModel< diff --git a/blocksuite/playground/apps/starter/utils/extensions.ts b/blocksuite/playground/apps/starter/utils/extensions.ts index 1c4fc84bb5..886e0f52ad 100644 --- a/blocksuite/playground/apps/starter/utils/extensions.ts +++ b/blocksuite/playground/apps/starter/utils/extensions.ts @@ -33,6 +33,7 @@ export function getTestCommonExtensions( di.override(DocModeProvider, mockDocModeService(editor)); }, }, + // CommentProviderExtension(mockCommentProvider()), ]; } diff --git a/packages/frontend/core/src/blocksuite/view-extensions/comment/comment-provider.ts b/packages/frontend/core/src/blocksuite/view-extensions/comment/comment-provider.ts index c0f1c10d34..7fd2a0024e 100644 --- a/packages/frontend/core/src/blocksuite/view-extensions/comment/comment-provider.ts +++ b/packages/frontend/core/src/blocksuite/view-extensions/comment/comment-provider.ts @@ -5,8 +5,18 @@ import { CommentProviderIdentifier } from '@blocksuite/affine/shared/services'; import type { BlockStdScope } from '@blocksuite/affine/std'; import { StdIdentifier } from '@blocksuite/affine/std'; import type { BaseSelection, ExtensionType } from '@blocksuite/affine/store'; +import { ImageSelection } from '@blocksuite/affine-shared/selection'; import { type Container } from '@blocksuite/global/di'; -import { BlockSelection, TextSelection } from '@blocksuite/std'; +import { + BlockSelection, + SurfaceSelection, + TextSelection, +} from '@blocksuite/std'; +import { + GfxBlockElementModel, + GfxControllerIdentifier, + GfxPrimitiveElementModel, +} from '@blocksuite/std/gfx'; import type { FrameworkProvider } from '@toeverything/infra'; import { DocCommentManagerService } from '../../../modules/comment/services/doc-comment-manager'; @@ -21,6 +31,8 @@ function getPreviewFromSelections( const previews: string[] = []; + const gfx = std.get(GfxControllerIdentifier); + for (const selection of selections) { if (selection instanceof TextSelection) { // Extract text from TextSelection @@ -35,9 +47,23 @@ function getPreviewFromSelections( const flavour = block.model.flavour.replace('affine:', ''); previews.push(`<${flavour}>`); } - } else if (selection.type === 'image') { + } else if (selection instanceof ImageSelection) { // Return <"Image"> for ImageSelection previews.push(''); + } else if ( + selection instanceof SurfaceSelection && + gfx.surface?.id === selection.blockId + ) { + selection.elements.forEach(elementId => { + const model = gfx.getElementById(elementId); + if (model instanceof GfxPrimitiveElementModel) { + const flavour = model.type.replace('affine:', ''); + previews.push(`<${flavour}>`); + } else if (model instanceof GfxBlockElementModel) { + const flavour = model.flavour.replace('affine:', ''); + previews.push(`<${flavour}>`); + } + }); } // Skip other types } diff --git a/packages/frontend/core/src/modules/editor/entities/editor.ts b/packages/frontend/core/src/modules/editor/entities/editor.ts index 0b66e73f60..349e5f752c 100644 --- a/packages/frontend/core/src/modules/editor/entities/editor.ts +++ b/packages/frontend/core/src/modules/editor/entities/editor.ts @@ -9,6 +9,7 @@ import { HighlightSelection } from '@blocksuite/affine/shared/selection'; import { DocModeProvider, findCommentedBlocks, + findCommentedElements, } from '@blocksuite/affine/shared/services'; import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx'; import type { InlineEditor } from '@blocksuite/std/inline'; @@ -231,6 +232,12 @@ export class Editor extends Entity { if (blockCommentedBlocks.length > 0) { finalId = blockCommentedBlocks[0].id; finalKey = 'blockIds'; + } else { + const commentedElements = findCommentedElements(std.store, commentId); + if (commentedElements.length > 0) { + finalId = commentedElements[0].id; + finalKey = 'elementIds'; + } } } // Workaround: clear selection to avoid comment editor flickering