diff --git a/blocksuite/affine/block-note/src/note-edgeless-block.ts b/blocksuite/affine/block-note/src/note-edgeless-block.ts index 1f3405edf2..83e0c92c09 100644 --- a/blocksuite/affine/block-note/src/note-edgeless-block.ts +++ b/blocksuite/affine/block-note/src/note-edgeless-block.ts @@ -245,10 +245,12 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent( } } - private _isFirstNote() { + private _isFirstVisibleNote() { return ( - this.model.parent?.children.find(child => - matchFlavours(child, ['affine:note']) + this.model.parent?.children.find( + child => + matchFlavours(child, ['affine:note']) && + child.displayMode !== NoteDisplayMode.EdgelessOnly ) === this.model ); } @@ -509,7 +511,8 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent( .editing=${this._editing} > - ${isCollapsable && (!this._isFirstNote() || !this._enablePageHeader) + ${isCollapsable && + (!this._isFirstVisibleNote() || !this._enablePageHeader) ? html`
@@ -354,17 +340,17 @@ export class OutlinePanelBody extends SignalWatcher( ${this._pageVisibleNotes.length ? repeat( this._pageVisibleNotes, - note => note.note.id, - (note, idx) => html` + item => item.note.id, + (item, idx) => html` note.note.id, - (note, idx) => + (item, idx) => html`` )} ` @@ -447,31 +437,23 @@ export class OutlinePanelBody extends SignalWatcher( this._lockActiveHeadingId = false; } + private readonly _selectedNotes$ = signal([]); + private _selectNote(e: SelectEvent) { const { selected, id, multiselect } = e.detail; + let selectedNotes = this._selectedNotes$.peek(); + if (!selected) { - this._selected = this._selected.filter(noteId => noteId !== id); + selectedNotes = selectedNotes.filter(noteId => noteId !== id); } else if (multiselect) { - this._selected = [...this._selected, id]; + selectedNotes = [...selectedNotes, id]; } else { - this._selected = [id]; + selectedNotes = [id]; } - // When edgeless mode, should select notes which display in both mode - const selectedIds = this._pageVisibleNotes.reduce((ids, item) => { - const note = item.note; - if ( - this._selected.includes(note.id) && - (!note.displayMode || - note.displayMode === NoteDisplayMode.DocAndEdgeless) - ) { - ids.push(note.id); - } - return ids; - }, [] as string[]); this.edgeless?.service.selection.set({ - elements: selectedIds, + elements: selectedNotes, editing: false, }); } @@ -537,25 +519,6 @@ export class OutlinePanelBody extends SignalWatcher( return; } - const oldSelectedSet = this._selected.reduce((pre, id) => { - pre.add(id); - return pre; - }, new Set()); - const newSelected: string[] = []; - - rootModel.children.forEach(block => { - if (!BlocksUtils.matchFlavours(block, ['affine:note'])) return; - - const blockModel = block as NoteBlockModel; - - if ( - blockModel.displayMode !== NoteDisplayMode.EdgelessOnly && - oldSelectedSet.has(block.id) - ) { - newSelected.push(block.id); - } - }); - this._pageVisibleNotes = getNotesFromDoc(this.doc, [ NoteDisplayMode.DocAndEdgeless, NoteDisplayMode.DocOnly, @@ -563,7 +526,6 @@ export class OutlinePanelBody extends SignalWatcher( this._edgelessOnlyNotes = getNotesFromDoc(this.doc, [ NoteDisplayMode.EdgelessOnly, ]); - this._selected = newSelected; } private _updateNoticeVisibility() { @@ -583,24 +545,38 @@ export class OutlinePanelBody extends SignalWatcher( } } - private _zoomToFit() { - const edgeless = this.edgeless; + private _watchSelectedNotes() { + this.disposables.add( + effect(() => { + const { std, doc, mode } = this.editor; - if (!edgeless) return; + const currSelectedNotes = + mode === 'edgeless' + ? std.selection + .filter(SurfaceSelection) + .filter(({ blockId }) => { + const model = doc.getBlock(blockId)?.model; + return !!model && matchFlavours(model, ['affine:note']); + }) + .map(({ blockId }) => blockId) + : (std.command.exec('getSelectedModels').selectedModels ?? []) + .map(model => { + let parent = model.parent; + while (parent && !matchFlavours(parent, ['affine:note'])) { + parent = parent.parent; + } + return parent ? [parent.id] : []; + }) + .flat(); - const bound = edgeless.gfx.elementsBound; - - this._oldViewport = { - zoom: edgeless.service.viewport.zoom, - center: { - x: edgeless.service.viewport.center.x, - y: edgeless.service.viewport.center.y, - }, - }; - edgeless.service.viewport.setViewportByBound( - new Bound(bound.x, bound.y, bound.w, bound.h), - this.viewportPadding, - true + const preSelected = this._selectedNotes$.peek(); + if ( + preSelected.length !== currSelectedNotes.length || + preSelected.some(id => !currSelectedNotes.includes(id)) + ) { + this._selectedNotes$.value = currSelectedNotes; + } + }) ); } @@ -615,29 +591,16 @@ export class OutlinePanelBody extends SignalWatcher( } ) ); + this._watchSelectedNotes(); } override disconnectedCallback(): void { super.disconnectedCallback(); - - if (!this._changedFlag && this._oldViewport) { - const edgeless = this.edgeless; - - if (!edgeless) return; - - edgeless.service.viewport.setViewport( - this._oldViewport.zoom, - [this._oldViewport.center.x, this._oldViewport.center.y], - true - ); - } - this._clearDocDisposables(); this._clearHighlightMask(); } override firstUpdated(): void { - this.disposables.addFromEvent(this, 'click', this._clickHandler); this.disposables.addFromEvent(this, 'dblclick', this._doubleClickHandler); } @@ -667,15 +630,8 @@ export class OutlinePanelBody extends SignalWatcher( this._setDocDisposables(); } - if ( - _changedProperties.has('mode') && - this.edgeless && - this._isEdgelessMode() - ) { + if (_changedProperties.has('edgeless')) { this._clearHighlightMask(); - if (_changedProperties.get('mode') === undefined) return; - - requestAnimationFrame(() => this._zoomToFit()); } } @@ -688,12 +644,6 @@ export class OutlinePanelBody extends SignalWatcher( @state() private accessor _pageVisibleNotes: OutlineNoteItem[] = []; - /** - * store the id of selected notes - */ - @state() - private accessor _selected: string[] = []; - @property({ attribute: false }) accessor doc!: Store; @@ -707,7 +657,7 @@ export class OutlinePanelBody extends SignalWatcher( accessor editor!: AffineEditorContainer; @property({ attribute: false }) - accessor enableNotesSorting!: boolean; + accessor enableNotesSorting: boolean = false; @property({ attribute: false }) accessor fitPadding!: number[]; diff --git a/blocksuite/presets/src/fragments/outline/card/outline-card.ts b/blocksuite/presets/src/fragments/outline/card/outline-card.ts index 97a0f02926..7ebd2dd10e 100644 --- a/blocksuite/presets/src/fragments/outline/card/outline-card.ts +++ b/blocksuite/presets/src/fragments/outline/card/outline-card.ts @@ -143,11 +143,13 @@ const styles = css` color: var(--affine-text-primary-color); } - .card-preview.edgeless .card-content:hover { + .card-preview .card-content:hover { cursor: pointer; } - .card-preview.edgeless .card-header-container:hover { + .card-container[data-invisible='false'] + .card-preview + .card-header-container:hover { cursor: grab; } @@ -156,11 +158,11 @@ const styles = css` opacity: 0.5; } - .card-container.selected .card-preview.edgeless { + .card-container.selected .card-preview { background: var(--affine-hover-color); } - .card-container.placeholder .card-preview.edgeless { + .card-container.placeholder .card-preview { background: var(--affine-hover-color); opacity: 0.9; } @@ -178,7 +180,7 @@ const styles = css` pointer-events: none; } - .card-preview.page outline-block-preview:hover { + .card-preview outline-block-preview:hover { color: var(--affine-brand-color); } `; diff --git a/blocksuite/presets/src/fragments/outline/outline-panel.ts b/blocksuite/presets/src/fragments/outline/outline-panel.ts index 122ffde3b2..c1324756cf 100644 --- a/blocksuite/presets/src/fragments/outline/outline-panel.ts +++ b/blocksuite/presets/src/fragments/outline/outline-panel.ts @@ -1,6 +1,7 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { effect } from '@preact/signals-core'; import { baseTheme } from '@toeverything/theme'; -import { css, html, LitElement, unsafeCSS } from 'lit'; +import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit'; import { property, state } from 'lit/decorators.js'; import type { AffineEditorContainer } from '../../editors/editor-container.js'; @@ -112,7 +113,25 @@ export class OutlinePanel extends SignalWatcher(WithDisposable(LitElement)) { override connectedCallback() { super.connectedCallback(); - this._loadSettingsFromLocalStorage(); + this.disposables.add( + effect(() => { + if (this.editor.mode === 'edgeless') { + this._enableNotesSorting = true; + } else { + this._loadSettingsFromLocalStorage(); + } + }) + ); + } + + override willUpdate(_changedProperties: PropertyValues): void { + if (_changedProperties.has('editor')) { + if (this.editor.mode === 'edgeless') { + this._enableNotesSorting = true; + } else { + this._loadSettingsFromLocalStorage(); + } + } } override render() { diff --git a/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts b/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts index e60539de43..981d2d8c6c 100644 --- a/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts +++ b/blocksuite/tests-legacy/fragments/outline/outline-panel.spec.ts @@ -44,20 +44,14 @@ test.describe('toc-panel', () => { return panel.locator(`affine-outline-panel-body .title`); } - async function toggleNoteSorting(page: Page) { - const enableSortingButton = page.locator( - '.outline-panel-header-container .note-sorting-button' - ); - await enableSortingButton.click(); - } - async function dragNoteCard(page: Page, fromCard: Locator, toCard: Locator) { const fromRect = await fromCard.boundingBox(); const toRect = await toCard.boundingBox(); await page.mouse.move(fromRect!.x + 10, fromRect!.y + 10); + await page.mouse.click(fromRect!.x + 10, fromRect!.y + 10); await page.mouse.down(); - await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 10 }); + await page.mouse.move(toRect!.x + 5, toRect!.y + 5, { steps: 20 }); await page.mouse.up(); } @@ -267,7 +261,6 @@ test.describe('toc-panel', () => { await page.mouse.click(100, 100); await toggleTocPanel(page); - await toggleNoteSorting(page); const docVisibleCard = page.locator( '.card-container[data-invisible="false"]' ); @@ -311,7 +304,6 @@ test.describe('toc-panel', () => { ); await toggleTocPanel(page); - await toggleNoteSorting(page); const docVisibleCard = page.locator( '.card-container[data-invisible="false"]' ); @@ -345,7 +337,6 @@ test.describe('toc-panel', () => { ); await toggleTocPanel(page); - await toggleNoteSorting(page); const docVisibleCard = page.locator( '.card-container[data-invisible="false"]' ); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx index b601d32d0a..18a6efda1f 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx @@ -8,7 +8,11 @@ import { WorkspaceService } from '@affine/core/modules/workspace'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx'; -import { matchFlavours, type NoteBlockModel } from '@blocksuite/affine/blocks'; +import { + matchFlavours, + type NoteBlockModel, + NoteDisplayMode, +} from '@blocksuite/affine/blocks'; import { Bound } from '@blocksuite/affine/global/utils'; import { ExpandFullIcon, @@ -160,12 +164,14 @@ export const EdgelessNoteHeader = ({ note }: { note: NoteBlockModel }) => { if (!flags.enable_page_block_header) return null; - const isFirstNote = - note.parent?.children.find(child => - matchFlavours(child, ['affine:note']) + const isFirstVisibleNote = + note.parent?.children.find( + child => + matchFlavours(child, ['affine:note']) && + child.displayMode === NoteDisplayMode.DocAndEdgeless ) === note; - if (!isFirstNote) return null; + if (!isFirstVisibleNote) return null; return (
diff --git a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts index 59aeba6f2a..a3adeadbb1 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts @@ -229,7 +229,11 @@ test.describe('edgeless note element toolbar', () => { await displayInPage.click(); await viewTocButton.click(); - await page.waitForSelector('affine-outline-panel'); - expect(page.locator('affine-outline-panel')).toBeVisible(); + const toc = page.locator('affine-outline-panel'); + await toc.waitFor({ state: 'visible' }); + const highlightNoteCards = toc.locator( + 'affine-outline-note-card > .selected' + ); + expect(highlightNoteCards).toHaveCount(1); }); }); diff --git a/tests/affine-local/e2e/blocksuite/outline.spec.ts b/tests/affine-local/e2e/blocksuite/outline.spec.ts index ad2bbafb97..761422780c 100644 --- a/tests/affine-local/e2e/blocksuite/outline.spec.ts +++ b/tests/affine-local/e2e/blocksuite/outline.spec.ts @@ -2,15 +2,25 @@ import { test } from '@affine-test/kit/playwright'; import { clickEdgelessModeButton, clickPageModeButton, + clickView, + createEdgelessNoteBlock, + locateElementToolbar, + locateModeSwitchButton, } from '@affine-test/kit/utils/editor'; +import { + pressEnter, + selectAllByKeyboard, +} from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; import { clickNewPageButton, createLinkedPage, getBlockSuiteEditorTitle, + type, waitForEditorLoad, waitForEmptyEditor, } from '@affine-test/kit/utils/page-logic'; +import { openRightSideBar } from '@affine-test/kit/utils/sidebar'; import { expect, type Locator, type Page } from '@playwright/test'; function getIndicators(container: Page | Locator) { @@ -156,3 +166,66 @@ test('outline viewer should be useable in doc peek preview', async ({ await expect(page.locator('affine-outline-panel')).toBeVisible(); } }); + +test('visibility sorting should be enabled in edgeless mode and disabled in page mode by default, and can be changed', async ({ + page, +}) => { + await openHomePage(page); + await clickNewPageButton(page); + await waitForEditorLoad(page); + await pressEnter(page); + await type(page, '# Heading 1'); + await openRightSideBar(page, 'outline'); + + const toc = page.locator('affine-outline-panel'); + const sortingButton = toc.locator('.note-sorting-button'); + await expect(sortingButton).not.toHaveClass(/active/); + expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); + + await clickEdgelessModeButton(page); + await expect(sortingButton).toHaveClass(/active/); + expect(toc.locator('[data-sortable="true"]')).toHaveCount(1); + + await sortingButton.click(); + await expect(sortingButton).not.toHaveClass(/active/); + expect(toc.locator('[data-sortable="false"]')).toHaveCount(1); +}); + +test('note cards of TOC should be highlight when selections contains the corresponding notes', async ({ + page, +}) => { + await openHomePage(page); + await clickNewPageButton(page); + await locateModeSwitchButton(page, 'edgeless').click(); + await waitForEditorLoad(page); + await openRightSideBar(page, 'outline'); + + const toc = page.locator('affine-outline-panel'); + const highlightNoteCards = toc.locator( + 'affine-outline-note-card > .selected' + ); + + await expect(highlightNoteCards).toHaveCount(0); + + await clickView(page, [0, 0]); + await selectAllByKeyboard(page); + await expect(highlightNoteCards).toHaveCount(1); + + await createEdgelessNoteBlock(page, [100, 100]); + await expect(highlightNoteCards).toHaveCount(1); + + await clickView(page, [200, 200]); + await selectAllByKeyboard(page); + await expect(highlightNoteCards).toHaveCount(2); + + await clickView(page, [100, 100]); + const toolbar = locateElementToolbar(page); + await toolbar.getByTestId('display-in-page').click(); + await clickPageModeButton(page); + await page.keyboard.press('ArrowDown'); + await expect(highlightNoteCards).toHaveCount(1); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await selectAllByKeyboard(page); + await expect(highlightNoteCards).toHaveCount(2); +}); diff --git a/tests/kit/src/utils/sidebar.ts b/tests/kit/src/utils/sidebar.ts index 1bff4e5dbd..26c99618a2 100644 --- a/tests/kit/src/utils/sidebar.ts +++ b/tests/kit/src/utils/sidebar.ts @@ -19,3 +19,11 @@ export async function clickSideBarUseAvatar(page: Page) { export async function clickNewPageButton(page: Page) { return page.getByTestId('sidebar-new-page-button').click(); } + +export async function openRightSideBar( + page: Page, + tab?: 'chat' | 'properties' | 'journal' | 'outline' | 'frame' +) { + await page.getByTestId('right-sidebar-toggle').click(); + tab && (await page.getByTestId(`sidebar-tab-${tab}`).click()); +}