feat(core): set doc mode and primary doc mode separately (#8359)

https://github.com/user-attachments/assets/98c282f2-4c53-475f-bf10-936a626c2630
This commit is contained in:
JimmFly 2024-10-17 13:48:45 +00:00
parent 7dae5c5dd5
commit bfb8d582ed
No known key found for this signature in database
GPG Key ID: 126E0320FEB0D05C
15 changed files with 192 additions and 90 deletions

View File

@ -11,4 +11,9 @@ export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
type: 'tags',
index: 'a0000001',
},
{
id: 'docPrimaryMode',
type: 'docPrimaryMode',
show: 'always-hide',
},
] as DocCustomPropertyInfo[];

View File

@ -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'),

View File

@ -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 = ({
</MenuItem>
)}
<MenuItem
prefixIcon={currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
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']()}
</MenuItem>
<MenuItem
data-testid="editor-option-menu-favorite"

View File

@ -1,4 +1,4 @@
import { RadioGroup, type RadioItem, toast, Tooltip } from '@affine/component';
import { RadioGroup, type RadioItem, Tooltip } from '@affine/component';
import { registerAffineCommand } from '@affine/core/commands';
import { EditorService } from '@affine/core/modules/editor';
import { useI18n } from '@affine/i18n';
@ -41,19 +41,15 @@ export const EditorModeSwitch = () => {
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) => {

View File

@ -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,
{

View File

@ -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<RadioItem[]>(
() => [
{
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 (
<PropertyValue>
<RadioGroup
width={194}
value={primaryMode}
onChange={handleChange}
items={DocModeItems}
/>
</PropertyValue>
);
};

View File

@ -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 = (
<>
<MobileMenuItem
prefixIcon={currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
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']()}
</MobileMenuItem>
<MobileMenuItem
data-testid="editor-option-menu-favorite"

View File

@ -1,12 +1,12 @@
{
"ar": 87,
"ar": 86,
"ca": 6,
"da": 7,
"de": 32,
"en": 100,
"es-AR": 16,
"es-AR": 15,
"es-CL": 18,
"es": 16,
"es": 15,
"fr": 77,
"hi": 2,
"it": 1,
@ -14,9 +14,9 @@
"ko": 92,
"pl": 0,
"pt-BR": 99,
"ru": 85,
"ru": 84,
"sv-SE": 5,
"ur": 3,
"zh-Hans": 100,
"zh-Hant": 100
"zh-Hant": 99
}

View File

@ -92,6 +92,7 @@
"Successfully deleted": "Successfully deleted",
"Successfully joined!": "Successfully joined!",
"Switch": "Switch",
"switchView": "Switch view",
"Sync": "Sync",
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
"Tags": "Tags",
@ -425,6 +426,8 @@
"com.affine.editCollectionName.createTips": "Collection is a smart folder where you can manually add docs or automatically add docs through rules.",
"com.affine.editCollectionName.name": "Name",
"com.affine.editCollectionName.name.placeholder": "Collection name",
"com.affine.editorDefaultMode.edgeless": "Default to Edgeless mode",
"com.affine.editorDefaultMode.page": "Default to Page mode",
"com.affine.empty.collection-detail.action.add-doc": "Add docs",
"com.affine.empty.collection-detail.action.add-rule": "Add rules",
"com.affine.empty.collection-detail.description": "Collection is a smart folder where you can manually add docs or automatically add docs through rules.",
@ -527,7 +530,7 @@
"com.affine.keyboardShortcuts.elbowedConnector": "Elbowed connector",
"com.affine.keyboardShortcuts.expandOrCollapseSidebar": "Expand/collapse sidebar",
"com.affine.keyboardShortcuts.goBack": "Go back",
"com.affine.keyboardShortcuts.goForward": "Go Forward",
"com.affine.keyboardShortcuts.goForward": "Go forward",
"com.affine.keyboardShortcuts.group": "Group",
"com.affine.keyboardShortcuts.groupDatabase": "Group as database",
"com.affine.keyboardShortcuts.hand": "Hand",
@ -551,7 +554,7 @@
"com.affine.keyboardShortcuts.straightConnector": "Straight connector",
"com.affine.keyboardShortcuts.strikethrough": "Strikethrough",
"com.affine.keyboardShortcuts.subtitle": "Check keyboard shortcuts quickly",
"com.affine.keyboardShortcuts.switch": "Switch",
"com.affine.keyboardShortcuts.switch": "Switch view",
"com.affine.keyboardShortcuts.text": "Text",
"com.affine.keyboardShortcuts.title": "Keyboard shortcuts",
"com.affine.keyboardShortcuts.unGroup": "Ungroup",
@ -647,6 +650,7 @@
"com.affine.page-properties.property.required": "Required",
"com.affine.page-properties.property.show-in-view": "Show in view",
"com.affine.page-properties.property.tags": "Tags",
"com.affine.page-properties.property.docPrimaryMode": "Doc mode",
"com.affine.page-properties.property.text": "Text",
"com.affine.page-properties.property.updatedBy": "Last edited by",
"com.affine.propertySidebar.property-list.section": "Properties",
@ -1213,6 +1217,10 @@
"com.affine.toastMessage.edgelessMode": "Edgeless mode",
"com.affine.toastMessage.movedTrash": "Moved to trash",
"com.affine.toastMessage.pageMode": "Page Mode",
"com.affine.toastMessage.defaultMode.page.title": "Default mode has changed",
"com.affine.toastMessage.defaultMode.page.message": "The default mode for this document has been changed to Page mode",
"com.affine.toastMessage.defaultMode.edgeless.title": "Default mode has changed",
"com.affine.toastMessage.defaultMode.edgeless.message": "The default mode for this document has been changed to Edgeless mode",
"com.affine.toastMessage.permanentlyDeleted": "Permanently deleted",
"com.affine.toastMessage.removedFavorites": "Removed from favourites",
"com.affine.toastMessage.rename": "Successfully renamed",

View File

@ -682,7 +682,6 @@
"com.affine.editCollectionName.name": "이름",
"com.affine.editCollectionName.name.placeholder": "컬렉션 이름",
"com.affine.editor.reference-not-found": "연결된 페이지를 찾을 수 없음",
"com.affine.editorModeSwitch.tooltip": "전환",
"com.affine.empty.collection-detail.action.add-doc": "문서 추가",
"com.affine.empty.collection-detail.action.add-rule": "규칙 추가",
"com.affine.empty.collection-detail.description": "컬렉션은 페이지를 수동으로 추가하거나 규칙을 통해 자동으로 페이지를 추가할 수 있는 스마트 폴더입니다.",

View File

@ -92,6 +92,7 @@
"Successfully deleted": "成功删除。",
"Successfully joined!": "加入成功!",
"Switch": "切换",
"switchView": "切换视图",
"Sync": "同步",
"Synced with AFFiNE Cloud": "AFFiNE Cloud 同步完成",
"Tags": "标签",
@ -438,6 +439,9 @@
"com.affine.empty.docs.trash-description": "已删除的文档将显示在此处。",
"com.affine.empty.tags.description": "为您的文档创建一个新标签。",
"com.affine.empty.tags.title": "标签管理",
"com.affine.editorDefaultMode.edgeless": "默认为无界模式",
"com.affine.editorDefaultMode.page": "默认为文档模式",
"com.affine.editor.reference-not-found": "未找到已链接的文档",
"com.affine.emptyDesc": "这里还没有文档",
"com.affine.enableAffineCloudModal.button.cancel": "取消",
"com.affine.error.hide-error": "隐藏错误详情",
@ -551,7 +555,7 @@
"com.affine.keyboardShortcuts.straightConnector": "直线连接器快捷键",
"com.affine.keyboardShortcuts.strikethrough": "删除线",
"com.affine.keyboardShortcuts.subtitle": "快速查看快捷键",
"com.affine.keyboardShortcuts.switch": "切换快捷键",
"com.affine.keyboardShortcuts.switch": "切换视图快捷键",
"com.affine.keyboardShortcuts.text": "文本",
"com.affine.keyboardShortcuts.title": "键盘快捷键",
"com.affine.keyboardShortcuts.unGroup": "取消分组",
@ -642,6 +646,7 @@
"com.affine.page-properties.property.required": "必选",
"com.affine.page-properties.property.show-in-view": "在视图中显示",
"com.affine.page-properties.property.tags": "标签",
"com.affine.page-properties.property.docPrimaryMode": "文档模式",
"com.affine.page-properties.property.text": "文本",
"com.affine.page-properties.property.updatedBy": "最后编辑者",
"com.affine.page-properties.settings.title": "自定义属性",
@ -1206,6 +1211,10 @@
"com.affine.toastMessage.rename": "更名成功",
"com.affine.toastMessage.restored": "{{title}} 已恢复",
"com.affine.toastMessage.successfullyDeleted": "删除成功",
"com.affine.toastMessage.defaultMode.page.title": "默认模式已更改",
"com.affine.toastMessage.defaultMode.page.message": "该文档的默认模式已更改为页面模式",
"com.affine.toastMessage.defaultMode.edgeless.title": "默认模式已更改",
"com.affine.toastMessage.defaultMode.edgeless.message": "该文档的默认模式已更改为无界模式",
"com.affine.today": "今天",
"com.affine.tomorrow": "明日",
"com.affine.top-tip.mobile": "在移动设备上仅限于查看。",

View File

@ -8,56 +8,20 @@ import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
waitForAllPagesLoad,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import { expect } from '@playwright/test';
test('Switch to edgeless by switch edgeless item', async ({ page }) => {
async function getCount(): Promise<number> {
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();
});

View File

@ -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');

View File

@ -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();
});

View File

@ -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(