diff --git a/blocksuite/affine/shared/src/services/notification-service.ts b/blocksuite/affine/shared/src/services/notification-service.ts index 1da78e739..5ac1c8bcb 100644 --- a/blocksuite/affine/shared/src/services/notification-service.ts +++ b/blocksuite/affine/shared/src/services/notification-service.ts @@ -29,6 +29,7 @@ export interface NotificationService { notify(options: { title: string | TemplateResult; message?: string | TemplateResult; + footer?: string | TemplateResult; accent?: 'info' | 'success' | 'warning' | 'error'; duration?: number; // unit ms, give 0 to disable auto dismiss abort?: AbortSignal; diff --git a/blocksuite/blocks/package.json b/blocksuite/blocks/package.json index 59410ab74..f75b9cd11 100644 --- a/blocksuite/blocks/package.json +++ b/blocksuite/blocks/package.json @@ -47,6 +47,7 @@ "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.6", + "@vanilla-extract/css": "^1.17.0", "date-fns": "^4.0.0", "dompurify": "^3.1.6", "fflate": "^0.8.2", diff --git a/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts b/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts index 15884a90f..5c0062cdb 100644 --- a/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts +++ b/blocksuite/blocks/src/root-block/widgets/element-toolbar/change-note-button.ts @@ -30,16 +30,18 @@ import { } from '@blocksuite/affine-model'; import { FeatureFlagService, + NotificationProvider, + SidebarExtensionIdentifier, ThemeProvider, } from '@blocksuite/affine-shared/services'; import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { - assertExists, Bound, countBy, maxBy, WithDisposable, } from '@blocksuite/global/utils'; +import { LinkedPageIcon } from '@blocksuite/icons/lit'; import { html, LitElement, nothing, type TemplateResult } from 'lit'; import { property, query } from 'lit/decorators.js'; import { join } from 'lit/directives/join.js'; @@ -52,6 +54,7 @@ import { } from '../../edgeless/components/panel/line-styles-panel.js'; import { getTooltipWithShortcut } from '../../edgeless/components/utils.js'; import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; +import * as styles from './styles.css'; const SIZE_LIST = [ { name: 'None', value: 0 }, @@ -141,15 +144,17 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { .getFlag('enable_advanced_block_visibility'); } + private get _pageBlockHeaderEnabled() { + return this.doc.get(FeatureFlagService).getFlag('enable_page_block_header'); + } + private get doc() { return this.edgeless.doc; } private get _enableAutoHeight() { return !( - this.edgeless.doc - .get(FeatureFlagService) - .getFlag('enable_page_block_header') && + this._pageBlockHeaderEnabled && this.notes.length === 1 && this.notes[0].parent?.children.find(child => matchFlavours(child, ['affine:note']) @@ -195,13 +200,16 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { return; } + this.doc.captureSync(); + this.crud.updateElement(note.id, { displayMode: newMode }); const noteParent = this.doc.getParent(note); - assertExists(noteParent); + if (!noteParent) return; + const noteParentChildNotes = noteParent.children.filter(block => matchFlavours(block, ['affine:note']) - ) as NoteBlockModel[]; + ); const noteParentLastNote = noteParentChildNotes[noteParentChildNotes.length - 1]; @@ -218,6 +226,61 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { if (newMode === NoteDisplayMode.DocOnly) { this.edgeless.service.selection.clear(); } + + const abortController = new AbortController(); + const clear = () => { + this.doc.history.off('stack-item-added', addHandler); + this.doc.history.off('stack-item-popped', popHandler); + disposable.dispose(); + }; + const closeNotify = () => { + abortController.abort(); + clear(); + }; + + const addHandler = this.doc.history.on('stack-item-added', closeNotify); + const popHandler = this.doc.history.on('stack-item-popped', closeNotify); + const disposable = this.edgeless.std.host.slots.unmounted.on(closeNotify); + + const undo = () => { + this.doc.undo(); + closeNotify(); + }; + + const viewInToc = () => { + const sidebar = this.edgeless.std.getOptional(SidebarExtensionIdentifier); + sidebar?.open('outline'); + closeNotify(); + }; + + const notification = this.edgeless.std.getOptional(NotificationProvider); + notification?.notify({ + title: 'Note displayed in Page Mode', + message: + 'Content added to your page. Find it in the TOC for quick navigation.', + accent: 'success', + duration: 1000 * 1000, + footer: html`
+ + +
`, + abort: abortController.signal, + onClose: () => { + clear(); + }, + }); } private _setShadowType(shadowType: string) { @@ -286,6 +349,11 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { const currentMode = DisplayModeMap[displayMode]; const onlyOne = len === 1; const isDocOnly = displayMode === NoteDisplayMode.DocOnly; + const isFirstNote = + onlyOne && + note.parent?.children.find(child => + matchFlavours(child, ['affine:note']) + ) === note; const theme = this.edgeless.std.get(ThemeProvider).theme; const buttons = [ onlyOne && this._advancedVisibilityEnabled @@ -315,6 +383,32 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { ` : nothing, + onlyOne && + !isFirstNote && + this._pageBlockHeaderEnabled && + !this._advancedVisibilityEnabled + ? html` + this._setDisplayMode( + note, + displayMode === NoteDisplayMode.EdgelessOnly + ? NoteDisplayMode.DocAndEdgeless + : NoteDisplayMode.EdgelessOnly + )} + > + ${LinkedPageIcon({ width: '20px', height: '20px' })} + ${displayMode === NoteDisplayMode.EdgelessOnly + ? 'Display In Page' + : 'Displayed In Page'} + ` + : nothing, + isDocOnly ? nothing : when( diff --git a/blocksuite/blocks/src/root-block/widgets/element-toolbar/styles.css.ts b/blocksuite/blocks/src/root-block/widgets/element-toolbar/styles.css.ts new file mode 100644 index 000000000..409eefeac --- /dev/null +++ b/blocksuite/blocks/src/root-block/widgets/element-toolbar/styles.css.ts @@ -0,0 +1,24 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const viewInPageNotifyFooter = style({ + display: 'flex', + justifyContent: 'flex-end', + gap: '12px', +}); + +export const viewInPageNotifyFooterButton = style({ + padding: '0px 6px', + borderRadius: '4px', + color: cssVarV2('text/primary'), + + fontSize: cssVar('fontSm'), + lineHeight: '22px', + fontWeight: '500', + textAlign: 'center', + + ':hover': { + background: cssVarV2('layer/background/hoverOverlay'), + }, +}); diff --git a/blocksuite/playground/package.json b/blocksuite/playground/package.json index 7eab83114..cdad78387 100644 --- a/blocksuite/playground/package.json +++ b/blocksuite/playground/package.json @@ -41,6 +41,7 @@ "devDependencies": { "@tweakpane/core": "^2.0.4", "@types/micromatch": "^4.0.9", + "@vanilla-extract/vite-plugin": "^4.0.19", "graphql": "^16.9.0", "magic-string": "^0.30.11", "vite": "^6.0.3", diff --git a/blocksuite/playground/vite.config.ts b/blocksuite/playground/vite.config.ts index aecfcd233..ccaee0bf4 100644 --- a/blocksuite/playground/vite.config.ts +++ b/blocksuite/playground/vite.config.ts @@ -4,6 +4,7 @@ import { cpus } from 'node:os'; import path, { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import type { GetManualChunk } from 'rollup'; import type { Plugin } from 'vite'; import { defineConfig, loadEnv } from 'vite'; @@ -187,6 +188,7 @@ export default defineConfig(({ mode }) => { forceBuildInstrument: true, }), wasm(), + vanillaExtractPlugin(), clearSiteDataPlugin(), ], esbuild: { diff --git a/blocksuite/presets/package.json b/blocksuite/presets/package.json index 6602e2604..9426957eb 100644 --- a/blocksuite/presets/package.json +++ b/blocksuite/presets/package.json @@ -40,6 +40,7 @@ "!dist/__tests__" ], "devDependencies": { + "@vanilla-extract/vite-plugin": "^4.0.19", "vitest": "^3.0.0" }, "version": "0.19.0" diff --git a/blocksuite/presets/src/__tests__/utils/setup.ts b/blocksuite/presets/src/__tests__/utils/setup.ts index 6990a3b0d..db87be4da 100644 --- a/blocksuite/presets/src/__tests__/utils/setup.ts +++ b/blocksuite/presets/src/__tests__/utils/setup.ts @@ -77,6 +77,7 @@ async function createEditor(collection: TestWorkspace, mode: DocMode = 'page') { app.style.width = '100%'; app.style.height = '1280px'; + app.style.overflowY = 'auto'; document.body.append(app); await editor.updateComplete; diff --git a/blocksuite/presets/vitest.config.ts b/blocksuite/presets/vitest.config.ts index 4bbafd878..875d8ad4f 100644 --- a/blocksuite/presets/vitest.config.ts +++ b/blocksuite/presets/vitest.config.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import { defineConfig } from 'vitest/config'; export default defineConfig(_configEnv => @@ -14,6 +15,7 @@ export default defineConfig(_configEnv => target: 'es2022', }, }, + plugins: [vanillaExtractPlugin()], test: { include: ['src/__tests__/**/*.spec.ts'], browser: { diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx index e14023662..76a86d48c 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -225,6 +225,7 @@ export function patchNotificationService({ { title: toReactNode(notification.title), message: toReactNode(notification.message), + footer: toReactNode(notification.footer), action: notification.action?.onClick ? { label: toReactNode(notification.action?.label), diff --git a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts index f416bceaf..59aeba6f2 100644 --- a/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts +++ b/tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts @@ -1,15 +1,18 @@ import { test } from '@affine-test/kit/playwright'; import { clickEdgelessModeButton, + clickView, createEdgelessNoteBlock, getEdgelessSelectedIds, getPageMode, locateEditorContainer, locateElementToolbar, + locateModeSwitchButton, } from '@affine-test/kit/utils/editor'; import { pasteByKeyboard, selectAllByKeyboard, + undoByKeyboard, } from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; import { @@ -143,14 +146,90 @@ test.describe('edgeless page header toolbar', () => { }); test.describe('edgeless note element toolbar', () => { - test('the toolbar of page block should not contains auto-height', async ({ + test('the toolbar of page block should not contains auto-height button and display in page button', async ({ page, }) => { await selectAllByKeyboard(page); const toolbar = locateElementToolbar(page); const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); + const displayInPage = toolbar.getByTestId('display-in-page'); await expect(toolbar).toBeVisible(); await expect(autoHeight).toHaveCount(0); + await expect(displayInPage).toHaveCount(0); + }); + + test('the toolbar of note block should contains auto-height button and display in page button', async ({ + page, + }) => { + await createEdgelessNoteBlock(page, [100, 100]); + await page.waitForSelector('.affine-paragraph-placeholder.visible'); + await clickView(page, [0, 0]); + await clickView(page, [100, 100]); + + const toolbar = locateElementToolbar(page); + const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); + const displayInPage = toolbar.getByTestId('display-in-page'); + + await expect(toolbar).toBeVisible(); + await expect(autoHeight).toBeVisible(); + await expect(displayInPage).toBeVisible(); + }); + + test('display in page button', async ({ page }) => { + const editorContainer = locateEditorContainer(page); + const notes = editorContainer.locator('affine-note'); + + await createEdgelessNoteBlock(page, [100, 100]); + await page.waitForSelector('.affine-paragraph-placeholder.visible'); + await page.keyboard.type('Note 2'); + await clickView(page, [0, 0]); + await clickView(page, [100, 100]); + + const toolbar = locateElementToolbar(page); + const displayInPage = toolbar.getByTestId('display-in-page'); + + await displayInPage.click(); + await locateModeSwitchButton(page, 'page').click(); + expect(notes).toHaveCount(2); + + await locateModeSwitchButton(page, 'edgeless').click(); + await clickView(page, [100, 100]); + await displayInPage.click(); + await locateModeSwitchButton(page, 'page').click(); + await waitForEditorLoad(page); + expect(notes).toHaveCount(1); + + const undoButton = page.getByTestId('undo-display-in-page'); + const viewTocButton = page.getByTestId('view-in-toc'); + + await locateModeSwitchButton(page, 'edgeless').click(); + await waitForEditorLoad(page); + await clickView(page, [100, 100]); + await displayInPage.click(); + expect(undoButton).toBeVisible(); + expect(viewTocButton).toBeVisible(); + + await undoButton.click(); + await expect(undoButton).toBeHidden(); + await locateModeSwitchButton(page, 'page').click(); + await waitForEditorLoad(page); + expect(notes).toHaveCount(1); + + await locateModeSwitchButton(page, 'edgeless').click(); + await waitForEditorLoad(page); + await clickView(page, [100, 100]); + await displayInPage.click(); + await undoByKeyboard(page); + await page.waitForTimeout(500); + expect( + undoButton, + 'the toast should be hidden immediately when undo by keyboard' + ).toBeHidden(); + + await displayInPage.click(); + await viewTocButton.click(); + await page.waitForSelector('affine-outline-panel'); + expect(page.locator('affine-outline-panel')).toBeVisible(); }); }); diff --git a/tests/kit/src/utils/keyboard.ts b/tests/kit/src/utils/keyboard.ts index 149fb6e24..814f0b2ca 100644 --- a/tests/kit/src/utils/keyboard.ts +++ b/tests/kit/src/utils/keyboard.ts @@ -70,6 +70,12 @@ export async function selectAllByKeyboard(page: Page) { await keyUpCtrlOrMeta(page); } +export async function undoByKeyboard(page: Page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('z', { delay: 50 }); + await keyUpCtrlOrMeta(page); +} + export async function writeTextToClipboard(page: Page, text: string) { // paste the url await page.evaluate( diff --git a/yarn.lock b/yarn.lock index 924191d45..0bbd4c648 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3910,6 +3910,7 @@ __metadata: "@toeverything/theme": "npm:^1.1.6" "@types/katex": "npm:^0.16.7" "@types/lodash.isequal": "npm:^4.5.8" + "@vanilla-extract/css": "npm:^1.17.0" date-fns: "npm:^4.0.0" dompurify: "npm:^3.1.6" fflate: "npm:^0.8.2" @@ -4034,6 +4035,7 @@ __metadata: "@tweakpane/core": "npm:^2.0.4" "@types/katex": "npm:^0.16.7" "@types/micromatch": "npm:^4.0.9" + "@vanilla-extract/vite-plugin": "npm:^4.0.19" browser-fs-access: "npm:^0.35.0" graphql: "npm:^16.9.0" jszip: "npm:^3.10.1" @@ -4068,6 +4070,7 @@ __metadata: "@lottiefiles/dotlottie-wc": "npm:^0.4.0" "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.6" + "@vanilla-extract/vite-plugin": "npm:^4.0.19" lit: "npm:^3.2.0" vitest: "npm:^3.0.0" yjs: "npm:^13.6.21" @@ -15530,7 +15533,7 @@ __metadata: languageName: node linkType: hard -"@vanilla-extract/vite-plugin@npm:^4.0.18": +"@vanilla-extract/vite-plugin@npm:^4.0.18, @vanilla-extract/vite-plugin@npm:^4.0.19": version: 4.0.19 resolution: "@vanilla-extract/vite-plugin@npm:4.0.19" dependencies: