mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
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 { type TocContext, tocContext } from '../config';
|
||||
import { getNotesFromDoc } from '../utils/query';
|
||||
import { getNotesFromStore } from '../utils/query';
|
||||
import * as styles from './outline-notice.css';
|
||||
|
||||
export const AFFINE_OUTLINE_NOTICE = 'affine-outline-notice';
|
||||
@@ -31,7 +31,7 @@ export class OutlineNotice extends SignalWatcher(
|
||||
}
|
||||
|
||||
const shouldShowNotice =
|
||||
getNotesFromDoc(this._context.editor$.value.store, [
|
||||
getNotesFromStore(this._context.editor$.value.store, [
|
||||
NoteDisplayMode.DocOnly,
|
||||
]).length > 0;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import type {
|
||||
import type { NoteCardEntity, NoteDropPayload } from '../utils/drag';
|
||||
import {
|
||||
getHeadingBlocksFromDoc,
|
||||
getNotesFromDoc,
|
||||
getNotesFromStore,
|
||||
isHeadingBlock,
|
||||
} from '../utils/query';
|
||||
import {
|
||||
@@ -91,7 +91,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
return this._context.editor$.value;
|
||||
}
|
||||
|
||||
private get doc() {
|
||||
private get store() {
|
||||
return this.editor.store;
|
||||
}
|
||||
|
||||
@@ -154,11 +154,11 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
}
|
||||
|
||||
private _moveSelectedNotes(insertIndex: number) {
|
||||
if (!this.doc.root) return;
|
||||
if (!this.store.root) return;
|
||||
|
||||
const pageVisibleNotes = this._pageVisibleNotes$.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>();
|
||||
children.forEach((block, index) => {
|
||||
@@ -189,14 +189,14 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
|
||||
const newChildren = [...leftPart, ...selected, ...rightPart];
|
||||
|
||||
this.doc.updateBlock(this.doc.root, {
|
||||
this.store.updateBlock(this.store.root, {
|
||||
children: newChildren,
|
||||
});
|
||||
}
|
||||
|
||||
private async _scrollToBlock(blockId: string) {
|
||||
// if focus title
|
||||
if (blockId === this.doc.root?.id) {
|
||||
if (blockId === this.store.root?.id) {
|
||||
this.editor.std.selection.setGroup('note', []);
|
||||
this.editor.std.event.active = false;
|
||||
focusTitle(this.editor);
|
||||
@@ -221,7 +221,7 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
const { selected, id, multiselect } = e.detail;
|
||||
const gfx = this.editor.std.get(GfxControllerIdentifier);
|
||||
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;
|
||||
|
||||
// map from signal to value
|
||||
@@ -302,12 +302,12 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
return hasHeadings || this._context.enableSorting$.value;
|
||||
};
|
||||
|
||||
this._pageVisibleNotes$.value = getNotesFromDoc(this.doc, [
|
||||
this._pageVisibleNotes$.value = getNotesFromStore(this.store, [
|
||||
NoteDisplayMode.DocAndEdgeless,
|
||||
NoteDisplayMode.DocOnly,
|
||||
]).filter(isRenderableNote);
|
||||
|
||||
this._edgelessOnlyNotes$.value = getNotesFromDoc(this.doc, [
|
||||
this._edgelessOnlyNotes$.value = getNotesFromStore(this.store, [
|
||||
NoteDisplayMode.EdgelessOnly,
|
||||
]).filter(isRenderableNote);
|
||||
})
|
||||
@@ -379,23 +379,23 @@ export class OutlinePanelBody extends SignalWatcher(
|
||||
}
|
||||
|
||||
private _renderDocTitle() {
|
||||
if (!this.doc.root) return nothing;
|
||||
if (!this.store.root) return nothing;
|
||||
|
||||
const hasNotEmptyHeadings =
|
||||
getHeadingBlocksFromDoc(
|
||||
this.doc,
|
||||
this.store,
|
||||
[NoteDisplayMode.DocOnly, NoteDisplayMode.DocAndEdgeless],
|
||||
true
|
||||
).length > 0;
|
||||
|
||||
if (!hasNotEmptyHeadings) return nothing;
|
||||
|
||||
const rootId = this.doc.root.id;
|
||||
const rootId = this.store.root.id;
|
||||
const active = rootId === this._activeHeadingId$.value;
|
||||
|
||||
return html`<affine-outline-block-preview
|
||||
class=${classMap({ active: active })}
|
||||
.block=${this.doc.root}
|
||||
.block=${this.store.root}
|
||||
@click=${() => {
|
||||
this._scrollToBlock(rootId).catch(console.error);
|
||||
}}
|
||||
|
||||
@@ -200,6 +200,7 @@ export class OutlineViewer extends SignalWatcher(
|
||||
)
|
||||
);
|
||||
|
||||
// title update
|
||||
this.disposables.add(
|
||||
this.editor.store.workspace.meta.docMetaUpdated.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
|
||||
@@ -9,15 +9,15 @@ import type { BlockModel, Store } from '@blocksuite/store';
|
||||
|
||||
import { headingKeys } from '../config.js';
|
||||
|
||||
export function getNotesFromDoc(
|
||||
doc: Store,
|
||||
export function getNotesFromStore(
|
||||
store: Store,
|
||||
modes: NoteDisplayMode[] = [
|
||||
NoteDisplayMode.DocAndEdgeless,
|
||||
NoteDisplayMode.DocOnly,
|
||||
NoteDisplayMode.EdgelessOnly,
|
||||
]
|
||||
) {
|
||||
const rootModel = doc.root;
|
||||
const rootModel = store.root;
|
||||
if (!rootModel) return [];
|
||||
|
||||
const notes: NoteBlockModel[] = [];
|
||||
@@ -59,7 +59,7 @@ export function getHeadingBlocksFromNote(
|
||||
}
|
||||
|
||||
export function getHeadingBlocksFromDoc(
|
||||
doc: Store,
|
||||
store: Store,
|
||||
modes: NoteDisplayMode[] = [
|
||||
NoteDisplayMode.DocAndEdgeless,
|
||||
NoteDisplayMode.DocOnly,
|
||||
@@ -67,6 +67,6 @@ export function getHeadingBlocksFromDoc(
|
||||
],
|
||||
ignoreEmpty = false
|
||||
) {
|
||||
const notes = getNotesFromDoc(doc, modes);
|
||||
const notes = getNotesFromStore(store, modes);
|
||||
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
|
||||
}
|
||||
|
||||
@@ -15,31 +15,22 @@ export const EditorOutlineViewer = ({
|
||||
}) => {
|
||||
const outlineViewerRef = useRef<OutlineViewer | null>(null);
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
if (outlineViewerRef.current === null) {
|
||||
console.error('outline viewer should be initialized');
|
||||
return;
|
||||
const onRefChange = useCallback(
|
||||
(container: HTMLDivElement | null) => {
|
||||
if (container && editor) {
|
||||
if (outlineViewerRef.current) {
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
if (!editor || !show) return null;
|
||||
|
||||
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']);
|
||||
});
|
||||
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user