L-Sun
2025-01-19 08:35:02 +00:00
parent 9d61b41c05
commit 6ba802fb17
13 changed files with 224 additions and 8 deletions
@@ -29,6 +29,7 @@ export interface NotificationService {
notify(options: { notify(options: {
title: string | TemplateResult; title: string | TemplateResult;
message?: string | TemplateResult; message?: string | TemplateResult;
footer?: string | TemplateResult;
accent?: 'info' | 'success' | 'warning' | 'error'; accent?: 'info' | 'success' | 'warning' | 'error';
duration?: number; // unit ms, give 0 to disable auto dismiss duration?: number; // unit ms, give 0 to disable auto dismiss
abort?: AbortSignal; abort?: AbortSignal;
+1
View File
@@ -47,6 +47,7 @@
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.6", "@toeverything/theme": "^1.1.6",
"@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0", "date-fns": "^4.0.0",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"fflate": "^0.8.2", "fflate": "^0.8.2",
@@ -30,16 +30,18 @@ import {
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
FeatureFlagService, FeatureFlagService,
NotificationProvider,
SidebarExtensionIdentifier,
ThemeProvider, ThemeProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { matchFlavours } from '@blocksuite/affine-shared/utils';
import { import {
assertExists,
Bound, Bound,
countBy, countBy,
maxBy, maxBy,
WithDisposable, WithDisposable,
} from '@blocksuite/global/utils'; } from '@blocksuite/global/utils';
import { LinkedPageIcon } from '@blocksuite/icons/lit';
import { html, LitElement, nothing, type TemplateResult } from 'lit'; import { html, LitElement, nothing, type TemplateResult } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { join } from 'lit/directives/join.js'; import { join } from 'lit/directives/join.js';
@@ -52,6 +54,7 @@ import {
} from '../../edgeless/components/panel/line-styles-panel.js'; } from '../../edgeless/components/panel/line-styles-panel.js';
import { getTooltipWithShortcut } from '../../edgeless/components/utils.js'; import { getTooltipWithShortcut } from '../../edgeless/components/utils.js';
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
import * as styles from './styles.css';
const SIZE_LIST = [ const SIZE_LIST = [
{ name: 'None', value: 0 }, { name: 'None', value: 0 },
@@ -141,15 +144,17 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
.getFlag('enable_advanced_block_visibility'); .getFlag('enable_advanced_block_visibility');
} }
private get _pageBlockHeaderEnabled() {
return this.doc.get(FeatureFlagService).getFlag('enable_page_block_header');
}
private get doc() { private get doc() {
return this.edgeless.doc; return this.edgeless.doc;
} }
private get _enableAutoHeight() { private get _enableAutoHeight() {
return !( return !(
this.edgeless.doc this._pageBlockHeaderEnabled &&
.get(FeatureFlagService)
.getFlag('enable_page_block_header') &&
this.notes.length === 1 && this.notes.length === 1 &&
this.notes[0].parent?.children.find(child => this.notes[0].parent?.children.find(child =>
matchFlavours(child, ['affine:note']) matchFlavours(child, ['affine:note'])
@@ -195,13 +200,16 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
return; return;
} }
this.doc.captureSync();
this.crud.updateElement(note.id, { displayMode: newMode }); this.crud.updateElement(note.id, { displayMode: newMode });
const noteParent = this.doc.getParent(note); const noteParent = this.doc.getParent(note);
assertExists(noteParent); if (!noteParent) return;
const noteParentChildNotes = noteParent.children.filter(block => const noteParentChildNotes = noteParent.children.filter(block =>
matchFlavours(block, ['affine:note']) matchFlavours(block, ['affine:note'])
) as NoteBlockModel[]; );
const noteParentLastNote = const noteParentLastNote =
noteParentChildNotes[noteParentChildNotes.length - 1]; noteParentChildNotes[noteParentChildNotes.length - 1];
@@ -218,6 +226,61 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
if (newMode === NoteDisplayMode.DocOnly) { if (newMode === NoteDisplayMode.DocOnly) {
this.edgeless.service.selection.clear(); 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`<div class=${styles.viewInPageNotifyFooter}>
<button
class=${styles.viewInPageNotifyFooterButton}
@click=${undo}
data-testid="undo-display-in-page"
>
Undo
</button>
<button
class=${styles.viewInPageNotifyFooterButton}
@click=${viewInToc}
data-testid="view-in-toc"
>
View in Toc
</button>
</div>`,
abort: abortController.signal,
onClose: () => {
clear();
},
});
} }
private _setShadowType(shadowType: string) { private _setShadowType(shadowType: string) {
@@ -286,6 +349,11 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
const currentMode = DisplayModeMap[displayMode]; const currentMode = DisplayModeMap[displayMode];
const onlyOne = len === 1; const onlyOne = len === 1;
const isDocOnly = displayMode === NoteDisplayMode.DocOnly; 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 theme = this.edgeless.std.get(ThemeProvider).theme;
const buttons = [ const buttons = [
onlyOne && this._advancedVisibilityEnabled onlyOne && this._advancedVisibilityEnabled
@@ -315,6 +383,32 @@ export class EdgelessChangeNoteButton extends WithDisposable(LitElement) {
` `
: nothing, : nothing,
onlyOne &&
!isFirstNote &&
this._pageBlockHeaderEnabled &&
!this._advancedVisibilityEnabled
? html`<editor-icon-button
aria-label="Display In Page"
.showTooltip=${displayMode === NoteDisplayMode.DocAndEdgeless}
.tooltip=${'This note is part of Page Mode. Click to remove it from the page.'}
data-testid="display-in-page"
@click=${() =>
this._setDisplayMode(
note,
displayMode === NoteDisplayMode.EdgelessOnly
? NoteDisplayMode.DocAndEdgeless
: NoteDisplayMode.EdgelessOnly
)}
>
${LinkedPageIcon({ width: '20px', height: '20px' })}
<span class="label"
>${displayMode === NoteDisplayMode.EdgelessOnly
? 'Display In Page'
: 'Displayed In Page'}</span
>
</editor-icon-button>`
: nothing,
isDocOnly isDocOnly
? nothing ? nothing
: when( : when(
@@ -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'),
},
});
+1
View File
@@ -41,6 +41,7 @@
"devDependencies": { "devDependencies": {
"@tweakpane/core": "^2.0.4", "@tweakpane/core": "^2.0.4",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@vanilla-extract/vite-plugin": "^4.0.19",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"vite": "^6.0.3", "vite": "^6.0.3",
+2
View File
@@ -4,6 +4,7 @@ import { cpus } from 'node:os';
import path, { resolve } from 'node:path'; import path, { resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import type { GetManualChunk } from 'rollup'; import type { GetManualChunk } from 'rollup';
import type { Plugin } from 'vite'; import type { Plugin } from 'vite';
import { defineConfig, loadEnv } from 'vite'; import { defineConfig, loadEnv } from 'vite';
@@ -187,6 +188,7 @@ export default defineConfig(({ mode }) => {
forceBuildInstrument: true, forceBuildInstrument: true,
}), }),
wasm(), wasm(),
vanillaExtractPlugin(),
clearSiteDataPlugin(), clearSiteDataPlugin(),
], ],
esbuild: { esbuild: {
+1
View File
@@ -40,6 +40,7 @@
"!dist/__tests__" "!dist/__tests__"
], ],
"devDependencies": { "devDependencies": {
"@vanilla-extract/vite-plugin": "^4.0.19",
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"version": "0.19.0" "version": "0.19.0"
@@ -77,6 +77,7 @@ async function createEditor(collection: TestWorkspace, mode: DocMode = 'page') {
app.style.width = '100%'; app.style.width = '100%';
app.style.height = '1280px'; app.style.height = '1280px';
app.style.overflowY = 'auto';
document.body.append(app); document.body.append(app);
await editor.updateComplete; await editor.updateComplete;
+2
View File
@@ -1,6 +1,7 @@
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
export default defineConfig(_configEnv => export default defineConfig(_configEnv =>
@@ -14,6 +15,7 @@ export default defineConfig(_configEnv =>
target: 'es2022', target: 'es2022',
}, },
}, },
plugins: [vanillaExtractPlugin()],
test: { test: {
include: ['src/__tests__/**/*.spec.ts'], include: ['src/__tests__/**/*.spec.ts'],
browser: { browser: {
@@ -225,6 +225,7 @@ export function patchNotificationService({
{ {
title: toReactNode(notification.title), title: toReactNode(notification.title),
message: toReactNode(notification.message), message: toReactNode(notification.message),
footer: toReactNode(notification.footer),
action: notification.action?.onClick action: notification.action?.onClick
? { ? {
label: toReactNode(notification.action?.label), label: toReactNode(notification.action?.label),
@@ -1,15 +1,18 @@
import { test } from '@affine-test/kit/playwright'; import { test } from '@affine-test/kit/playwright';
import { import {
clickEdgelessModeButton, clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock, createEdgelessNoteBlock,
getEdgelessSelectedIds, getEdgelessSelectedIds,
getPageMode, getPageMode,
locateEditorContainer, locateEditorContainer,
locateElementToolbar, locateElementToolbar,
locateModeSwitchButton,
} from '@affine-test/kit/utils/editor'; } from '@affine-test/kit/utils/editor';
import { import {
pasteByKeyboard, pasteByKeyboard,
selectAllByKeyboard, selectAllByKeyboard,
undoByKeyboard,
} from '@affine-test/kit/utils/keyboard'; } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page'; import { openHomePage } from '@affine-test/kit/utils/load-page';
import { import {
@@ -143,14 +146,90 @@ test.describe('edgeless page header toolbar', () => {
}); });
test.describe('edgeless note element 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, page,
}) => { }) => {
await selectAllByKeyboard(page); await selectAllByKeyboard(page);
const toolbar = locateElementToolbar(page); const toolbar = locateElementToolbar(page);
const autoHeight = toolbar.getByTestId('edgeless-note-auto-height'); const autoHeight = toolbar.getByTestId('edgeless-note-auto-height');
const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible(); await expect(toolbar).toBeVisible();
await expect(autoHeight).toHaveCount(0); 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();
}); });
}); });
+6
View File
@@ -70,6 +70,12 @@ export async function selectAllByKeyboard(page: Page) {
await keyUpCtrlOrMeta(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) { export async function writeTextToClipboard(page: Page, text: string) {
// paste the url // paste the url
await page.evaluate( await page.evaluate(
+4 -1
View File
@@ -3910,6 +3910,7 @@ __metadata:
"@toeverything/theme": "npm:^1.1.6" "@toeverything/theme": "npm:^1.1.6"
"@types/katex": "npm:^0.16.7" "@types/katex": "npm:^0.16.7"
"@types/lodash.isequal": "npm:^4.5.8" "@types/lodash.isequal": "npm:^4.5.8"
"@vanilla-extract/css": "npm:^1.17.0"
date-fns: "npm:^4.0.0" date-fns: "npm:^4.0.0"
dompurify: "npm:^3.1.6" dompurify: "npm:^3.1.6"
fflate: "npm:^0.8.2" fflate: "npm:^0.8.2"
@@ -4034,6 +4035,7 @@ __metadata:
"@tweakpane/core": "npm:^2.0.4" "@tweakpane/core": "npm:^2.0.4"
"@types/katex": "npm:^0.16.7" "@types/katex": "npm:^0.16.7"
"@types/micromatch": "npm:^4.0.9" "@types/micromatch": "npm:^4.0.9"
"@vanilla-extract/vite-plugin": "npm:^4.0.19"
browser-fs-access: "npm:^0.35.0" browser-fs-access: "npm:^0.35.0"
graphql: "npm:^16.9.0" graphql: "npm:^16.9.0"
jszip: "npm:^3.10.1" jszip: "npm:^3.10.1"
@@ -4068,6 +4070,7 @@ __metadata:
"@lottiefiles/dotlottie-wc": "npm:^0.4.0" "@lottiefiles/dotlottie-wc": "npm:^0.4.0"
"@preact/signals-core": "npm:^1.8.0" "@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.6" "@toeverything/theme": "npm:^1.1.6"
"@vanilla-extract/vite-plugin": "npm:^4.0.19"
lit: "npm:^3.2.0" lit: "npm:^3.2.0"
vitest: "npm:^3.0.0" vitest: "npm:^3.0.0"
yjs: "npm:^13.6.21" yjs: "npm:^13.6.21"
@@ -15530,7 +15533,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 4.0.19
resolution: "@vanilla-extract/vite-plugin@npm:4.0.19" resolution: "@vanilla-extract/vite-plugin@npm:4.0.19"
dependencies: dependencies: