Compare commits

...

3 Commits

Author SHA1 Message Date
L-Sun 3d30670987 refactor(editor): use add and delete impl move block 2025-05-29 17:36:29 +08:00
donteatfriedrice bc67766bb9 fix(editor): cleanup transformer middleware slot subscriptions (#12630)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved resource management by introducing explicit cleanup for various middleware components, ensuring that resources are properly released when no longer needed.

- **Refactor**
  - Updated middleware logic to support cleanup functions, enhancing the stability and performance of the application by preventing potential memory leaks.

- **Chores**
  - Enhanced lifecycle management in core systems to automatically dispose of resources when appropriate.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 08:33:30 +00:00
L-Sun 9a96cfded0 fix(editor): viewportElement is undefined in edgeless root block (#12626)
This PR fixed that `rootComponent.viewportElement` is undefeined in edgeless mode, which leads that toast can not be render in playground.

https://github.com/toeverything/AFFiNE/blob/388641bc89caf37451c2d57ae5e538d432bf1518/blocksuite/affine/components/src/toast/create.ts#L23-L35

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Improved internal code organization for better maintainability. No changes to visible features or functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 08:10:54 +00:00
14 changed files with 233 additions and 91 deletions
@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
) as SurfaceBlockModel;
}
private get _viewportElement(): HTMLElement {
get viewportElement(): HTMLElement {
return this.std.get(ViewportElementProvider).viewportElement;
}
@@ -267,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
this.gfx.viewport.onResize();
});
resizeObserver.observe(this._viewportElement);
resizeObserver.observe(this.viewportElement);
this._resizeObserver = resizeObserver;
}
@@ -37,7 +37,7 @@ const handlePoint = (
};
const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
slots.afterExport.subscribe(payload => {
const afterExportSubscription = slots.afterExport.subscribe(payload => {
if (payload.type === 'block') {
const snapshot = payload.snapshot;
@@ -53,10 +53,14 @@ const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
}
}
});
return () => {
afterExportSubscription.unsubscribe();
};
};
export const copyMiddleware = (std: BlockStdScope): TransformerMiddleware => {
return ({ slots }) => {
sliceText(slots, std);
return sliceText(slots, std);
};
};
@@ -3,7 +3,7 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const fileNameMiddleware =
(fileName?: string): TransformerMiddleware =>
({ slots }) => {
slots.beforeImport.subscribe(payload => {
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
if (payload.type !== 'page') {
return;
}
@@ -20,4 +20,8 @@ export const fileNameMiddleware =
],
};
});
return () => {
beforeImportSubscription.unsubscribe();
};
};
@@ -528,7 +528,7 @@ export const pasteMiddleware = (
): TransformerMiddleware => {
return ({ slots }) => {
let tr: PasteTr | undefined;
slots.beforeImport.subscribe(payload => {
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
const { snapshot } = payload;
flatNote(snapshot);
@@ -543,13 +543,18 @@ export const pasteMiddleware = (
}
}
});
slots.afterImport.subscribe(payload => {
const afterImportSubscription = slots.afterImport.subscribe(payload => {
if (tr && payload.type === 'slice') {
tr.pasted();
tr.focusPasted();
tr.convertToLinkedDoc();
}
});
return () => {
beforeImportSubscription.unsubscribe();
afterImportSubscription.unsubscribe();
};
};
};
@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
map(({ model }) => model)
);
afterImportBlock$
const afterImportBlockSubscription = afterImportBlock$
.pipe(filter(model => matchModels(model, [DatabaseBlockModel])))
.subscribe(model => {
Object.keys(model.props.cells).forEach(cellId => {
@@ -44,7 +44,7 @@ export const replaceIdMiddleware =
});
// replace LinkedPage pageId with new id in paragraph blocks
afterImportBlock$
const replaceLinkedPageIdSubscription = afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [ParagraphBlockModel, ListBlockModel])
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
}
});
afterImportBlock$
const replaceSurfaceRefIdSubscription = afterImportBlock$
.pipe(filter(model => matchModels(model, [SurfaceRefBlockModel])))
.subscribe(model => {
const original = model.props.reference;
@@ -105,7 +105,7 @@ export const replaceIdMiddleware =
});
// TODO(@fundon): process linked block/element
afterImportBlock$
const replaceLinkedDocIdSubscription = afterImportBlock$
.pipe(
filter(model =>
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
// Before Import
slots.beforeImport
const beforeImportPageSubscription = slots.beforeImport
.pipe(filter(payload => payload.type === 'page'))
.subscribe(payload => {
if (idMap.has(payload.snapshot.meta.id)) {
@@ -140,7 +140,7 @@ export const replaceIdMiddleware =
payload.snapshot.meta.id = newId;
});
slots.beforeImport
const beforeImportBlockSubscription = slots.beforeImport
.pipe(
filter(
(payload): payload is BeforeImportBlockPayload =>
@@ -244,4 +244,13 @@ export const replaceIdMiddleware =
});
}
});
return () => {
afterImportBlockSubscription.unsubscribe();
replaceLinkedPageIdSubscription.unsubscribe();
replaceSurfaceRefIdSubscription.unsubscribe();
replaceLinkedDocIdSubscription.unsubscribe();
beforeImportPageSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
};
@@ -5,33 +5,42 @@ export const surfaceRefToEmbed =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
let pageId: string | null = null;
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
pageId = payload.snapshot.pageId;
}
}
});
slots.beforeImport.subscribe(payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
);
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
payload => {
// only handle surface-ref block snapshot
if (
payload.type !== 'block' ||
payload.snapshot.flavour !== 'affine:surface-ref'
)
return;
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
const isNotSameDoc = pageId !== std.store.doc.id;
if (pageId && isNotSameDoc) {
// The blockId of the original surface-ref block
const blockId = payload.snapshot.id;
payload.snapshot.id = std.workspace.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
pageId,
params: {
mode: 'page',
blockIds: [blockId],
},
};
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
const isNotSameDoc = pageId !== std.store.doc.id;
if (pageId && isNotSameDoc) {
// The blockId of the original surface-ref block
const blockId = payload.snapshot.id;
payload.snapshot.id = std.workspace.idGenerator();
payload.snapshot.flavour = 'affine:embed-linked-doc';
payload.snapshot.props = {
pageId,
params: {
mode: 'page',
blockIds: [blockId],
},
};
}
}
});
);
return () => {
beforeImportSliceSubscription.unsubscribe();
beforeImportBlockSubscription.unsubscribe();
};
};
@@ -3,9 +3,13 @@ import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
export const titleMiddleware =
(metas: DocMeta[]): TransformerMiddleware =>
({ slots, adapterConfigs }) => {
slots.beforeExport.subscribe(() => {
const beforeExportSubscription = slots.beforeExport.subscribe(() => {
for (const meta of metas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
return () => {
beforeExportSubscription.unsubscribe();
};
};
@@ -79,7 +79,7 @@ export const uploadMiddleware = (
}
}
blockView$
const blockViewSubscription = blockView$
.pipe(
map(payload => {
if (assetsManager.uploadingAssetsMap.size === 0) return null;
@@ -110,5 +110,9 @@ export const uploadMiddleware = (
)
)
.subscribe();
return () => {
blockViewSubscription.unsubscribe();
};
};
};
@@ -40,7 +40,7 @@ export const gfxBlocksFilter = (
}
return ({ slots, transformerConfigs }) => {
slots.beforeExport.subscribe(payload => {
const beforeExportSubscription = slots.beforeExport.subscribe(payload => {
if (payload.type !== 'block') {
return;
}
@@ -54,7 +54,7 @@ export const gfxBlocksFilter = (
}
});
slots.afterExport.subscribe(payload => {
const afterExportSubscription = slots.afterExport.subscribe(payload => {
if (payload.type !== 'block') {
return;
}
@@ -110,5 +110,10 @@ export const gfxBlocksFilter = (
});
}
});
return () => {
beforeExportSubscription.unsubscribe();
afterExportSubscription.unsubscribe();
};
};
};
@@ -9,36 +9,45 @@ export const newIdCrossDoc =
let samePage = false;
const oldToNewIdMap = new Map<string, string>();
slots.beforeImport.subscribe(payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.store.id;
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
payload => {
if (payload.type === 'slice') {
samePage = payload.snapshot.pageId === std.store.id;
}
if (payload.type === 'block' && !samePage) {
const newId = std.workspace.idGenerator();
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
}
}
if (payload.type === 'block' && !samePage) {
const newId = std.workspace.idGenerator();
);
oldToNewIdMap.set(payload.snapshot.id, newId);
payload.snapshot.id = newId;
const afterImportBlockSubscription = slots.afterImport.subscribe(
payload => {
if (
!samePage &&
payload.type === 'block' &&
matchModels(payload.model, [DatabaseBlockModel])
) {
const originalCells = payload.model.props.cells;
const newCells = {
...originalCells,
};
Object.keys(originalCells).forEach(cellId => {
if (oldToNewIdMap.has(cellId)) {
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
}
});
payload.model.props.cells$.value = newCells;
}
}
});
);
slots.afterImport.subscribe(payload => {
if (
!samePage &&
payload.type === 'block' &&
matchModels(payload.model, [DatabaseBlockModel])
) {
const originalCells = payload.model.props.cells;
const newCells = {
...originalCells,
};
Object.keys(originalCells).forEach(cellId => {
if (oldToNewIdMap.has(cellId)) {
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
}
});
payload.model.props.cells$.value = newCells;
}
});
return () => {
beforeImportSliceSubscription.unsubscribe();
afterImportBlockSubscription.unsubscribe();
};
};
@@ -7,19 +7,25 @@ import type { TransformerMiddleware } from '@blocksuite/store';
export const reorderList =
(std: BlockStdScope): TransformerMiddleware =>
({ slots }) => {
slots.afterImport.subscribe(payload => {
if (payload.type === 'block') {
const model = payload.model;
if (
matchModels(model, [ListBlockModel]) &&
model.props.type === 'numbered'
) {
const next = std.store.getNext(model);
correctNumberedListsOrderToPrev(std.store, model);
if (next) {
correctNumberedListsOrderToPrev(std.store, next);
const afterImportBlockSubscription = slots.afterImport.subscribe(
payload => {
if (payload.type === 'block') {
const model = payload.model;
if (
matchModels(model, [ListBlockModel]) &&
model.props.type === 'numbered'
) {
const next = std.store.getNext(model);
correctNumberedListsOrderToPrev(std.store, model);
if (next) {
correctNumberedListsOrderToPrev(std.store, next);
}
}
}
}
});
);
return () => {
afterImportBlockSubscription.unsubscribe();
};
};
@@ -22,6 +22,7 @@ import {
type BlockModel,
type BlockOptions,
type BlockProps,
toDraftModel,
type YBlock,
} from '../block/index.js';
import { DocCRUD } from './crud.js';
@@ -1143,13 +1144,82 @@ export class Store {
return;
}
this.transact(() => {
this._crud.moveBlocks(
blocksToMove.map(model => model.id),
newParent.id,
targetSibling?.id ?? null,
shouldInsertBeforeSibling
if (
blocksToMove.length > 1 &&
targetSibling &&
blocksToMove.includes(targetSibling)
) {
console.error(
'Cannot move blocks when the target sibling is in the blocks to move'
);
return;
}
if (blocksToMove.length === 1 && targetSibling === blocksToMove[0]) {
return;
}
if (blocksToMove.includes(newParent)) {
console.error(
'Cannot move blocks when the new parent is in the blocks to move'
);
return;
}
for (let i = 0; i < blocksToMove.length - 1; i++) {
const block = blocksToMove[i];
const nextBlock = blocksToMove[i + 1];
if (
block.parent &&
block.parent === nextBlock.parent &&
this.getNext(block) !== nextBlock
) {
console.error(
'The blocks to move are not contiguous under their parent'
);
return;
}
}
this.transact(() => {
let insertIndex = 0;
const updateInsertIndex = () => {
if (targetSibling) {
const targetIndex = newParent.children.indexOf(targetSibling);
if (targetIndex === -1) {
console.warn('Target sibling not found, just insert to the end');
} else {
insertIndex = shouldInsertBeforeSibling
? targetIndex
: targetIndex + 1;
}
}
};
blocksToMove.forEach(block => {
const draftModel = toDraftModel(block);
const props = {
...draftModel.props,
children: block.children,
};
console.log(props);
this._crud.deleteBlock(block.id, {
deleteChildren: false,
});
updateInsertIndex();
this._crud.addBlock(
draftModel.id,
draftModel.flavour,
props,
newParent.id,
insertIndex
);
});
});
}
@@ -113,6 +113,8 @@ type TransformerMiddlewareOptions = {
transformerConfigs: Map<string, unknown>;
};
type TransformerMiddlewareCleanup = () => void;
export type TransformerMiddleware = (
options: TransformerMiddlewareOptions
) => void;
) => void | TransformerMiddlewareCleanup;
@@ -1,3 +1,4 @@
import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { nextTick } from '@blocksuite/global/utils';
import { Subject } from 'rxjs';
@@ -67,6 +68,8 @@ export class Transformer {
private readonly _docCRUD: DocCRUD;
private readonly _disposables: DisposableGroup = new DisposableGroup();
private readonly _slots: TransformerSlots = {
beforeImport: new Subject<BeforeImportPayload>(),
afterImport: new Subject<AfterImportPayload>(),
@@ -366,13 +369,16 @@ export class Transformer {
this._docCRUD = docCRUD;
middlewares.forEach(middleware => {
middleware({
const cleanup = middleware({
slots: this._slots,
docCRUD: this._docCRUD,
assetsManager: this._assetsManager,
adapterConfigs: this._adapterConfigs,
transformerConfigs: this._transformerConfigs,
});
if (cleanup) {
this._disposables.add(cleanup);
}
});
}
@@ -646,4 +652,9 @@ export class Transformer {
reset() {
this._assetsManager.cleanup();
}
[Symbol.dispose]() {
this._disposables.dispose();
this._assetsManager.cleanup();
}
}