fix(editor): toc viewer no update after delete heading in edgeless mode (#12411)
Close [BS-3494](https://linear.app/affine-design/issue/BS-3494/在白板删除note的标题,切回page模式,toc没更新) Other changes: `doc` -> `store` ### Before https://github.com/user-attachments/assets/ddce20b9-eda2-414b-9452-d8d54a811cf1 ### After https://github.com/user-attachments/assets/7124b8a1-9ab4-4e09-b0ff-7ea2cc9613c2 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved synchronization of the outline viewer to ensure updates after editing headings in edgeless mode. - **Tests** - Added an end-to-end test verifying that the outline viewer correctly reflects changes made in edgeless mode. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -7,7 +7,7 @@ import { effect, signal } from '@preact/signals-core';
|
|||||||
import { html, nothing } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
|
|
||||||
import { type TocContext, tocContext } from '../config';
|
import { type TocContext, tocContext } from '../config';
|
||||||
import { getNotesFromDoc } from '../utils/query';
|
import { getNotesFromStore } from '../utils/query';
|
||||||
import * as styles from './outline-notice.css';
|
import * as styles from './outline-notice.css';
|
||||||
|
|
||||||
export const AFFINE_OUTLINE_NOTICE = 'affine-outline-notice';
|
export const AFFINE_OUTLINE_NOTICE = 'affine-outline-notice';
|
||||||
@@ -31,7 +31,7 @@ export class OutlineNotice extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldShowNotice =
|
const shouldShowNotice =
|
||||||
getNotesFromDoc(this._context.editor$.value.store, [
|
getNotesFromStore(this._context.editor$.value.store, [
|
||||||
NoteDisplayMode.DocOnly,
|
NoteDisplayMode.DocOnly,
|
||||||
]).length > 0;
|
]).length > 0;
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import type {
|
|||||||
import type { NoteCardEntity, NoteDropPayload } from '../utils/drag';
|
import type { NoteCardEntity, NoteDropPayload } from '../utils/drag';
|
||||||
import {
|
import {
|
||||||
getHeadingBlocksFromDoc,
|
getHeadingBlocksFromDoc,
|
||||||
getNotesFromDoc,
|
getNotesFromStore,
|
||||||
isHeadingBlock,
|
isHeadingBlock,
|
||||||
} from '../utils/query';
|
} from '../utils/query';
|
||||||
import {
|
import {
|
||||||
@@ -91,7 +91,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
return this._context.editor$.value;
|
return this._context.editor$.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get doc() {
|
private get store() {
|
||||||
return this.editor.store;
|
return this.editor.store;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +154,11 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _moveSelectedNotes(insertIndex: number) {
|
private _moveSelectedNotes(insertIndex: number) {
|
||||||
if (!this.doc.root) return;
|
if (!this.store.root) return;
|
||||||
|
|
||||||
const pageVisibleNotes = this._pageVisibleNotes$.peek();
|
const pageVisibleNotes = this._pageVisibleNotes$.peek();
|
||||||
const selected = this._allSelectedNotes$.peek();
|
const selected = this._allSelectedNotes$.peek();
|
||||||
const children = this.doc.root.children.slice();
|
const children = this.store.root.children.slice();
|
||||||
|
|
||||||
const noteIndex = new Map<NoteBlockModel, number>();
|
const noteIndex = new Map<NoteBlockModel, number>();
|
||||||
children.forEach((block, index) => {
|
children.forEach((block, index) => {
|
||||||
@@ -189,14 +189,14 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
|
|
||||||
const newChildren = [...leftPart, ...selected, ...rightPart];
|
const newChildren = [...leftPart, ...selected, ...rightPart];
|
||||||
|
|
||||||
this.doc.updateBlock(this.doc.root, {
|
this.store.updateBlock(this.store.root, {
|
||||||
children: newChildren,
|
children: newChildren,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _scrollToBlock(blockId: string) {
|
private async _scrollToBlock(blockId: string) {
|
||||||
// if focus title
|
// if focus title
|
||||||
if (blockId === this.doc.root?.id) {
|
if (blockId === this.store.root?.id) {
|
||||||
this.editor.std.selection.setGroup('note', []);
|
this.editor.std.selection.setGroup('note', []);
|
||||||
this.editor.std.event.active = false;
|
this.editor.std.event.active = false;
|
||||||
focusTitle(this.editor);
|
focusTitle(this.editor);
|
||||||
@@ -221,7 +221,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
const { selected, id, multiselect } = e.detail;
|
const { selected, id, multiselect } = e.detail;
|
||||||
const gfx = this.editor.std.get(GfxControllerIdentifier);
|
const gfx = this.editor.std.get(GfxControllerIdentifier);
|
||||||
const editorMode = this.editor.std.get(DocModeProvider).getEditorMode();
|
const editorMode = this.editor.std.get(DocModeProvider).getEditorMode();
|
||||||
const note = this.doc.getBlock(id)?.model;
|
const note = this.store.getBlock(id)?.model;
|
||||||
if (!note || !matchModels(note, [NoteBlockModel])) return;
|
if (!note || !matchModels(note, [NoteBlockModel])) return;
|
||||||
|
|
||||||
// map from signal to value
|
// map from signal to value
|
||||||
@@ -302,12 +302,12 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
return hasHeadings || this._context.enableSorting$.value;
|
return hasHeadings || this._context.enableSorting$.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
this._pageVisibleNotes$.value = getNotesFromDoc(this.doc, [
|
this._pageVisibleNotes$.value = getNotesFromStore(this.store, [
|
||||||
NoteDisplayMode.DocAndEdgeless,
|
NoteDisplayMode.DocAndEdgeless,
|
||||||
NoteDisplayMode.DocOnly,
|
NoteDisplayMode.DocOnly,
|
||||||
]).filter(isRenderableNote);
|
]).filter(isRenderableNote);
|
||||||
|
|
||||||
this._edgelessOnlyNotes$.value = getNotesFromDoc(this.doc, [
|
this._edgelessOnlyNotes$.value = getNotesFromStore(this.store, [
|
||||||
NoteDisplayMode.EdgelessOnly,
|
NoteDisplayMode.EdgelessOnly,
|
||||||
]).filter(isRenderableNote);
|
]).filter(isRenderableNote);
|
||||||
})
|
})
|
||||||
@@ -379,23 +379,23 @@ export class OutlinePanelBody extends SignalWatcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderDocTitle() {
|
private _renderDocTitle() {
|
||||||
if (!this.doc.root) return nothing;
|
if (!this.store.root) return nothing;
|
||||||
|
|
||||||
const hasNotEmptyHeadings =
|
const hasNotEmptyHeadings =
|
||||||
getHeadingBlocksFromDoc(
|
getHeadingBlocksFromDoc(
|
||||||
this.doc,
|
this.store,
|
||||||
[NoteDisplayMode.DocOnly, NoteDisplayMode.DocAndEdgeless],
|
[NoteDisplayMode.DocOnly, NoteDisplayMode.DocAndEdgeless],
|
||||||
true
|
true
|
||||||
).length > 0;
|
).length > 0;
|
||||||
|
|
||||||
if (!hasNotEmptyHeadings) return nothing;
|
if (!hasNotEmptyHeadings) return nothing;
|
||||||
|
|
||||||
const rootId = this.doc.root.id;
|
const rootId = this.store.root.id;
|
||||||
const active = rootId === this._activeHeadingId$.value;
|
const active = rootId === this._activeHeadingId$.value;
|
||||||
|
|
||||||
return html`<affine-outline-block-preview
|
return html`<affine-outline-block-preview
|
||||||
class=${classMap({ active: active })}
|
class=${classMap({ active: active })}
|
||||||
.block=${this.doc.root}
|
.block=${this.store.root}
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this._scrollToBlock(rootId).catch(console.error);
|
this._scrollToBlock(rootId).catch(console.error);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export class OutlineViewer extends SignalWatcher(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// title update
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.editor.store.workspace.meta.docMetaUpdated.subscribe(() => {
|
this.editor.store.workspace.meta.docMetaUpdated.subscribe(() => {
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import type { BlockModel, Store } from '@blocksuite/store';
|
|||||||
|
|
||||||
import { headingKeys } from '../config.js';
|
import { headingKeys } from '../config.js';
|
||||||
|
|
||||||
export function getNotesFromDoc(
|
export function getNotesFromStore(
|
||||||
doc: Store,
|
store: Store,
|
||||||
modes: NoteDisplayMode[] = [
|
modes: NoteDisplayMode[] = [
|
||||||
NoteDisplayMode.DocAndEdgeless,
|
NoteDisplayMode.DocAndEdgeless,
|
||||||
NoteDisplayMode.DocOnly,
|
NoteDisplayMode.DocOnly,
|
||||||
NoteDisplayMode.EdgelessOnly,
|
NoteDisplayMode.EdgelessOnly,
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
const rootModel = doc.root;
|
const rootModel = store.root;
|
||||||
if (!rootModel) return [];
|
if (!rootModel) return [];
|
||||||
|
|
||||||
const notes: NoteBlockModel[] = [];
|
const notes: NoteBlockModel[] = [];
|
||||||
@@ -59,7 +59,7 @@ export function getHeadingBlocksFromNote(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getHeadingBlocksFromDoc(
|
export function getHeadingBlocksFromDoc(
|
||||||
doc: Store,
|
store: Store,
|
||||||
modes: NoteDisplayMode[] = [
|
modes: NoteDisplayMode[] = [
|
||||||
NoteDisplayMode.DocAndEdgeless,
|
NoteDisplayMode.DocAndEdgeless,
|
||||||
NoteDisplayMode.DocOnly,
|
NoteDisplayMode.DocOnly,
|
||||||
@@ -67,6 +67,6 @@ export function getHeadingBlocksFromDoc(
|
|||||||
],
|
],
|
||||||
ignoreEmpty = false
|
ignoreEmpty = false
|
||||||
) {
|
) {
|
||||||
const notes = getNotesFromDoc(doc, modes);
|
const notes = getNotesFromStore(store, modes);
|
||||||
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
|
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,31 +15,22 @@ export const EditorOutlineViewer = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const outlineViewerRef = useRef<OutlineViewer | null>(null);
|
const outlineViewerRef = useRef<OutlineViewer | null>(null);
|
||||||
|
|
||||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
const onRefChange = useCallback(
|
||||||
if (container) {
|
(container: HTMLDivElement | null) => {
|
||||||
if (outlineViewerRef.current === null) {
|
if (container && editor) {
|
||||||
console.error('outline viewer should be initialized');
|
if (outlineViewerRef.current) {
|
||||||
return;
|
outlineViewerRef.current.remove();
|
||||||
|
}
|
||||||
|
outlineViewerRef.current = new OutlineViewer();
|
||||||
|
outlineViewerRef.current.editor = editor;
|
||||||
|
outlineViewerRef.current.toggleOutlinePanel = openOutlinePanel ?? null;
|
||||||
|
container.append(outlineViewerRef.current);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[editor, openOutlinePanel]
|
||||||
|
);
|
||||||
|
|
||||||
container.append(outlineViewerRef.current);
|
if (!editor || !show) return null;
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!editor || !show) return;
|
|
||||||
|
|
||||||
if (!outlineViewerRef.current) {
|
|
||||||
outlineViewerRef.current = new OutlineViewer();
|
|
||||||
}
|
|
||||||
if (outlineViewerRef.current.editor !== editor) {
|
|
||||||
outlineViewerRef.current.editor = editor;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
outlineViewerRef.current.toggleOutlinePanel !== openOutlinePanel &&
|
|
||||||
openOutlinePanel
|
|
||||||
) {
|
|
||||||
outlineViewerRef.current.toggleOutlinePanel = openOutlinePanel;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={styles.root} ref={onRefChange} />;
|
return <div className={styles.root} ref={onRefChange} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -179,6 +179,25 @@ test('should hide edgeless-only note headings', async ({ page }) => {
|
|||||||
await expect(h1InPanel).toContainText(['Heading 1']);
|
await expect(h1InPanel).toContainText(['Heading 1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('outline viewer should update after change heading in edgeless mode', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await createTitle(page);
|
||||||
|
await pressEnter(page);
|
||||||
|
|
||||||
|
await type(page, '# ');
|
||||||
|
await type(page, 'Heading 1');
|
||||||
|
|
||||||
|
await clickEdgelessModeButton(page);
|
||||||
|
const note = page.locator('affine-edgeless-note');
|
||||||
|
await note.dblclick();
|
||||||
|
await type(page, '# New Heading');
|
||||||
|
await clickPageModeButton(page);
|
||||||
|
|
||||||
|
const indicators = getIndicators(page);
|
||||||
|
await expect(indicators).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('outline viewer should be useable in doc peek preview', async ({
|
test('outline viewer should be useable in doc peek preview', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user