refactor(editor): add helper function for undo notification (#11903)

### What Changes
- Refactors the `notify` function call with undo button. It is called `notifyWithUndo`, which can  associate the visibility of the `NotificationCard` with the top undo item in history stack, such as undo by shortcut `Cmd+Z`.
- change icon of the "Insert into page" button. Close [BS-3267](https://linear.app/affine-design/issue/BS-3267/frame和group的insert-into-page图标也更换一下)
This commit is contained in:
L-Sun
2025-04-23 02:43:56 +00:00
committed by L-Sun
parent 27ff9ab9f4
commit 9dbdd4b7ba
14 changed files with 136 additions and 157 deletions

View File

@@ -11,7 +11,7 @@ import {
import { EMBED_CARD_HEIGHT } from '@blocksuite/affine-shared/consts';
import { NotificationProvider } from '@blocksuite/affine-shared/services';
import { matchModels } from '@blocksuite/affine-shared/utils';
import { BlockStdScope, EditorLifeCycleExtension } from '@blocksuite/std';
import { BlockStdScope } from '@blocksuite/std';
import {
type BlockModel,
type BlockSnapshot,
@@ -337,45 +337,12 @@ export function promptDocTitle(std: BlockStdScope, autofill?: string) {
});
}
export function notifyDocCreated(std: BlockStdScope, doc: Store) {
const notification = std.getOptional(NotificationProvider);
if (!notification) return;
const abortController = new AbortController();
const clear = () => {
doc.history.off('stack-item-added', addHandler);
doc.history.off('stack-item-popped', popHandler);
disposable.unsubscribe();
};
const closeNotify = () => {
abortController.abort();
clear();
};
// edit or undo or switch doc, close notify toast
const addHandler = doc.history.on('stack-item-added', closeNotify);
const popHandler = doc.history.on('stack-item-popped', closeNotify);
const disposable = std
.get(EditorLifeCycleExtension)
.slots.unmounted.subscribe(closeNotify);
notification.notify({
export function notifyDocCreated(std: BlockStdScope) {
std.getOptional(NotificationProvider)?.notifyWithUndoAction({
title: 'Linked doc created',
message: 'You can click undo to recovery block content',
accent: 'info',
duration: 10 * 1000,
actions: [
{
key: 'undo',
label: 'Undo',
onClick: () => {
doc.undo();
clear();
},
},
],
abort: abortController.signal,
onClose: clear,
});
}

View File

@@ -230,7 +230,7 @@ export const builtinToolbarConfig = {
draftedModels,
title
);
notifyDocCreated(std, store);
notifyDocCreated(std);
track('DocCreated', {
segment: 'doc',

View File

@@ -16,6 +16,7 @@ import {
SurfaceRefBlockSchema,
} from '@blocksuite/affine-model';
import {
NotificationProvider,
type ToolbarContext,
type ToolbarModuleConfig,
ToolbarModuleExtension,
@@ -26,7 +27,11 @@ import {
} from '@blocksuite/affine-shared/utils';
import { mountFrameTitleEditor } from '@blocksuite/affine-widget-frame-title';
import { Bound } from '@blocksuite/global/gfx';
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
import {
EditIcon,
InsertIntoPageIcon,
UngroupIcon,
} from '@blocksuite/icons/lit';
import { type BlockComponent, BlockFlavourIdentifier } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
@@ -46,9 +51,8 @@ const builtinSurfaceToolbarConfig = {
{
id: 'a.insert-into-page',
label: 'Insert into Page',
showLabel: true,
tooltip: 'Insert into Page',
icon: PageIcon(),
icon: InsertIntoPageIcon(),
when: ctx => ctx.getSurfaceModelsByType(FrameBlockModel).length === 1,
run(ctx) {
const model = ctx.getCurrentModelByType(FrameBlockModel);
@@ -78,13 +82,23 @@ const builtinSurfaceToolbarConfig = {
);
}
ctx.store.captureSync();
ctx.store.addBlock(
SurfaceRefBlockSchema.model.flavour,
{ reference: frameId, refFlavour: FrameBlockSchema.model.flavour },
lastNoteId
);
toast(ctx.host, 'Frame has been inserted into doc');
const notification = ctx.std.getOptional(NotificationProvider);
if (notification) {
notification.notifyWithUndoAction({
title: 'Frame inserted into Page.',
message: 'Frame has been inserted into doc',
accent: 'success',
});
} else {
toast(ctx.host, 'Frame has been inserted into doc');
}
},
},
{

View File

@@ -31,10 +31,7 @@ import {
LinkedPageIcon,
ScissorsIcon,
} from '@blocksuite/icons/lit';
import {
BlockFlavourIdentifier,
EditorLifeCycleExtension,
} from '@blocksuite/std';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { computed } from '@preact/signals-core';
import { html } from 'lit';
@@ -504,34 +501,6 @@ function setDisplayMode(
ctx.selection.clear();
}
const abortController = new AbortController();
const clear = () => {
ctx.history.off('stack-item-added', addHandler);
ctx.history.off('stack-item-popped', popHandler);
disposable.unsubscribe();
};
const closeNotify = () => {
abortController.abort();
clear();
};
const addHandler = ctx.history.on('stack-item-added', closeNotify);
const popHandler = ctx.history.on('stack-item-popped', closeNotify);
const disposable = ctx.std
.get(EditorLifeCycleExtension)
.slots.unmounted.subscribe(closeNotify);
const undo = () => {
ctx.store.undo();
closeNotify();
};
const viewInToc = () => {
const sidebar = ctx.std.getOptional(SidebarExtensionIdentifier);
sidebar?.open('outline');
closeNotify();
};
const data =
newMode === NoteDisplayMode.EdgelessOnly
? {
@@ -544,27 +513,21 @@ function setDisplayMode(
};
const notification = ctx.std.getOptional(NotificationProvider);
notification?.notify({
notification?.notifyWithUndoAction({
title: data.title,
message: `${data.message} Find it in the TOC for quick navigation.`,
accent: 'success',
duration: 5 * 1000,
actions: [
{
key: 'undo-display-in-page',
label: 'Undo',
onClick: () => undo(),
},
{
key: 'view-in-toc',
label: 'View in Toc',
onClick: () => viewInToc(),
onClick: () => {
const sidebar = ctx.std.getOptional(SidebarExtensionIdentifier);
sidebar?.open('outline');
},
},
],
abort: abortController.signal,
onClose: () => {
clear();
},
});
ctx.track('NoteDisplayModeChanged', {

View File

@@ -58,7 +58,7 @@ export const quickActionConfig: QuickActionConfig[] = [
draftedModels,
title
).catch(console.error);
notifyDocCreated(std, doc);
notifyDocCreated(std);
})
.catch(console.error);
},

View File

@@ -244,7 +244,7 @@ const turnIntoLinkedDoc = {
draftedModels,
title
);
notifyDocCreated(std, store);
notifyDocCreated(std);
track('DocCreated', {
segment: 'doc',

View File

@@ -347,7 +347,7 @@ export const moreActions = [
other: 'new doc',
});
notifyDocCreated(ctx.std, ctx.store);
notifyDocCreated(ctx.std);
};
create().catch(console.error);

View File

@@ -156,7 +156,7 @@ export class PageKeyboardManager {
draftedModels,
title
).catch(console.error);
notifyDocCreated(rootComponent.std, doc);
notifyDocCreated(rootComponent.std);
})
.catch(console.error);
}

View File

@@ -1,52 +1,22 @@
import { NotificationProvider } from '@blocksuite/affine-shared/services';
import { type BlockStdScope, EditorLifeCycleExtension } from '@blocksuite/std';
import { type BlockStdScope } from '@blocksuite/std';
import { toast } from '../toast/toast.js';
function notify(std: BlockStdScope, title: string, message: string) {
const notification = std.getOptional(NotificationProvider);
const { store: doc, host } = std;
const { host } = std;
if (!notification) {
toast(host, title);
return;
}
const abortController = new AbortController();
const clear = () => {
doc.history.off('stack-item-added', addHandler);
doc.history.off('stack-item-popped', popHandler);
disposable.unsubscribe();
};
const closeNotify = () => {
abortController.abort();
clear();
};
// edit or undo or switch doc, close notify toast
const addHandler = doc.history.on('stack-item-added', closeNotify);
const popHandler = doc.history.on('stack-item-popped', closeNotify);
const disposable = host.std
.get(EditorLifeCycleExtension)
.slots.unmounted.subscribe(closeNotify);
notification.notify({
notification.notifyWithUndoAction({
title,
message,
accent: 'info',
duration: 10 * 1000,
actions: [
{
key: 'undo',
label: 'Undo',
onClick: () => {
doc.undo();
clear();
},
},
],
abort: abortController.signal,
onClose: clear,
});
}

View File

@@ -14,7 +14,11 @@ import {
import { matchModels } from '@blocksuite/affine-shared/utils';
import { getRootBlock } from '@blocksuite/affine-widget-edgeless-toolbar';
import { Bound } from '@blocksuite/global/gfx';
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
import {
EditIcon,
InsertIntoPageIcon,
UngroupIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import { ungroupCommand } from '../command';
@@ -25,9 +29,8 @@ export const groupToolbarConfig = {
{
id: 'a.insert-into-page',
label: 'Insert into Page',
showLabel: true,
tooltip: 'Insert into Page',
icon: PageIcon(),
icon: InsertIntoPageIcon(),
when: ctx => ctx.getSurfaceModelsByType(GroupElementModel).length === 1,
run(ctx) {
const model = ctx.getCurrentModelByType(GroupElementModel);

View File

@@ -1,5 +1,6 @@
import { createIdentifier } from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import { createIdentifier, type ServiceProvider } from '@blocksuite/global/di';
import { EditorLifeCycleExtension } from '@blocksuite/std';
import { type ExtensionType, StoreIdentifier } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
export interface NotificationService {
@@ -37,8 +38,16 @@ export interface NotificationService {
label: string | TemplateResult;
onClick: () => void;
}[];
onClose: () => void;
onClose?: () => void;
}): void;
/**
* Notify with undo action, it is a helper function to notify with undo action.
* And the notification card will be closed when undo action is triggered by shortcut key or other ways.
*/
notifyWithUndoAction: (
options: Parameters<NotificationService['notify']>[0]
) => void;
}
export const NotificationProvider = createIdentifier<NotificationService>(
@@ -46,11 +55,69 @@ export const NotificationProvider = createIdentifier<NotificationService>(
);
export function NotificationExtension(
notificationService: NotificationService
notificationService: Omit<NotificationService, 'notifyWithUndoAction'>
): ExtensionType {
return {
setup: di => {
di.addImpl(NotificationProvider, notificationService);
di.addImpl(NotificationProvider, provider => {
return {
...notificationService,
notifyWithUndoAction: options => {
notifyWithUndoActionImpl(
provider,
notificationService.notify,
options
);
},
};
});
},
};
}
function notifyWithUndoActionImpl(
provider: ServiceProvider,
notify: NotificationService['notify'],
options: Parameters<NotificationService['notifyWithUndoAction']>[0]
) {
const store = provider.get(StoreIdentifier);
const abortController = new AbortController();
const abort = () => {
abortController.abort();
};
options.abort?.addEventListener('abort', abort);
const clearOnClose = () => {
store.history.off('stack-item-added', addHandler);
store.history.off('stack-item-popped', popHandler);
disposable.unsubscribe();
options.abort?.removeEventListener('abort', abort);
};
const addHandler = store.history.on('stack-item-added', abort);
const popHandler = store.history.on('stack-item-popped', abort);
const disposable = provider
.get(EditorLifeCycleExtension)
.slots.unmounted.subscribe(() => abort());
notify({
...options,
actions: [
{
key: 'notification-card-undo',
label: 'Undo',
onClick: () => {
store.undo();
abortController.abort();
},
},
...(options.actions ?? []),
],
abort: abortController.signal,
onClose: () => {
options.onClose?.();
clearOnClose();
},
});
}

View File

@@ -97,6 +97,10 @@ export function mockNotificationService(editor: TestAffineEditorContainer) {
// todo: implement in playground
console.log(notification);
},
notifyWithUndoAction: notification => {
// todo: implement in playground
console.log(notification);
},
};
return notificationService;
}