fix(editor): add comment entire to inner toolbar (#13304)

Close
[BS-3624](https://linear.app/affine-design/issue/BS-3624/page模式单选图片的时候希望有comment-按钮)




#### PR Dependency Tree


* **PR #13304** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Added a comment button to the image and surface reference block
toolbars for easier commenting.

* **Refactor**
* Simplified array flattening operations across multiple components and
utilities by replacing `.map(...).flat()` with `.flatMap(...)`,
improving code readability and maintainability.

* **Bug Fixes**
* Improved comment creation logic to allow adding comments even when
selections exist.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-29 13:21:56 +08:00
committed by GitHub
parent bd161c54b2
commit 091bac1047
15 changed files with 82 additions and 83 deletions
@@ -1,6 +1,7 @@
import { ImageBlockModel } from '@blocksuite/affine-model'; import { ImageBlockModel } from '@blocksuite/affine-model';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig, type ToolbarModuleConfig,
ToolbarModuleExtension, ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
@@ -49,6 +50,10 @@ const builtinToolbarConfig = {
}); });
}, },
}, },
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',
@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const movedElements = new Set([ const movedElements = new Set([
...selectedElements, ...selectedElements,
...selectedElements ...selectedElements.flatMap(el =>
.map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : [])) isGfxGroupCompatibleModel(el) ? el.descendantElements : []
.flat(), ),
]); ]);
movedElements.forEach(element => { movedElements.forEach(element => {
@@ -5,6 +5,7 @@ import {
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig, type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit'; import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -61,6 +62,10 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show(); surfaceRefBlock.captionElement.show();
}, },
}, },
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
id: 'a.clipboard', id: 'a.clipboard',
placement: ActionPlacement.More, placement: ActionPlacement.More,
@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
ignoreEmpty = false ignoreEmpty = false
) { ) {
const notes = getNotesFromStore(store, modes); const notes = getNotesFromStore(store, modes);
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat(); return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty));
} }
@@ -103,54 +103,52 @@ export class InlineCommentManager extends LifeCycleWatcher {
id: CommentId, id: CommentId,
selections: BaseSelection[] selections: BaseSelection[]
) => { ) => {
const needCommentTexts = selections const needCommentTexts = selections.flatMap(selection => {
.map(selection => { if (!selection.is(TextSelection)) return [];
if (!selection.is(TextSelection)) return []; const [_, { selectedBlocks }] = this.std.command
const [_, { selectedBlocks }] = this.std.command .chain()
.chain() .pipe(getSelectedBlocksCommand, {
.pipe(getSelectedBlocksCommand, { textSelection: selection,
textSelection: selection, })
}) .run();
.run();
if (!selectedBlocks) return []; if (!selectedBlocks) return [];
type MakeRequired<T, K extends keyof T> = T & { type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>; [key in K]: NonNullable<T[key]>;
}; };
return selectedBlocks return selectedBlocks
.map( .map(
({ model }) => ({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const [model, getInlineEditorByModel(this.std, model)] as const
) )
.filter( .filter(
( (
pair pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] => ): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1] !!pair[0].text && !!pair[1]
) )
.map(([model, inlineEditor]) => { .map(([model, inlineEditor]) => {
let from: TextRangePoint; let from: TextRangePoint;
let to: TextRangePoint | null; let to: TextRangePoint | null;
if (model.id === selection.from.blockId) { if (model.id === selection.from.blockId) {
from = selection.from; from = selection.from;
to = null; to = null;
} else if (model.id === selection.to?.blockId) { } else if (model.id === selection.to?.blockId) {
from = selection.to; from = selection.to;
to = null; to = null;
} else { } else {
from = { from = {
blockId: model.id, blockId: model.id,
index: 0, index: 0,
length: model.text.yText.length, length: model.text.yText.length,
}; };
to = null; to = null;
} }
return [new TextSelection({ from, to }), inlineEditor] as const; return [new TextSelection({ from, to }), inlineEditor] as const;
}); });
}) });
.flat();
if (needCommentTexts.length === 0) return; if (needCommentTexts.length === 0) return;
@@ -64,7 +64,7 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
// may be hover on a block or element, in this case // may be hover on a block or element, in this case
// the selection is empty, so we need to get the current model // the selection is empty, so we need to get the current model
if (model && selections.length === 0) { if (model) {
if (model instanceof BlockModel) { if (model instanceof BlockModel) {
commentProvider.addComment([ commentProvider.addComment([
new BlockSelection({ new BlockSelection({
@@ -11,14 +11,12 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
return new DOMRect(); return new DOMRect();
} }
const lockedElementsByFrame = selected const lockedElementsByFrame = selected.flatMap(selectable => {
.map(selectable => { if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) { return selectable.descendantElements;
return selectable.descendantElements; }
} return [];
return []; });
})
.flat();
selected = [...new Set([...selected, ...lockedElementsByFrame])]; selected = [...new Set([...selected, ...lockedElementsByFrame])];
@@ -113,11 +113,9 @@ export class LinkedDocPopover extends SignalWatcher(
} }
private get _flattenActionList() { private get _flattenActionList() {
return this._actionGroup return this._actionGroup.flatMap(group =>
.map(group => group.items.map(item => ({ ...item, groupName: group.name }))
group.items.map(item => ({ ...item, groupName: group.name })) );
)
.flat();
} }
private get _query() { private get _query() {
@@ -142,15 +142,13 @@ export class SlashMenu extends WithDisposable(LitElement) {
// We search first and second layer // We search first and second layer
if (this._filteredItems.length !== 0 && depth >= 1) break; if (this._filteredItems.length !== 0 && depth >= 1) break;
queue = queue queue = queue.flatMap(item => {
.map<typeof queue>(item => { if (isSubMenuItem(item)) {
if (isSubMenuItem(item)) { return item.subMenu;
return item.subMenu; } else {
} else { return [];
return []; }
} });
})
.flat();
depth++; depth++;
} }
@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
return; return;
} }
const elementIds = selections const elementIds = selections.flatMap(s =>
.map(s => (s.editing || s.inoperable ? [] : s.elements)) s.editing || s.inoperable ? [] : s.elements
.flat(); );
const count = elementIds.length; const count = elementIds.length;
const activated = context.activated && Boolean(count); const activated = context.activated && Boolean(count);
@@ -229,8 +229,7 @@ export function renderToolbar(
? module.config.when(context) ? module.config.when(context)
: (module.config.when ?? true) : (module.config.when ?? true)
) )
.map<ToolbarActions>(module => module.config.actions) .flatMap(module => module.config.actions);
.flat();
const combined = combine(actions, context); const combined = combine(actions, context);
@@ -255,8 +255,7 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
); );
return embeddings return embeddings
.map(e => (e.status === 'fulfilled' ? e.value.embeddings : null)) .flatMap(e => (e.status === 'fulfilled' ? e.value.embeddings : null))
.flat()
.filter((v): v is number[] => !!v && Array.isArray(v)); .filter((v): v is number[] => !!v && Array.isArray(v));
} catch (e: any) { } catch (e: any) {
metrics.ai metrics.ai
@@ -19,7 +19,7 @@ import {
} from '../widgets/ai-panel/ai-panel'; } from '../widgets/ai-panel/ai-panel';
export function AiSlashMenuConfigExtension() { export function AiSlashMenuConfigExtension() {
const AIItems = pageAIGroups.map(group => group.items).flat(); const AIItems = pageAIGroups.flatMap(group => group.items);
const iconWrapper = (icon: AIItemConfig['icon']) => { const iconWrapper = (icon: AIItemConfig['icon']) => {
return html`<div style="color: var(--affine-primary-color)"> return html`<div style="color: var(--affine-primary-color)">
@@ -66,7 +66,7 @@ export class Collection extends Entity<{ id: string }> {
}, },
], ],
}) })
.pipe(map(result => result.groups.map(group => group.items).flat())); .pipe(map(result => result.groups.flatMap(group => group.items)));
}) })
); );
} }
@@ -51,8 +51,7 @@ import stickerContent${id} from './stickers/${category}/Content/${sticker}';`,
} }
const importStatements = Object.values(data) const importStatements = Object.values(data)
.map(v => Object.values(v).map(v => v.importStatement)) .flatMap(v => Object.values(v).map(v => v.importStatement))
.flat()
.join('\n'); .join('\n');
const templates = `const templates = { const templates = `const templates = {