diff --git a/packages/common/infra/src/modules/doc/constants.ts b/packages/common/infra/src/modules/doc/constants.ts index 31a526fa8583b..cf1081908168a 100644 --- a/packages/common/infra/src/modules/doc/constants.ts +++ b/packages/common/infra/src/modules/doc/constants.ts @@ -11,4 +11,9 @@ export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [ type: 'tags', index: 'a0000001', }, + { + id: 'docPrimaryMode', + type: 'docPrimaryMode', + show: 'always-hide', + }, ] as DocCustomPropertyInfo[]; diff --git a/packages/frontend/component/src/ui/property/property.css.ts b/packages/frontend/component/src/ui/property/property.css.ts index a13f88d2b9361..a0a28e4c58ac7 100644 --- a/packages/frontend/component/src/ui/property/property.css.ts +++ b/packages/frontend/component/src/ui/property/property.css.ts @@ -65,6 +65,7 @@ export const hide = style({ export const propertyNameContainer = style({ display: 'flex', flexDirection: 'column', + justifyContent: 'center', position: 'relative', borderRadius: 4, fontSize: cssVar('fontSm'), diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx index 85b8a99dbcdaf..afe65515d8ad8 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-header/menu/index.tsx @@ -1,4 +1,4 @@ -import { toast } from '@affine/component'; +import { notify } from '@affine/component'; import { Menu, MenuItem, @@ -74,6 +74,7 @@ export const PageHeaderMenuButton = ({ editorService.editor.doc.meta$.map(meta => meta.trash) ); const currentMode = useLiveData(editorService.editor.mode$); + const primaryMode = useLiveData(editorService.editor.doc.primaryMode$); const workbench = useService(WorkbenchService).workbench; @@ -148,16 +149,22 @@ export const PageHeaderMenuButton = ({ }, [rename]); const handleSwitchMode = useCallback(() => { - editorService.editor.toggleMode(); + const mode = primaryMode === 'page' ? 'edgeless' : 'page'; + editorService.editor.doc.setPrimaryMode(mode); track.$.header.docOptions.switchPageMode({ - mode: currentMode === 'page' ? 'edgeless' : 'page', + mode, }); - toast( - currentMode === 'page' - ? t['com.affine.toastMessage.edgelessMode']() - : t['com.affine.toastMessage.pageMode']() - ); - }, [currentMode, editorService, t]); + notify.success({ + title: + primaryMode === 'page' + ? t['com.affine.toastMessage.defaultMode.edgeless.title']() + : t['com.affine.toastMessage.defaultMode.page.title'](), + message: + primaryMode === 'page' + ? t['com.affine.toastMessage.defaultMode.edgeless.message']() + : t['com.affine.toastMessage.defaultMode.page.message'](), + }); + }, [primaryMode, editorService, t]); const handleMenuOpenChange = useCallback((open: boolean) => { if (open) { @@ -264,14 +271,13 @@ export const PageHeaderMenuButton = ({ )} : } + prefixIcon={primaryMode === 'page' ? : } data-testid="editor-option-menu-edgeless" onSelect={handleSwitchMode} > - {t['Convert to ']()} - {currentMode === 'page' - ? t['com.affine.pageMode.edgeless']() - : t['com.affine.pageMode.page']()} + {primaryMode === 'page' + ? t['com.affine.editorDefaultMode.edgeless']() + : t['com.affine.editorDefaultMode.page']()} { if (currentMode === 'page' || isSharedMode || trash) return; editor.setMode('page'); editor.setSelector(undefined); - editor.doc.setPrimaryMode('page'); - toast(t['com.affine.toastMessage.pageMode']()); track.$.header.actions.switchPageMode({ mode: 'page' }); - }, [currentMode, editor, isSharedMode, t, trash]); + }, [currentMode, editor, isSharedMode, trash]); const toggleEdgeless = useCallback(() => { if (currentMode === 'edgeless' || isSharedMode || trash) return; editor.setMode('edgeless'); editor.setSelector(undefined); - editor.doc.setPrimaryMode('edgeless'); - toast(t['com.affine.toastMessage.edgelessMode']()); track.$.header.actions.switchPageMode({ mode: 'edgeless' }); - }, [currentMode, editor, isSharedMode, t, trash]); + }, [currentMode, editor, isSharedMode, trash]); const onModeChange = useCallback( (mode: DocMode) => { diff --git a/packages/frontend/core/src/components/doc-properties/types/constant.tsx b/packages/frontend/core/src/components/doc-properties/types/constant.tsx index 2c8c4f73c8d16..c442aca8b01d4 100644 --- a/packages/frontend/core/src/components/doc-properties/types/constant.tsx +++ b/packages/frontend/core/src/components/doc-properties/types/constant.tsx @@ -3,6 +3,7 @@ import { CheckBoxCheckLinearIcon, CreatedEditedIcon, DateTimeIcon, + FileIcon, NumberIcon, TagIcon, TextIcon, @@ -11,6 +12,7 @@ import { import { CheckboxValue } from './checkbox'; import { CreatedByValue, UpdatedByValue } from './created-updated-by'; import { DateValue } from './date'; +import { DocPrimaryModeValue } from './doc-primary-mode'; import { NumberValue } from './number'; import { TagsValue } from './tags'; import { TextValue } from './text'; @@ -54,6 +56,11 @@ export const DocPropertyTypes = { uniqueId: 'tags', renameable: false, }, + docPrimaryMode: { + icon: FileIcon, + value: DocPrimaryModeValue, + name: 'com.affine.page-properties.property.docPrimaryMode', + }, } as Record< string, { diff --git a/packages/frontend/core/src/components/doc-properties/types/doc-primary-mode.tsx b/packages/frontend/core/src/components/doc-properties/types/doc-primary-mode.tsx new file mode 100644 index 0000000000000..81882d7b3e9ce --- /dev/null +++ b/packages/frontend/core/src/components/doc-properties/types/doc-primary-mode.tsx @@ -0,0 +1,58 @@ +import { + notify, + PropertyValue, + RadioGroup, + type RadioItem, +} from '@affine/component'; +import { useI18n } from '@affine/i18n'; +import type { DocMode } from '@blocksuite/affine/blocks'; +import { DocService, useLiveData, useService } from '@toeverything/infra'; +import { useCallback, useMemo } from 'react'; + +export const DocPrimaryModeValue = () => { + const t = useI18n(); + const doc = useService(DocService).doc; + + const primaryMode = useLiveData(doc.primaryMode$); + + const DocModeItems = useMemo( + () => [ + { + value: 'page' as DocMode, + label: t['Page'](), + }, + { + value: 'edgeless' as DocMode, + label: t['Edgeless'](), + }, + ], + [t] + ); + + const handleChange = useCallback( + (mode: DocMode) => { + doc.setPrimaryMode(mode); + notify.success({ + title: + mode === 'page' + ? t['com.affine.toastMessage.defaultMode.page.title']() + : t['com.affine.toastMessage.defaultMode.edgeless.title'](), + message: + mode === 'page' + ? t['com.affine.toastMessage.defaultMode.page.message']() + : t['com.affine.toastMessage.defaultMode.edgeless.message'](), + }); + }, + [doc, t] + ); + return ( + + + + ); +}; diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx index 7047c0a9b12f0..321cf554253d5 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx @@ -1,4 +1,4 @@ -import { IconButton, toast } from '@affine/component'; +import { IconButton, notify } from '@affine/component'; import { MenuSeparator, MobileMenu, @@ -39,21 +39,29 @@ export const PageHeaderMenuButton = () => { const isInTrash = useLiveData( editorService.editor.doc.meta$.map(meta => meta.trash) ); - const currentMode = useLiveData(editorService.editor.mode$); + const primaryMode = useLiveData(editorService.editor.doc.primaryMode$); const { favorite, toggleFavorite } = useFavorite(docId); const handleSwitchMode = useCallback(() => { - editorService.editor.toggleMode(); + const mode = primaryMode === 'page' ? 'edgeless' : 'page'; + // TODO(@JimmFly): remove setMode when there has view mode switch + editorService.editor.setMode(mode); + editorService.editor.doc.setPrimaryMode(mode); track.$.header.docOptions.switchPageMode({ - mode: currentMode === 'page' ? 'edgeless' : 'page', + mode, }); - toast( - currentMode === 'page' - ? t['com.affine.toastMessage.edgelessMode']() - : t['com.affine.toastMessage.pageMode']() - ); - }, [currentMode, editorService, t]); + notify.success({ + title: + primaryMode === 'page' + ? t['com.affine.toastMessage.defaultMode.edgeless.title']() + : t['com.affine.toastMessage.defaultMode.page.title'](), + message: + primaryMode === 'page' + ? t['com.affine.toastMessage.defaultMode.edgeless.message']() + : t['com.affine.toastMessage.defaultMode.page.message'](), + }); + }, [primaryMode, editorService, t]); const handleMenuOpenChange = useCallback((open: boolean) => { if (open) { @@ -75,14 +83,13 @@ export const PageHeaderMenuButton = () => { const EditMenu = ( <> : } + prefixIcon={primaryMode === 'page' ? : } data-testid="editor-option-menu-mode-switch" onSelect={handleSwitchMode} > - {t['Convert to ']()} - {currentMode === 'page' - ? t['com.affine.pageMode.edgeless']() - : t['com.affine.pageMode.page']()} + {primaryMode === 'page' + ? t['com.affine.editorDefaultMode.edgeless']() + : t['com.affine.editorDefaultMode.page']()} { - async function getCount(): Promise { - return page.evaluate(() => { - // @ts-expect-error - return globalThis.__toastCount; - }); - } await openHomePage(page); await waitForEditorLoad(page); await clickNewPageButton(page); const btn = page.getByTestId('switch-edgeless-mode-button'); - await page.evaluate(() => { - // @ts-expect-error - globalThis.__toastCount = 0; - window.addEventListener('affine-toast:emit', () => { - // @ts-expect-error - globalThis.__toastCount++; - }); - }); - await btn.click(); - await page.waitForTimeout(100); - { - const count = await getCount(); - expect(count).toBe(1); - } - const edgeless = page.locator('affine-edgeless-root'); - await expect(edgeless).toBeVisible(); - - const editorWrapperPadding = await page - .locator('.editor-wrapper.edgeless-mode') - .evaluate(element => { - return window.getComputedStyle(element).getPropertyValue('padding'); - }); - expect(editorWrapperPadding).toBe('0px'); - { - const count = await getCount(); - expect(count).toBe(1); - } - await btn.click(); - await btn.click(); - await btn.click(); - await page.waitForTimeout(100); - { - const count = await getCount(); - expect(count).toBe(1); - } + await btn.click({ delay: 100 }); + await ensureInEdgelessMode(page); }); test('Quick Switch Doc Mode, Doc Mode should stable', async ({ page }) => { @@ -94,13 +58,26 @@ test('Quick Switch Doc Mode, Doc Mode should stable', async ({ page }) => { expect(await getPageMode(page)).toBe('page'); }); -test('Convert to edgeless by editor header items', async ({ page }) => { +test('default to edgeless by editor header items', async ({ page }) => { await openHomePage(page); await waitForEditorLoad(page); - await clickNewPageButton(page); + await clickNewPageButton(page, 'this is a new page'); + const title = getBlockSuiteEditorTitle(page); + expect(await title.textContent()).toBe('this is a new page'); + await clickPageMoreActions(page); const menusEdgelessItem = page.getByTestId('editor-option-menu-edgeless'); await menusEdgelessItem.click({ delay: 100 }); + + await clickSideBarAllPageButton(page); + await waitForAllPagesLoad(page); + const docItem = page.locator( + `[data-testid="page-list-item"]:has-text("this is a new page")` + ); + expect(docItem).not.toBeUndefined(); + await docItem.click(); + + await waitForEditorLoad(page); const edgeless = page.locator('affine-edgeless-root'); await expect(edgeless).toBeVisible(); }); diff --git a/tests/affine-local/e2e/page-properties.spec.ts b/tests/affine-local/e2e/page-properties.spec.ts index 4ca873b4b0fa0..c1b58b6cdb5dd 100644 --- a/tests/affine-local/e2e/page-properties.spec.ts +++ b/tests/affine-local/e2e/page-properties.spec.ts @@ -22,6 +22,7 @@ import { openWorkspaceProperties, removeSelectedTag, searchAndCreateTag, + togglePropertyListVisibility, } from '@affine-test/kit/utils/properties'; import { expect } from '@playwright/test'; @@ -117,8 +118,9 @@ test('property table reordering', async ({ page }) => { 'bottom' ); - // new order should be (Tags), Number, Date, Checkbox, Text + // new order should be Doc mode, (Tags), Number, Date, Checkbox, Text for (const [index, property] of [ + 'Doc mode', 'Tags', 'Number', 'Date', @@ -157,6 +159,7 @@ test('page info show more will show all properties', async ({ page }) => { await page.click('[data-testid="property-collapsible-button"]'); for (const [index, property] of [ + 'Doc mode', 'Tags', 'Text', 'Number', @@ -179,6 +182,7 @@ test('change page properties visibility', async ({ page }) => { await addCustomProperty(page, page, 'number'); await addCustomProperty(page, page, 'date'); await addCustomProperty(page, page, 'checkbox'); + await togglePropertyListVisibility(page); // add some number to number property await clickPropertyValue(page, 'Number'); diff --git a/tests/affine-mobile/e2e/detail.spec.ts b/tests/affine-mobile/e2e/detail.spec.ts index 10ca9625d1e51..675feeb5c9336 100644 --- a/tests/affine-mobile/e2e/detail.spec.ts +++ b/tests/affine-mobile/e2e/detail.spec.ts @@ -18,7 +18,7 @@ test('switch to page mode', async ({ page }) => { await page.click('[data-testid="detail-page-header-more-button"]'); await expect(page.getByRole('dialog')).toBeVisible(); - await page.getByRole('menuitem', { name: 'convert to page' }).click(); + await page.getByRole('menuitem', { name: 'Default to Page mode' }).click(); await expect(page.locator('.doc-title-container')).toBeVisible(); }); diff --git a/tests/kit/utils/properties.ts b/tests/kit/utils/properties.ts index a52e8373b559e..5742f3208f984 100644 --- a/tests/kit/utils/properties.ts +++ b/tests/kit/utils/properties.ts @@ -93,11 +93,36 @@ export const clickAddPropertyButton = async (root: Locator | Page) => { .click(); }; +export const ensureAddPropertyButtonVisible = async ( + page: Page, + root: Locator | Page +) => { + if ( + await root + .getByRole('button', { + name: 'Add property', + }) + .isVisible() + ) { + return; + } + await togglePropertyListVisibility(page); + await page.waitForTimeout(500); + await expect( + root.getByRole('button', { name: 'Add property' }) + ).toBeVisible(); +}; + +export const togglePropertyListVisibility = async (page: Page) => { + await page.getByTestId('property-collapsible-button').click(); +}; + export const addCustomProperty = async ( page: Page, root: Locator | Page, type: string ) => { + ensureAddPropertyButtonVisible(page, root); await clickAddPropertyButton(root); await page .locator(