From 69db99636becef2892143832118f3827563b7634 Mon Sep 17 00:00:00 2001 From: JimmFly Date: Mon, 2 Oct 2023 11:22:12 +0800 Subject: [PATCH] feat(core): add editor commanads (#4514) Co-authored-by: Peng Xiao --- apps/core/src/atoms/trash-modal.ts | 13 + apps/core/src/commands/affine-navigation.tsx | 20 +- .../setting-sidebar/style.css.ts | 5 +- .../affine/share-page-modal/index.tsx | 3 + .../operation-menu.tsx | 86 ++--- .../block-suite-mode-switch/index.tsx | 66 ++-- .../block-suite-page-list/index.tsx | 15 +- .../core/src/components/pure/cmdk/main.css.ts | 16 + apps/core/src/components/pure/cmdk/main.tsx | 4 + .../workspace-slider-bar/collections/page.tsx | 17 +- .../src/components/root-app-sidebar/index.tsx | 23 +- .../affine/use-block-suite-meta-helper.ts | 28 ++ apps/core/src/hooks/affine/use-export-page.ts | 79 +++++ ...se-register-blocksuite-editor-commands.tsx | 208 ++++++++++++ .../hooks/affine/use-trash-modal-helper.ts | 27 ++ .../hooks/use-register-workspace-commands.ts | 10 +- apps/core/src/pages/workspace/detail-page.tsx | 3 +- .../src/stories/page-list.stories.tsx | 1 - .../src/stories/share-menu.stories.tsx | 2 + .../components/block-suite-editor/index.tsx | 2 - ...se-register-blocksuite-editor-commands.tsx | 35 -- .../components/page-list/all-pages-body.tsx | 1 - .../components/page-list/operation-cell.tsx | 19 +- .../page-list/operation-menu-items/export.tsx | 319 +++++------------- .../components/share-menu/share-export.tsx | 25 +- .../src/components/share-menu/share-menu.tsx | 1 + packages/i18n/src/resources/en.json | 5 +- .../e2e/local-first-collections-items.spec.ts | 1 + tests/affine-local/e2e/quick-search.spec.ts | 52 +++ 29 files changed, 673 insertions(+), 413 deletions(-) create mode 100644 apps/core/src/atoms/trash-modal.ts create mode 100644 apps/core/src/hooks/affine/use-export-page.ts create mode 100644 apps/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx create mode 100644 apps/core/src/hooks/affine/use-trash-modal-helper.ts delete mode 100644 packages/component/src/components/block-suite-editor/use-register-blocksuite-editor-commands.tsx diff --git a/apps/core/src/atoms/trash-modal.ts b/apps/core/src/atoms/trash-modal.ts new file mode 100644 index 000000000..54d83a3eb --- /dev/null +++ b/apps/core/src/atoms/trash-modal.ts @@ -0,0 +1,13 @@ +import { atom } from 'jotai'; + +export type TrashModal = { + open: boolean; + pageId: string; + pageTitle: string; +}; + +export const trashModalAtom = atom({ + open: false, + pageId: '', + pageTitle: '', +}); diff --git a/apps/core/src/commands/affine-navigation.tsx b/apps/core/src/commands/affine-navigation.tsx index bf907857b..fe12f3112 100644 --- a/apps/core/src/commands/affine-navigation.tsx +++ b/apps/core/src/commands/affine-navigation.tsx @@ -17,14 +17,14 @@ export function registerAffineNavigationCommands({ store, workspace, navigationHelper, - pageMode, - setPageMode, + pageListMode, + setPageListMode, }: { t: ReturnType; store: ReturnType; navigationHelper: ReturnType; - pageMode: PageModeOption; - setPageMode: React.Dispatch>; + pageListMode: PageModeOption; + setPageListMode: React.Dispatch>; workspace: Workspace; }) { const unsubs: Array<() => void> = []; @@ -36,7 +36,7 @@ export function registerAffineNavigationCommands({ label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](), run() { navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); - setPageMode('all'); + setPageListMode('all'); }, }) ); @@ -47,12 +47,12 @@ export function registerAffineNavigationCommands({ category: 'affine:navigation', icon: , preconditionStrategy: () => { - return pageMode !== 'page'; + return pageListMode !== 'page'; }, label: () => t['com.affine.cmdk.affine.navigation.goto-page-list'](), run() { navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); - setPageMode('page'); + setPageListMode('page'); }, }) ); @@ -63,12 +63,12 @@ export function registerAffineNavigationCommands({ category: 'affine:navigation', icon: , preconditionStrategy: () => { - return pageMode !== 'edgeless'; + return pageListMode !== 'edgeless'; }, label: () => t['com.affine.cmdk.affine.navigation.goto-edgeless-list'](), run() { navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); - setPageMode('edgeless'); + setPageListMode('edgeless'); }, }) ); @@ -109,7 +109,7 @@ export function registerAffineNavigationCommands({ label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](), run() { navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH); - setPageMode('all'); + setPageListMode('all'); }, }) ); diff --git a/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts b/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts index 92b2dcc96..f867a5b83 100644 --- a/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts +++ b/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts @@ -112,7 +112,7 @@ globalStyle(`${accountButton} .avatar`, { width: '28px', height: '28px', borderRadius: '50%', - fontSize: '22px', + fontSize: '20px', display: 'flex', justifyContent: 'center', alignItems: 'center', @@ -120,9 +120,10 @@ globalStyle(`${accountButton} .avatar`, { }); globalStyle(`${accountButton} .avatar.not-sign`, { - borderColor: 'var(--affine-icon-secondary)', color: 'var(--affine-icon-secondary)', background: 'var(--affine-white)', + paddingBottom: '2px', + border: '1px solid var(--affine-icon-secondary)', }); globalStyle(`${accountButton} .content`, { flexGrow: '1', diff --git a/apps/core/src/components/affine/share-page-modal/index.tsx b/apps/core/src/components/affine/share-page-modal/index.tsx index 55517842c..2f90249a1 100644 --- a/apps/core/src/components/affine/share-page-modal/index.tsx +++ b/apps/core/src/components/affine/share-page-modal/index.tsx @@ -6,6 +6,7 @@ import { import type { Page } from '@blocksuite/store'; import { useState } from 'react'; +import { useExportPage } from '../../../hooks/affine/use-export-page'; import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page'; import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace'; import { EnableAffineCloudModal } from '../enable-affine-cloud-modal'; @@ -18,6 +19,7 @@ type SharePageModalProps = { export const SharePageModal = ({ workspace, page }: SharePageModalProps) => { const onTransformWorkspace = useOnTransformWorkspace(); const [open, setOpen] = useState(false); + const exportHandler = useExportPage(page); return ( <> { useIsSharedPage={useIsSharedPage} onEnableAffineCloud={() => setOpen(true)} togglePagePublic={async () => {}} + exportHandler={exportHandler} /> {workspace.flavour === WorkspaceFlavour.LOCAL ? ( { const t = useAFFiNEI18N(); const ref = useRef(null); + const { openPage } = useNavigateHelper(); + // fixme(himself65): remove these hooks ASAP const [workspace] = useCurrentWorkspace(); - const blockSuiteWorkspace = workspace.blockSuiteWorkspace; + const currentPage = blockSuiteWorkspace.getPage(pageId); + assertExists(currentPage); + const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( meta => meta.id === pageId ) as PageMeta; - const [setting, setSetting] = useAtom(pageSettingFamily(pageId)); - const mode = setting?.mode ?? 'page'; - + const currentMode = useAtomValue(currentModeAtom); const favorite = pageMeta.favorite ?? false; + const { setPageMeta, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace); - const [openConfirm, setOpenConfirm] = useState(false); - const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace); + const { togglePageMode, toggleFavorite } = + useBlockSuiteMetaHelper(blockSuiteWorkspace); const { importFile } = usePageHelper(blockSuiteWorkspace); + const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); + const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); + + const handleOpenTrashModal = useCallback(() => { + setTrashModal({ + open: true, + pageId, + pageTitle: pageMeta.title, + }); + }, [pageId, pageMeta.title, setTrashModal]); + const handleFavorite = useCallback(() => { - setPageMeta(pageId, { favorite: !favorite }); + toggleFavorite(pageId); toast( favorite ? t['com.affine.toastMessage.removedFavorites']() : t['com.affine.toastMessage.addedFavorites']() ); - }, [favorite, pageId, setPageMeta, t]); + }, [favorite, pageId, t, toggleFavorite]); const handleSwitchMode = useCallback(() => { - setSetting(setting => ({ - mode: setting?.mode === 'page' ? 'edgeless' : 'page', - })); + togglePageMode(pageId); toast( - mode === 'page' + currentMode === 'page' ? t['com.affine.toastMessage.edgelessMode']() : t['com.affine.toastMessage.pageMode']() ); - }, [mode, setSetting, t]); - const handleOnConfirm = useCallback(() => { - removeToTrash(pageId); - toast(t['com.affine.toastMessage.movedTrash']()); - setOpenConfirm(false); - }, [pageId, removeToTrash, t]); + }, [currentMode, pageId, t, togglePageMode]); const menuItemStyle = { padding: '4px 12px', transition: 'all 0.3s', }; - const { openPage } = useNavigateHelper(); - const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); + + const exportHandler = useExportPage(currentPage); const setPageMode = useSetAtom(setPageModeAtom); + const duplicate = useCallback(async () => { - const currentPage = blockSuiteWorkspace.getPage(pageId); - assertExists(currentPage); const currentPageMeta = currentPage.meta; const newPage = createPage(); await newPage.waitForLoaded(); + const update = encodeStateAsUpdate(currentPage.spaceDoc); applyUpdate(newPage.spaceDoc, update); + setPageMeta(newPage.id, { tags: currentPageMeta.tags, favorite: currentPageMeta.favorite, }); - setPageMode(newPage.id, mode); + setPageMode(newPage.id, currentMode); setPageTitle(newPage.id, `${currentPageMeta.title}(1)`); openPage(blockSuiteWorkspace.id, newPage.id); }, [ - blockSuiteWorkspace, + blockSuiteWorkspace.id, createPage, - mode, + currentMode, + currentPage.meta, + currentPage.spaceDoc, openPage, - pageId, setPageMeta, setPageMode, setPageTitle, @@ -130,7 +142,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => { - {mode === 'page' ? : } + {currentMode === 'page' ? : } } data-testid="editor-option-menu-edgeless" @@ -138,7 +150,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => { style={menuItemStyle} > {t['Convert to ']()} - {mode === 'page' + {currentMode === 'page' ? t['com.affine.pageMode.edgeless']() : t['com.affine.pageMode.page']()} @@ -194,13 +206,11 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => { > {t['Import']()} - + { - setOpenConfirm(true); - }} + onSelect={handleOpenTrashModal} /> ); @@ -218,12 +228,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => { > - ); diff --git a/apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx b/apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx index 7e9f25bbb..77a723387 100644 --- a/apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx +++ b/apps/core/src/components/blocksuite/block-suite-mode-switch/index.tsx @@ -2,11 +2,12 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; import { Tooltip } from '@toeverything/components/tooltip'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; -import { useAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; import type { CSSProperties } from 'react'; -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; -import { pageSettingFamily } from '../../../atoms'; +import { currentModeAtom } from '../../../atoms/mode'; +import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; import type { BlockSuiteWorkspace } from '../../../shared'; import { toast } from '../../../utils'; import { StyledEditorModeSwitch, StyledKeyboardItem } from './style'; @@ -34,14 +35,17 @@ export const EditorModeSwitch = ({ blockSuiteWorkspace, pageId, }: EditorModeSwitchProps) => { - const [setting, setSetting] = useAtom(pageSettingFamily(pageId)); - const currentMode = setting?.mode ?? 'page'; + const t = useAFFiNEI18N(); const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find( meta => meta.id === pageId ); - const t = useAFFiNEI18N(); assertExists(pageMeta); const { trash } = pageMeta; + + const { togglePageMode, switchToEdgelessMode, switchToPageMode } = + useBlockSuiteMetaHelper(blockSuiteWorkspace); + const currentMode = useAtomValue(currentModeAtom); + useEffect(() => { if (trash) { return; @@ -53,21 +57,33 @@ export const EditorModeSwitch = ({ : e.key === 's' && e.altKey ) { e.preventDefault(); - setSetting(setting => { - if (setting?.mode !== 'page') { - toast(t['com.affine.toastMessage.pageMode']()); - return { ...setting, mode: 'page' }; - } else { - toast(t['com.affine.toastMessage.edgelessMode']()); - return { ...setting, mode: 'edgeless' }; - } - }); + togglePageMode(pageId); + toast( + currentMode === 'page' + ? t['com.affine.toastMessage.edgelessMode']() + : t['com.affine.toastMessage.pageMode']() + ); } }; document.addEventListener('keydown', keydown, { capture: true }); return () => document.removeEventListener('keydown', keydown, { capture: true }); - }, [setSetting, t, trash]); + }, [currentMode, pageId, t, togglePageMode, trash]); + + const onSwitchToPageMode = useCallback(() => { + if (currentMode === 'page') { + return; + } + switchToPageMode(pageId); + toast(t['com.affine.toastMessage.pageMode']()); + }, [currentMode, pageId, switchToPageMode, t]); + const onSwitchToEdgelessMode = useCallback(() => { + if (currentMode === 'edgeless') { + return; + } + switchToEdgelessMode(pageId); + toast(t['com.affine.toastMessage.edgelessMode']()); + }, [currentMode, pageId, switchToEdgelessMode, t]); return ( }> @@ -81,28 +97,14 @@ export const EditorModeSwitch = ({ active={currentMode === 'page'} hide={trash && currentMode !== 'page'} trash={trash} - onClick={() => { - setSetting(setting => { - if (setting?.mode !== 'page') { - toast(t['com.affine.toastMessage.pageMode']()); - } - return { ...setting, mode: 'page' }; - }); - }} + onClick={onSwitchToPageMode} /> { - setSetting(setting => { - if (setting?.mode !== 'edgeless') { - toast(t['com.affine.toastMessage.edgelessMode']()); - } - return { ...setting, mode: 'edgeless' }; - }); - }} + onClick={onSwitchToEdgelessMode} /> diff --git a/apps/core/src/components/blocksuite/block-suite-page-list/index.tsx b/apps/core/src/components/blocksuite/block-suite-page-list/index.tsx index b905d1965..a37504d63 100644 --- a/apps/core/src/components/blocksuite/block-suite-page-list/index.tsx +++ b/apps/core/src/components/blocksuite/block-suite-page-list/index.tsx @@ -15,6 +15,7 @@ import { Suspense, useCallback, useMemo } from 'react'; import { allPageModeSelectAtom } from '../../../atoms'; import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper'; +import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper'; import { useGetPageInfoById } from '../../../hooks/use-get-page-info'; import type { BlockSuiteWorkspace } from '../../../shared'; import { toast } from '../../../utils'; @@ -134,7 +135,6 @@ export const BlockSuitePageList = ({ const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace); const { toggleFavorite, - removeToTrash, restoreFromTrash, permanentlyDeletePage, cancelPublicPage, @@ -144,6 +144,8 @@ export const BlockSuitePageList = ({ usePageHelper(blockSuiteWorkspace); const t = useAFFiNEI18N(); const getPageInfo = useGetPageInfoById(blockSuiteWorkspace); + const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); + const tagOptionMap = useMemo( () => Object.fromEntries( @@ -246,10 +248,13 @@ export const BlockSuitePageList = ({ onClickRestore: () => { restoreFromTrash(pageMeta.id); }, - removeToTrash: () => { - removeToTrash(pageMeta.id); - toast(t['com.affine.toastMessage.successfullyDeleted']()); - }, + removeToTrash: () => + setTrashModal({ + open: true, + pageId: pageMeta.id, + pageTitle: pageMeta.title, + }), + onRestorePage: () => { restoreFromTrash(pageMeta.id); toast( diff --git a/apps/core/src/components/pure/cmdk/main.css.ts b/apps/core/src/components/pure/cmdk/main.css.ts index a1e730d85..025fce2ac 100644 --- a/apps/core/src/components/pure/cmdk/main.css.ts +++ b/apps/core/src/components/pure/cmdk/main.css.ts @@ -133,7 +133,23 @@ globalStyle(`${root} [cmdk-item]`, { globalStyle(`${root} [cmdk-item][data-selected=true]`, { background: 'var(--affine-background-secondary-color)', }); +globalStyle(`${root} [cmdk-item][data-selected=true][data-is-danger=true]`, { + background: 'var(--affine-background-error-color)', + color: 'var(--affine-error-color)', +}); globalStyle(`${root} [cmdk-item][data-selected=true] ${itemIcon}`, { color: 'var(--affine-icon-color)', }); +globalStyle( + `${root} [cmdk-item][data-selected=true][data-is-danger=true] ${itemIcon}`, + { + color: 'var(--affine-error-color)', + } +); +globalStyle( + `${root} [cmdk-item][data-selected=true][data-is-danger=true] ${itemLabel}`, + { + color: 'var(--affine-error-color)', + } +); diff --git a/apps/core/src/components/pure/cmdk/main.tsx b/apps/core/src/components/pure/cmdk/main.tsx index 7f869bc1f..2bd1ec3bc 100644 --- a/apps/core/src/components/pure/cmdk/main.tsx +++ b/apps/core/src/components/pure/cmdk/main.tsx @@ -64,6 +64,10 @@ const QuickSearchGroup = ({ onOpenChange?.(false); }} value={command.value} + data-is-danger={ + command.id === 'editor:page-move-to-trash' || + command.id === 'editor:edgeless-move-to-trash' + } >
{command.icon}
void; }) => { - const { removeToTrash } = useBlockSuiteMetaHelper(workspace); const t = useAFFiNEI18N(); + const { setTrashModal } = useTrashModalHelper(workspace); + const onClickDelete = useCallback(() => { + setTrashModal({ + open: true, + pageId: page.id, + pageTitle: page.title, + }); + }, [page.id, page.title, setTrashModal]); const actions = useMemo< Array< | { @@ -97,9 +104,7 @@ export const PageOperations = ({ ), name: t['com.affine.trashOperation.delete'](), - click: () => { - removeToTrash(page.id); - }, + click: onClickDelete, type: 'danger', }, ], @@ -107,10 +112,10 @@ export const PageOperations = ({ inAllowList, t, inExcludeList, + onClickDelete, removeFromAllowList, page.id, addToExcludeList, - removeToTrash, ] ); return ( diff --git a/apps/core/src/components/root-app-sidebar/index.tsx b/apps/core/src/components/root-app-sidebar/index.tsx index b1f7f7067..c6c496211 100644 --- a/apps/core/src/components/root-app-sidebar/index.tsx +++ b/apps/core/src/components/root-app-sidebar/index.tsx @@ -10,7 +10,7 @@ import { SidebarContainer, SidebarScrollableContainer, } from '@affine/component/app-sidebar'; -import { useCollectionManager } from '@affine/component/page-list'; +import { MoveToTrash, useCollectionManager } from '@affine/component/page-list'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { DeleteTemporarilyIcon, @@ -27,6 +27,7 @@ import { forwardRef, useCallback, useEffect, useMemo } from 'react'; import { openWorkspaceListModalAtom } from '../../atoms'; import { useHistoryAtom } from '../../atoms/history'; import { useAppSetting } from '../../atoms/settings'; +import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper'; import type { AllWorkspace } from '../../shared'; import { currentCollectionsAtom } from '../../utils/user-setting'; import { CollectionsList } from '../pure/workspace-slider-bar/collections'; @@ -110,6 +111,20 @@ export const RootAppSidebar = ({ openPage(page.id); }, [createPage, openPage]); + const { trashModal, setTrashModal, handleOnConfirm } = + useTrashModalHelper(blockSuiteWorkspace); + const deletePageTitle = trashModal.pageTitle; + const trashConfirmOpen = trashModal.open; + const onTrashConfirmOpenChange = useCallback( + (open: boolean) => { + setTrashModal({ + ...trashModal, + open, + }); + }, + [trashModal, setTrashModal] + ); + // Listen to the "New Page" action from the menu useEffect(() => { if (environment.isDesktop) { @@ -159,6 +174,12 @@ export const RootAppSidebar = ({ ) } > + { + setPageMode(pageId, 'page'); + }, + [setPageMode] + ); + const switchToEdgelessMode = useCallback( + (pageId: string) => { + setPageMode(pageId, 'edgeless'); + }, + [setPageMode] + ); + const togglePageMode = useCallback( + (pageId: string) => { + setPageMode(pageId, currentMode === 'edgeless' ? 'page' : 'edgeless'); + }, + [currentMode, setPageMode] + ); const addToFavorite = useCallback( (pageId: string) => { @@ -115,6 +139,10 @@ export function useBlockSuiteMetaHelper( ); return { + switchToPageMode, + switchToEdgelessMode, + togglePageMode, + publicPage, cancelPublicPage, diff --git a/apps/core/src/hooks/affine/use-export-page.ts b/apps/core/src/hooks/affine/use-export-page.ts new file mode 100644 index 000000000..0cab4d4b3 --- /dev/null +++ b/apps/core/src/hooks/affine/use-export-page.ts @@ -0,0 +1,79 @@ +import { pushNotificationAtom } from '@affine/component/notification-center'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { PageBlockModel } from '@blocksuite/blocks'; +import { ContentParser } from '@blocksuite/blocks/content-parser'; +import type { Page } from '@blocksuite/store'; +import { useSetAtom } from 'jotai'; +import { useCallback } from 'react'; + +type ExportType = 'pdf' | 'html' | 'png' | 'markdown'; +const typeToContentParserMethodMap = { + pdf: 'exportPdf', + html: 'exportHtml', + png: 'exportPng', + markdown: 'exportMarkdown', +} satisfies Record; + +const contentParserWeakMap = new WeakMap(); + +const getContentParser = (page: Page) => { + if (!contentParserWeakMap.has(page)) { + contentParserWeakMap.set( + page, + new ContentParser(page, { + imageProxyEndpoint: !environment.isDesktop + ? runtimeConfig.imageProxyUrl + : undefined, + }) + ); + } + return contentParserWeakMap.get(page) as ContentParser; +}; + +interface ExportHandlerOptions { + page: Page; + type: ExportType; +} + +async function exportHandler({ page, type }: ExportHandlerOptions) { + if (type === 'pdf' && environment.isDesktop && page.meta.mode === 'page') { + window.apis?.export.savePDFFileAs( + (page.root as PageBlockModel).title.toString() + ); + } else { + const contentParser = getContentParser(page); + const method = typeToContentParserMethodMap[type]; + await contentParser[method](); + } +} + +export const useExportPage = (page: Page) => { + const pushNotification = useSetAtom(pushNotificationAtom); + const t = useAFFiNEI18N(); + + const onClickHandler = useCallback( + async (type: ExportType) => { + try { + await exportHandler({ + page, + type, + }); + pushNotification({ + title: t['com.affine.export.success.title'](), + message: t['com.affine.export.success.message'](), + type: 'success', + }); + } catch (err) { + console.error(err); + pushNotification({ + title: t['com.affine.export.error.title'](), + message: t['com.affine.export.error.message'](), + type: 'error', + }); + } + }, + [page, pushNotification, t] + ); + + return onClickHandler; +}; diff --git a/apps/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx b/apps/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx new file mode 100644 index 000000000..dc7fdc232 --- /dev/null +++ b/apps/core/src/hooks/affine/use-register-blocksuite-editor-commands.tsx @@ -0,0 +1,208 @@ +import { toast } from '@affine/component'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { assertExists } from '@blocksuite/global/utils'; +import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; +import type { Workspace } from '@blocksuite/store'; +import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta'; +import { + PreconditionStrategy, + registerAffineCommand, +} from '@toeverything/infra/command'; +import { useCallback, useEffect } from 'react'; + +import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; +import { useExportPage } from './use-export-page'; +import { useTrashModalHelper } from './use-trash-modal-helper'; + +export function useRegisterBlocksuiteEditorCommands( + blockSuiteWorkspace: Workspace, + pageId: string, + mode: 'page' | 'edgeless' +) { + const t = useAFFiNEI18N(); + const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace); + const currentPage = blockSuiteWorkspace.getPage(pageId); + assertExists(currentPage); + const pageMeta = getPageMeta(pageId); + assertExists(pageMeta); + const favorite = pageMeta.favorite ?? false; + const trash = pageMeta.trash ?? false; + + const { togglePageMode, toggleFavorite, restoreFromTrash } = + useBlockSuiteMetaHelper(blockSuiteWorkspace); + const exportHandler = useExportPage(currentPage); + const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace); + const onClickDelete = useCallback(() => { + setTrashModal({ + open: true, + pageId: pageId, + pageTitle: pageMeta.title, + }); + }, [pageId, pageMeta.title, setTrashModal]); + + useEffect(() => { + const unsubs: Array<() => void> = []; + const preconditionStrategy = () => + PreconditionStrategy.InPaperOrEdgeless && !trash; + + //TODO: add back when edgeless presentation is ready + + // this is pretty hack and easy to break. need a better way to communicate with blocksuite editor + // unsubs.push( + // registerAffineCommand({ + // id: 'editor:edgeless-presentation-start', + // preconditionStrategy: () => PreconditionStrategy.InEdgeless && !trash, + // category: 'editor:edgeless', + // icon: , + // label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](), + // run() { + // document + // .querySelector('edgeless-toolbar') + // ?.shadowRoot?.querySelector( + // '.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child' + // ) + // ?.click(); + // }, + // }) + // ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: favorite + ? t['com.affine.favoritePageOperation.remove']() + : t['com.affine.favoritePageOperation.add'](), + run() { + toggleFavorite(pageId); + toast( + favorite + ? t['com.affine.cmdk.affine.editor.remove-from-favourites']() + : t['com.affine.cmdk.affine.editor.add-to-favourites']() + ); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-convert-to-${ + mode === 'page' ? 'edgeless' : 'page' + }`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: `${t['Convert to ']()}${ + mode === 'page' + ? t['com.affine.pageMode.edgeless']() + : t['com.affine.pageMode.page']() + }`, + run() { + togglePageMode(pageId); + toast( + mode === 'page' + ? t['com.affine.toastMessage.edgelessMode']() + : t['com.affine.toastMessage.pageMode']() + ); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-export-to-pdf`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['Export to PDF'](), + run() { + exportHandler('pdf'); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-export-to-html`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['Export to HTML'](), + run() { + exportHandler('html'); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-export-to-png`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['Export to PNG'](), + run() { + exportHandler('png'); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-export-to-markdown`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['Export to Markdown'](), + run() { + exportHandler('markdown'); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-move-to-trash`, + preconditionStrategy, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['com.affine.moveToTrash.title'](), + run() { + onClickDelete(); + }, + }) + ); + + unsubs.push( + registerAffineCommand({ + id: `editor:${mode}-restore-from-trash`, + preconditionStrategy: () => + PreconditionStrategy.InPaperOrEdgeless && trash, + category: `editor:${mode}`, + icon: mode === 'page' ? : , + label: t['com.affine.cmdk.affine.editor.restore-from-trash'](), + run() { + restoreFromTrash(pageId); + }, + }) + ); + + return () => { + unsubs.forEach(unsub => unsub()); + }; + }, [ + favorite, + mode, + onClickDelete, + exportHandler, + pageId, + pageMeta.title, + restoreFromTrash, + t, + toggleFavorite, + togglePageMode, + trash, + ]); +} diff --git a/apps/core/src/hooks/affine/use-trash-modal-helper.ts b/apps/core/src/hooks/affine/use-trash-modal-helper.ts new file mode 100644 index 000000000..27c4f7922 --- /dev/null +++ b/apps/core/src/hooks/affine/use-trash-modal-helper.ts @@ -0,0 +1,27 @@ +import { toast } from '@affine/component'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import type { Workspace } from '@blocksuite/store'; +import { useAtom } from 'jotai'; +import { useCallback } from 'react'; + +import { trashModalAtom } from '../../atoms/trash-modal'; +import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; + +export function useTrashModalHelper(blocksuiteWorkspace: Workspace) { + const t = useAFFiNEI18N(); + const [trashModal, setTrashModal] = useAtom(trashModalAtom); + const { pageId } = trashModal; + const { removeToTrash } = useBlockSuiteMetaHelper(blocksuiteWorkspace); + + const handleOnConfirm = useCallback(() => { + removeToTrash(pageId); + toast(t['com.affine.toastMessage.movedTrash']()); + setTrashModal({ ...trashModal, open: false }); + }, [pageId, removeToTrash, setTrashModal, t, trashModal]); + + return { + trashModal, + setTrashModal, + handleOnConfirm, + }; +} diff --git a/apps/core/src/hooks/use-register-workspace-commands.ts b/apps/core/src/hooks/use-register-workspace-commands.ts index ff1e3ef5d..ac78467ea 100644 --- a/apps/core/src/hooks/use-register-workspace-commands.ts +++ b/apps/core/src/hooks/use-register-workspace-commands.ts @@ -21,7 +21,7 @@ export function useRegisterWorkspaceCommands() { const [currentWorkspace] = useCurrentWorkspace(); const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace); const navigationHelper = useNavigateHelper(); - const [pageMode, setPageMode] = useAtom(allPageModeSelectAtom); + const [pageListMode, setPageListMode] = useAtom(allPageModeSelectAtom); useEffect(() => { const unsubs: Array<() => void> = []; unsubs.push( @@ -30,8 +30,8 @@ export function useRegisterWorkspaceCommands() { t, workspace: currentWorkspace.blockSuiteWorkspace, navigationHelper, - pageMode, - setPageMode, + pageListMode, + setPageListMode, }) ); unsubs.push(registerAffineSettingsCommands({ store, t, theme })); @@ -54,7 +54,7 @@ export function useRegisterWorkspaceCommands() { theme, currentWorkspace.blockSuiteWorkspace, navigationHelper, - pageMode, - setPageMode, + pageListMode, + setPageListMode, ]); } diff --git a/apps/core/src/pages/workspace/detail-page.tsx b/apps/core/src/pages/workspace/detail-page.tsx index 33ae305b7..6039b16f8 100644 --- a/apps/core/src/pages/workspace/detail-page.tsx +++ b/apps/core/src/pages/workspace/detail-page.tsx @@ -24,6 +24,7 @@ import type { Map as YMap } from 'yjs'; import { getUIAdapter } from '../../adapters/workspace'; import { setPageModeAtom } from '../../atoms'; import { currentModeAtom } from '../../atoms/mode'; +import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands'; import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { currentCollectionsAtom } from '../../utils/user-setting'; @@ -38,7 +39,7 @@ const DetailPageImpl = (): ReactElement => { const collectionManager = useCollectionManager(currentCollectionsAtom); const mode = useAtomValue(currentModeAtom); const setPageMode = useSetAtom(setPageModeAtom); - + useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode); const onLoad = useCallback( (page: Page, editor: EditorContainer) => { try { diff --git a/apps/storybook/src/stories/page-list.stories.tsx b/apps/storybook/src/stories/page-list.stories.tsx index 008238945..f8e9e73c4 100644 --- a/apps/storybook/src/stories/page-list.stories.tsx +++ b/apps/storybook/src/stories/page-list.stories.tsx @@ -24,7 +24,6 @@ export const AffineOperationCell: StoryFn = ({ }) => ; AffineOperationCell.args = { - title: 'Example Page', favorite: false, isPublic: true, onToggleFavoritePage: () => toast('Toggle favorite page'), diff --git a/apps/storybook/src/stories/share-menu.stories.tsx b/apps/storybook/src/stories/share-menu.stories.tsx index b1b858bdc..b711e0c2e 100644 --- a/apps/storybook/src/stories/share-menu.stories.tsx +++ b/apps/storybook/src/stories/share-menu.stories.tsx @@ -92,6 +92,7 @@ export const Basic: StoryFn = () => { workspace={localWorkspace} onEnableAffineCloud={unimplemented} togglePagePublic={unimplemented} + exportHandler={unimplemented} /> ); }; @@ -122,6 +123,7 @@ export const AffineBasic: StoryFn = () => { workspace={affineWorkspace} onEnableAffineCloud={unimplemented} togglePagePublic={unimplemented} + exportHandler={unimplemented} /> ); }; diff --git a/packages/component/src/components/block-suite-editor/index.tsx b/packages/component/src/components/block-suite-editor/index.tsx index a76cf8b8f..e5e5c46cc 100644 --- a/packages/component/src/components/block-suite-editor/index.tsx +++ b/packages/component/src/components/block-suite-editor/index.tsx @@ -13,7 +13,6 @@ import { blockSuiteEditorHeaderStyle, blockSuiteEditorStyle, } from './index.css'; -import { useRegisterBlocksuiteEditorCommands } from './use-register-blocksuite-editor-commands'; export type EditorProps = { page: Page; @@ -165,7 +164,6 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() { export const BlockSuiteEditor = memo(function BlockSuiteEditor( props: EditorProps & ErrorBoundaryProps ): ReactElement { - useRegisterBlocksuiteEditorCommands(); return ( { - const unsubs: Array<() => void> = []; - const getEdgeless = () => { - return document.querySelector('affine-edgeless-page'); - }; - unsubs.push( - registerAffineCommand({ - id: 'editor:edgeless-presentation-start', - preconditionStrategy: () => !!getEdgeless(), - category: 'editor:edgeless', - icon: , - label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](), - run() { - // this is pretty hack and easy to break. need a better way to communicate with blocksuite editor - document - .querySelector('edgeless-toolbar') - ?.shadowRoot?.querySelector( - '.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child' - ) - ?.click(); - }, - }) - ); - return () => { - unsubs.forEach(unsub => unsub()); - }; - }, [t]); -} diff --git a/packages/component/src/components/page-list/all-pages-body.tsx b/packages/component/src/components/page-list/all-pages-body.tsx index c2fea7f96..4bad8fe88 100644 --- a/packages/component/src/components/page-list/all-pages-body.tsx +++ b/packages/component/src/components/page-list/all-pages-body.tsx @@ -131,7 +131,6 @@ export const AllPagesBody = ({ active={!!favorite} /> void; @@ -27,7 +26,6 @@ export interface OperationCellProps { } export const OperationCell = ({ - title, favorite, isPublic, onOpenPageInNewTab, @@ -36,7 +34,6 @@ export const OperationCell = ({ onDisablePublicSharing, }: OperationCellProps) => { const t = useAFFiNEI18N(); - const [open, setOpen] = useState(false); const [openDisableShared, setOpenDisableShared] = useState(false); const OperationMenu = ( @@ -77,12 +74,7 @@ export const OperationCell = ({ {t['com.affine.openPageOperation.newTab']()} )} - { - setOpen(true); - }} - /> + ); return ( @@ -99,15 +91,6 @@ export const OperationCell = ({ - { - onRemoveToTrash(); - setOpen(false); - }} - onOpenChange={setOpen} - /> { + onSelect: () => void; + className?: string; + type: T; + icon: ReactNode; + label: string; +} + +interface ExportProps { + exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise; + className?: string; +} + +export function ExportMenuItem({ onSelect, className, -}: CommonMenuItemProps<{ type: 'pdf' }>) => { - const t = useAFFiNEI18N(); - const { currentEditor } = globalThis; - const setPushNotification = useSetAtom(pushNotificationAtom); - - const onClickDownloadPDF = useCallback(() => { - if (!currentEditor) { - return; - } - - if (environment.isDesktop && currentEditor.mode === 'page') { - window.apis?.export - .savePDFFileAs( - (currentEditor.page.root as PageBlockModel).title.toString() - ) - .then(() => { - onSelect?.({ type: 'pdf' }); - setPushNotification({ - title: t['com.affine.export.success.title'](), - message: t['com.affine.export.success.message'](), - type: 'success', - }); - }) - .catch(err => { - console.error(err); - setPushNotification({ - title: t['com.affine.export.error.title'](), - message: t['com.affine.export.error.message'](), - type: 'error', - }); - }); - } else { - const contentParser = getContentParser(currentEditor.page); - - contentParser - .exportPdf() - .then(() => { - onSelect?.({ type: 'pdf' }); - setPushNotification({ - title: t['com.affine.export.success.title'](), - message: t['com.affine.export.success.message'](), - type: 'success', - }); - }) - .catch(err => { - console.error(err); - setPushNotification({ - title: t['com.affine.export.error.title'](), - message: t['com.affine.export.error.message'](), - type: 'error', - }); - }); - } - }, [currentEditor, onSelect, setPushNotification, t]); - + type, + icon, + label, +}: ExportMenuItemProps) { return ( - - - } + preFix={{icon}} > - {t['Export to PDF']()} + {label} ); -}; +} -export const ExportToHtmlMenuItem = ({ - onSelect, - className, -}: CommonMenuItemProps<{ type: 'html' }>) => { +export const ExportMenuItems = ({ + exportHandler, + className = transitionStyle, +}: ExportProps) => { const t = useAFFiNEI18N(); - const { currentEditor } = globalThis; - const setPushNotification = useSetAtom(pushNotificationAtom); - const onClickExportHtml = useCallback(() => { - if (!currentEditor) { - return; - } - const contentParser = getContentParser(currentEditor.page); - contentParser - .exportHtml() - .then(() => { - onSelect?.({ type: 'html' }); - setPushNotification({ - title: t['com.affine.export.success.title'](), - message: t['com.affine.export.success.message'](), - type: 'success', - }); - }) - .catch(err => { - console.error(err); - setPushNotification({ - title: t['com.affine.export.error.title'](), - message: t['com.affine.export.error.message'](), - type: 'error', - }); - }); - onSelect?.({ type: 'html' }); - }, [currentEditor, onSelect, setPushNotification, t]); - return ( - <> - - - - } - > - {t['Export to HTML']()} - - + const itemMap = useMemo( + () => [ + { + component: ExportMenuItem, + props: { + onSelect: () => exportHandler('pdf'), + className: className, + type: 'pdf', + icon: , + label: t['Export to PDF'](), + }, + }, + { + component: ExportMenuItem, + props: { + onSelect: () => exportHandler('html'), + className: className, + type: 'html', + icon: , + label: t['Export to HTML'](), + }, + }, + { + component: ExportMenuItem, + props: { + onSelect: () => exportHandler('png'), + className: className, + type: 'png', + icon: , + label: t['Export to PNG'](), + }, + }, + { + component: ExportMenuItem, + props: { + onSelect: () => exportHandler('markdown'), + className: className, + type: 'markdown', + icon: , + label: t['Export to Markdown'](), + }, + }, + ], + [className, exportHandler, t] ); + const items = itemMap.map(({ component: Component, props }) => ( + + )); + return <>{items}; }; -export const ExportToPngMenuItem = ({ - onSelect, - className, -}: CommonMenuItemProps<{ type: 'png' }>) => { +export const Export = ({ exportHandler, className }: ExportProps) => { const t = useAFFiNEI18N(); - const { currentEditor } = globalThis; - const setPushNotification = useSetAtom(pushNotificationAtom); - - const onClickDownloadPNG = useCallback(() => { - if (!currentEditor) { - return; - } - const contentParser = getContentParser(currentEditor.page); - - contentParser - .exportPng() - .then(() => { - onSelect?.({ type: 'png' }); - setPushNotification({ - title: t['com.affine.export.success.title'](), - message: t['com.affine.export.success.message'](), - type: 'success', - }); - }) - .catch(err => { - console.error(err); - setPushNotification({ - title: t['com.affine.export.error.title'](), - message: t['com.affine.export.error.message'](), - type: 'error', - }); - }); - }, [currentEditor, onSelect, setPushNotification, t]); - - return ( - <> - - - - } - > - {t['Export to PNG']()} - - + const items = ( + ); -}; - -export const ExportToMarkdownMenuItem = ({ - onSelect, - className, -}: CommonMenuItemProps<{ type: 'markdown' }>) => { - const t = useAFFiNEI18N(); - const { currentEditor } = globalThis; - const setPushNotification = useSetAtom(pushNotificationAtom); - const onClickExportMarkdown = useCallback(() => { - if (!currentEditor) { - return; - } - const contentParser = getContentParser(currentEditor.page); - contentParser - .exportMarkdown() - .then(() => { - onSelect?.({ type: 'markdown' }); - setPushNotification({ - title: t['com.affine.export.success.title'](), - message: t['com.affine.export.success.message'](), - type: 'success', - }); - }) - .catch(err => { - console.error(err); - setPushNotification({ - title: t['com.affine.export.error.title'](), - message: t['com.affine.export.error.message'](), - type: 'error', - }); - }); - onSelect?.({ type: 'markdown' }); - }, [currentEditor, onSelect, setPushNotification, t]); - return ( - <> - - - - } - > - {t['Export to Markdown']()} - - - ); -}; - -// fixme: refactor this file, export function may should be passed by 'props', this file is just a ui component -export const Export = () => { - const t = useAFFiNEI18N(); return ( - - - - - - } + items={items} triggerOptions={{ className: transitionStyle, preFix: ( diff --git a/packages/component/src/components/share-menu/share-export.tsx b/packages/component/src/components/share-menu/share-export.tsx index 4f86cc586..40f7b9939 100644 --- a/packages/component/src/components/share-menu/share-export.tsx +++ b/packages/component/src/components/share-menu/share-export.tsx @@ -3,20 +3,19 @@ import { LinkIcon } from '@blocksuite/icons'; import { Button } from '@toeverything/components/button'; import { Divider } from '@toeverything/components/divider'; -import { - ExportToHtmlMenuItem, - ExportToMarkdownMenuItem, - ExportToPdfMenuItem, - ExportToPngMenuItem, -} from '../page-list/operation-menu-items/export'; +import { ExportMenuItems } from '../page-list/operation-menu-items/export'; import * as styles from './index.css'; import type { ShareMenuProps } from './share-menu'; import { useSharingUrl } from './use-share-url'; -export const ShareExport = (props: ShareMenuProps) => { +export const ShareExport = ({ + workspace, + currentPage, + exportHandler, +}: ShareMenuProps) => { const t = useAFFiNEI18N(); - const workspaceId = props.workspace.id; - const pageId = props.currentPage.id; + const workspaceId = workspace.id; + const pageId = currentPage.id; const { onClickCopyLink } = useSharingUrl({ workspaceId, pageId, @@ -29,10 +28,10 @@ export const ShareExport = (props: ShareMenuProps) => { {t['com.affine.share-menu.ShareViaExport']()}
- - - - +
diff --git a/packages/component/src/components/share-menu/share-menu.tsx b/packages/component/src/components/share-menu/share-menu.tsx index 2e5f178b1..dc52039d8 100644 --- a/packages/component/src/components/share-menu/share-menu.tsx +++ b/packages/component/src/components/share-menu/share-menu.tsx @@ -27,6 +27,7 @@ export interface ShareMenuProps< ) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void]; onEnableAffineCloud: () => void; togglePagePublic: () => Promise; + exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise; } export const ShareMenu = (props: ShareMenuProps) => { diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index 91b4afcb5..1c0649ad4 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -615,5 +615,8 @@ "com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace", "com.affine.cmdk.affine.category.affine.edgeless": "Edgeless", "com.affine.cmdk.affine.category.affine.collections": "Collections", - "com.affine.cmdk.affine.import-workspace": "Import Workspace" + "com.affine.cmdk.affine.import-workspace": "Import Workspace", + "com.affine.cmdk.affine.editor.add-to-favourites": "Add to Favourites", + "com.affine.cmdk.affine.editor.remove-from-favourites": "Remove from Favourites", + "com.affine.cmdk.affine.editor.restore-from-trash": "Restore from Trash" } diff --git a/tests/affine-local/e2e/local-first-collections-items.spec.ts b/tests/affine-local/e2e/local-first-collections-items.spec.ts index 7b29d4a34..1cf834f4f 100644 --- a/tests/affine-local/e2e/local-first-collections-items.spec.ts +++ b/tests/affine-local/e2e/local-first-collections-items.spec.ts @@ -67,6 +67,7 @@ test('Show collections items in sidebar', async ({ page }) => { .getByTestId('collection-page-option') .getByText('Delete'); await deletePage.click(); + await page.getByTestId('confirm-delete-page').click(); expect(await collections.getByTestId('collection-page').count()).toBe(0); await first.hover(); await first.getByTestId('collection-options').click(); diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts index bfc0a7c25..f1adb90c8 100644 --- a/tests/affine-local/e2e/quick-search.spec.ts +++ b/tests/affine-local/e2e/quick-search.spec.ts @@ -27,6 +27,12 @@ const keyboardDownAndSelect = async (page: Page, label: string) => { } }; +const commandsIsVisible = async (page: Page, label: string) => { + const locators = page.locator('[cmdk-item] [data-testid="cmdk-label"]'); + const actual = (await locators.allInnerTexts()).find(text => text === label); + return !!actual; +}; + async function assertTitle(page: Page, text: string) { const edgeless = page.locator('affine-edgeless-page'); if (!edgeless) { @@ -280,3 +286,49 @@ test('assert the recent browse pages are on the recent list', async ({ ); } }); + +test('can use cmdk to export pdf', async ({ page }) => { + await openHomePage(page); + await waitForEditorLoad(page); + await clickNewPageButton(page); + await getBlockSuiteEditorTitle(page).click(); + await getBlockSuiteEditorTitle(page).fill('this is a new page to export'); + await openQuickSearchByShortcut(page); + const [download] = await Promise.all([ + page.waitForEvent('download'), + keyboardDownAndSelect(page, 'Export to PDF'), + ]); + expect(download.suggestedFilename()).toBe('this is a new page to export.pdf'); +}); +test('can use cmdk to export png', async ({ page }) => { + await openHomePage(page); + await waitForEditorLoad(page); + await clickNewPageButton(page); + await getBlockSuiteEditorTitle(page).click(); + await getBlockSuiteEditorTitle(page).fill('this is a new page to export'); + await openQuickSearchByShortcut(page); + const [download] = await Promise.all([ + page.waitForEvent('download'), + keyboardDownAndSelect(page, 'Export to PNG'), + ]); + expect(download.suggestedFilename()).toBe('this is a new page to export.png'); +}); + +test('can use cmdk to delete page and restore it', async ({ page }) => { + await openHomePage(page); + await waitForEditorLoad(page); + await clickNewPageButton(page); + await getBlockSuiteEditorTitle(page).click(); + await getBlockSuiteEditorTitle(page).fill('this is a new page to delete'); + await openQuickSearchByShortcut(page); + await keyboardDownAndSelect(page, 'Move to Trash'); + await page.getByTestId('confirm-delete-page').click(); + const restoreButton = page.getByTestId('page-restore-button'); + await expect(restoreButton).toBeVisible(); + await openQuickSearchByShortcut(page); + expect(await commandsIsVisible(page, 'Move to Trash')).toBe(false); + expect(await commandsIsVisible(page, 'Export to PDF')).toBe(false); + expect(await commandsIsVisible(page, 'Restore from Trash')).toBe(true); + await keyboardDownAndSelect(page, 'Restore from Trash'); + await expect(restoreButton).not.toBeVisible(); +});