());
- 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());
+}