From fec698fd8b06fc954cff5f012004ef2fc5af1814 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Mon, 31 Mar 2025 12:35:02 +0000 Subject: [PATCH] chore(editor): improve selection of doc in canvas (#11314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [BS-2705](https://linear.app/affine-design/issue/BS-2705/[improvement]-ι€šθΏ‡-viability-control-选择-hide-in-edgeless) This PR disabled selecting operation of notes that are only shown in page mode. --- .../src/edgeless/gfx-tool/default-tool.ts | 8 ++ .../src/body/outline-panel-body.ts | 83 +++++++++++++------ .../blocksuite/outline/outline-panel.spec.ts | 9 +- .../blocksuite/e2e/edgeless/note/note.spec.ts | 35 ++++++++ 4 files changed, 107 insertions(+), 28 deletions(-) diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts index af91be6345..133ff8c4f1 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts @@ -10,6 +10,8 @@ import { type ConnectorElementModel, GroupElementModel, MindmapElementModel, + NoteBlockModel, + NoteDisplayMode, } from '@blocksuite/affine-model'; import { resetNativeSelection } from '@blocksuite/affine-shared/utils'; import { DisposableGroup } from '@blocksuite/global/disposable'; @@ -141,6 +143,12 @@ export class DefaultTool extends BaseTool { if (el instanceof MindmapElementModel) { return bound.contains(el.elementBound); } + if ( + el instanceof NoteBlockModel && + el.props.displayMode === NoteDisplayMode.DocOnly + ) { + return false; + } return true; }); diff --git a/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts b/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts index 49347cc394..2c5fd2e733 100644 --- a/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts +++ b/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts @@ -12,7 +12,13 @@ import { import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import type { BlockModel } from '@blocksuite/store'; import { consume } from '@lit/context'; -import { effect, signal } from '@preact/signals-core'; +import { + batch, + computed, + effect, + type Signal, + signal, +} from '@preact/signals-core'; import { html, nothing } from 'lit'; import { query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -53,7 +59,22 @@ export class OutlinePanelBody extends SignalWatcher( private readonly _edgelessOnlyNotes$ = signal([]); - private readonly _selectedNotes$ = signal([]); + private readonly _selectedNotes$: Record< + NoteDisplayMode, + Signal + > = { + [NoteDisplayMode.DocOnly]: signal([]), + [NoteDisplayMode.DocAndEdgeless]: signal([]), + [NoteDisplayMode.EdgelessOnly]: signal([]), + }; + + private readonly _allSelectedNotes$ = computed(() => + [ + NoteDisplayMode.DocAndEdgeless, + NoteDisplayMode.DocOnly, + NoteDisplayMode.EdgelessOnly, + ].flatMap(mode => this._selectedNotes$[mode].value) + ); private _clearHighlightMask = () => {}; @@ -136,7 +157,7 @@ export class OutlinePanelBody extends SignalWatcher( if (!this.doc.root) return; const pageVisibleNotes = this._pageVisibleNotes$.peek(); - const selected = this._selectedNotes$.peek(); + const selected = this._allSelectedNotes$.peek(); const children = this.doc.root.children.slice(); const noteIndex = new Map(); @@ -203,23 +224,39 @@ export class OutlinePanelBody extends SignalWatcher( const note = this.doc.getBlock(id)?.model; if (!note || !matchModels(note, [NoteBlockModel])) return; - let selectedNotes = this._selectedNotes$.peek(); + // map from signal to value + const selectedNotes = Object.fromEntries( + Object.entries(this._selectedNotes$).map(([k, v]) => [k, v.peek()]) + ) as Record; - if (!selected) { - selectedNotes = selectedNotes.filter(_note => _note !== note); - } else if (multiselect) { - selectedNotes = [...selectedNotes, note]; + if (multiselect) { + selectedNotes[note.props.displayMode] = selected + ? [...selectedNotes[note.props.displayMode], note] + : selectedNotes[note.props.displayMode].filter(_note => _note !== note); } else { - selectedNotes = [note]; + selectedNotes[note.props.displayMode] = selected ? [note] : []; + Object.keys(this._selectedNotes$).forEach(mode => { + if (mode !== note.props.displayMode) { + selectedNotes[mode as NoteDisplayMode] = []; + } + }); } + // We use gfx.selection and effect to keep sync between canvas and outline panel if (editorMode === 'edgeless') { gfx.selection.set({ - elements: selectedNotes.map(({ id }) => id), + elements: [...selectedNotes.both, ...selectedNotes.edgeless].map( + ({ id }) => id + ), editing: false, }); + this._selectedNotes$.doc.value = selectedNotes.doc; } else { - this._selectedNotes$.value = selectedNotes; + [NoteDisplayMode.DocOnly, NoteDisplayMode.DocAndEdgeless].forEach( + mode => { + this._selectedNotes$[mode].value = selectedNotes[mode]; + } + ); } } @@ -237,13 +274,16 @@ export class OutlinePanelBody extends SignalWatcher( return !!model && matchModels(model, [NoteBlockModel]); }); - const preSelected = this._selectedNotes$.peek(); - if ( - preSelected.length !== currSelectedNotes.length || - preSelected.some(note => !currSelectedNotes.includes(note)) - ) { - this._selectedNotes$.value = currSelectedNotes; - } + // update selected notes from edgeless selection + batch(() => { + [NoteDisplayMode.DocAndEdgeless, NoteDisplayMode.EdgelessOnly].forEach( + mode => { + this._selectedNotes$[mode].value = currSelectedNotes.filter( + note => note.props.displayMode === mode + ); + } + ); + }); }); } @@ -280,11 +320,6 @@ export class OutlinePanelBody extends SignalWatcher( std.dnd.monitor({ onDragStart: () => { this._dragging$.value = true; - this._selectedNotes$.value = this._selectedNotes$ - .peek() - .filter(note => { - return this._pageVisibleNotes$.value.includes(note); - }); }, onDrag: data => { const target = data.location.current.dropTargets[0]; @@ -377,7 +412,7 @@ export class OutlinePanelBody extends SignalWatcher( index=${index} .note=${note} .activeHeadingId=${this._activeHeadingId$.value} - .status=${this._selectedNotes$.value.includes(note) + .status=${this._allSelectedNotes$.value.includes(note) ? this._dragging$.value ? 'dragging' : 'selected' diff --git a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts index 5b44133186..652cd28d71 100644 --- a/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts @@ -320,9 +320,10 @@ test.describe('TOC and edgeless selection', () => { expect(await getEdgelessSelectedIds(page)).toHaveLength(0); await cards.nth(1).click(); - expect(await getEdgelessSelectedIds(page)).toHaveLength(1); - await cards.nth(1).click(); - expect(await getEdgelessSelectedIds(page)).toHaveLength(0); + expect( + await getEdgelessSelectedIds(page), + 'should not select doc only note' + ).toHaveLength(0); await cards.nth(2).click(); expect(await getEdgelessSelectedIds(page)).toHaveLength(1); @@ -333,7 +334,7 @@ test.describe('TOC and edgeless selection', () => { await cards.nth(1).click({ modifiers: ['Shift'] }); await cards.nth(2).click({ modifiers: ['Shift'] }); - expect(await getEdgelessSelectedIds(page)).toHaveLength(3); + expect(await getEdgelessSelectedIds(page)).toHaveLength(2); }); test('should select note cards when select note blocks in canvas', async ({ diff --git a/tests/blocksuite/e2e/edgeless/note/note.spec.ts b/tests/blocksuite/e2e/edgeless/note/note.spec.ts index aa91278864..bb601b223c 100644 --- a/tests/blocksuite/e2e/edgeless/note/note.spec.ts +++ b/tests/blocksuite/e2e/edgeless/note/note.spec.ts @@ -7,6 +7,9 @@ import { assertEdgelessTool, changeEdgelessNoteBackground, changeNoteDisplayMode, + dragBetweenViewCoords, + getSelectedBound, + getSelectedBoundCount, locatorComponentToolbar, locatorEdgelessZoomToolButton, selectNoteInEdgeless, @@ -19,6 +22,7 @@ import { import { click, clickBlockById, + clickView, dragBetweenCoords, dragBetweenIndices, enterPlaygroundRoom, @@ -31,6 +35,7 @@ import { pressBackspace, pressEnter, pressTab, + selectAllByKeyboard, type, undoByKeyboard, waitForInlineEditorStateUpdated, @@ -541,3 +546,33 @@ test('select text cross blocks in edgeless note', async ({ page }) => { await pressBackspace(page); await assertRichTexts(page, ['ac']); }); + +test('should not select doc only note', async ({ page }) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + await switchEditorMode(page); + await clickView(page, [0, 0]); + + await selectAllByKeyboard(page); + const noteBound = await getSelectedBound(page); + + await triggerComponentToolbarAction(page, 'changeNoteDisplayMode'); + await waitNextFrame(page); + await changeNoteDisplayMode(page, NoteDisplayMode.DocOnly); + + await selectAllByKeyboard(page); + expect(await getSelectedBoundCount(page)).toBe(0); + + await clickView(page, [ + 0.5 * (noteBound[0] + noteBound[2]), + 0.5 * (noteBound[1] + noteBound[3]), + ]); + expect(await getSelectedBoundCount(page)).toBe(0); + + await dragBetweenViewCoords( + page, + [noteBound[0] - 10, noteBound[1] - 10], + [noteBound[0] + noteBound[2] + 10, noteBound[1] + noteBound[3] + 10] + ); + expect(await getSelectedBoundCount(page)).toBe(0); +});