feat(core): add editor commanads (#4514)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
13
apps/core/src/atoms/trash-modal.ts
Normal file
13
apps/core/src/atoms/trash-modal.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
|
export type TrashModal = {
|
||||||
|
open: boolean;
|
||||||
|
pageId: string;
|
||||||
|
pageTitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trashModalAtom = atom<TrashModal>({
|
||||||
|
open: false,
|
||||||
|
pageId: '',
|
||||||
|
pageTitle: '',
|
||||||
|
});
|
||||||
@@ -17,14 +17,14 @@ export function registerAffineNavigationCommands({
|
|||||||
store,
|
store,
|
||||||
workspace,
|
workspace,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
pageMode,
|
pageListMode,
|
||||||
setPageMode,
|
setPageListMode,
|
||||||
}: {
|
}: {
|
||||||
t: ReturnType<typeof useAFFiNEI18N>;
|
t: ReturnType<typeof useAFFiNEI18N>;
|
||||||
store: ReturnType<typeof createStore>;
|
store: ReturnType<typeof createStore>;
|
||||||
navigationHelper: ReturnType<typeof useNavigateHelper>;
|
navigationHelper: ReturnType<typeof useNavigateHelper>;
|
||||||
pageMode: PageModeOption;
|
pageListMode: PageModeOption;
|
||||||
setPageMode: React.Dispatch<React.SetStateAction<PageModeOption>>;
|
setPageListMode: React.Dispatch<React.SetStateAction<PageModeOption>>;
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}) {
|
}) {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
@@ -36,7 +36,7 @@ export function registerAffineNavigationCommands({
|
|||||||
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
|
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
|
||||||
run() {
|
run() {
|
||||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||||
setPageMode('all');
|
setPageListMode('all');
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -47,12 +47,12 @@ export function registerAffineNavigationCommands({
|
|||||||
category: 'affine:navigation',
|
category: 'affine:navigation',
|
||||||
icon: <ArrowRightBigIcon />,
|
icon: <ArrowRightBigIcon />,
|
||||||
preconditionStrategy: () => {
|
preconditionStrategy: () => {
|
||||||
return pageMode !== 'page';
|
return pageListMode !== 'page';
|
||||||
},
|
},
|
||||||
label: () => t['com.affine.cmdk.affine.navigation.goto-page-list'](),
|
label: () => t['com.affine.cmdk.affine.navigation.goto-page-list'](),
|
||||||
run() {
|
run() {
|
||||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||||
setPageMode('page');
|
setPageListMode('page');
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -63,12 +63,12 @@ export function registerAffineNavigationCommands({
|
|||||||
category: 'affine:navigation',
|
category: 'affine:navigation',
|
||||||
icon: <ArrowRightBigIcon />,
|
icon: <ArrowRightBigIcon />,
|
||||||
preconditionStrategy: () => {
|
preconditionStrategy: () => {
|
||||||
return pageMode !== 'edgeless';
|
return pageListMode !== 'edgeless';
|
||||||
},
|
},
|
||||||
label: () => t['com.affine.cmdk.affine.navigation.goto-edgeless-list'](),
|
label: () => t['com.affine.cmdk.affine.navigation.goto-edgeless-list'](),
|
||||||
run() {
|
run() {
|
||||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
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'](),
|
label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](),
|
||||||
run() {
|
run() {
|
||||||
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
|
||||||
setPageMode('all');
|
setPageListMode('all');
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ globalStyle(`${accountButton} .avatar`, {
|
|||||||
width: '28px',
|
width: '28px',
|
||||||
height: '28px',
|
height: '28px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
fontSize: '22px',
|
fontSize: '20px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -120,9 +120,10 @@ globalStyle(`${accountButton} .avatar`, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${accountButton} .avatar.not-sign`, {
|
globalStyle(`${accountButton} .avatar.not-sign`, {
|
||||||
borderColor: 'var(--affine-icon-secondary)',
|
|
||||||
color: 'var(--affine-icon-secondary)',
|
color: 'var(--affine-icon-secondary)',
|
||||||
background: 'var(--affine-white)',
|
background: 'var(--affine-white)',
|
||||||
|
paddingBottom: '2px',
|
||||||
|
border: '1px solid var(--affine-icon-secondary)',
|
||||||
});
|
});
|
||||||
globalStyle(`${accountButton} .content`, {
|
globalStyle(`${accountButton} .content`, {
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
||||||
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
|
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
|
||||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||||
@@ -18,6 +19,7 @@ type SharePageModalProps = {
|
|||||||
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
||||||
const onTransformWorkspace = useOnTransformWorkspace();
|
const onTransformWorkspace = useOnTransformWorkspace();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const exportHandler = useExportPage(page);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
@@ -26,6 +28,7 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
|||||||
useIsSharedPage={useIsSharedPage}
|
useIsSharedPage={useIsSharedPage}
|
||||||
onEnableAffineCloud={() => setOpen(true)}
|
onEnableAffineCloud={() => setOpen(true)}
|
||||||
togglePagePublic={async () => {}}
|
togglePagePublic={async () => {}}
|
||||||
|
exportHandler={exportHandler}
|
||||||
/>
|
/>
|
||||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||||
<EnableAffineCloudModal
|
<EnableAffineCloudModal
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ import {
|
|||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { pageSettingFamily, setPageModeAtom } from '../../../atoms';
|
import { setPageModeAtom } from '../../../atoms';
|
||||||
|
import { currentModeAtom } from '../../../atoms/mode';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
|
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
||||||
|
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
@@ -43,72 +46,81 @@ type PageMenuProps = {
|
|||||||
export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
// fixme(himself65): remove these hooks ASAP
|
// fixme(himself65): remove these hooks ASAP
|
||||||
const [workspace] = useCurrentWorkspace();
|
const [workspace] = useCurrentWorkspace();
|
||||||
|
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
|
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||||
|
assertExists(currentPage);
|
||||||
|
|
||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
meta => meta.id === pageId
|
meta => meta.id === pageId
|
||||||
) as PageMeta;
|
) as PageMeta;
|
||||||
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
|
const currentMode = useAtomValue(currentModeAtom);
|
||||||
const mode = setting?.mode ?? 'page';
|
|
||||||
|
|
||||||
const favorite = pageMeta.favorite ?? false;
|
const favorite = pageMeta.favorite ?? false;
|
||||||
|
|
||||||
const { setPageMeta, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
|
const { setPageMeta, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
|
||||||
const [openConfirm, setOpenConfirm] = useState(false);
|
const { togglePageMode, toggleFavorite } =
|
||||||
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||||
const { importFile } = usePageHelper(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(() => {
|
const handleFavorite = useCallback(() => {
|
||||||
setPageMeta(pageId, { favorite: !favorite });
|
toggleFavorite(pageId);
|
||||||
toast(
|
toast(
|
||||||
favorite
|
favorite
|
||||||
? t['com.affine.toastMessage.removedFavorites']()
|
? t['com.affine.toastMessage.removedFavorites']()
|
||||||
: t['com.affine.toastMessage.addedFavorites']()
|
: t['com.affine.toastMessage.addedFavorites']()
|
||||||
);
|
);
|
||||||
}, [favorite, pageId, setPageMeta, t]);
|
}, [favorite, pageId, t, toggleFavorite]);
|
||||||
const handleSwitchMode = useCallback(() => {
|
const handleSwitchMode = useCallback(() => {
|
||||||
setSetting(setting => ({
|
togglePageMode(pageId);
|
||||||
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
|
|
||||||
}));
|
|
||||||
toast(
|
toast(
|
||||||
mode === 'page'
|
currentMode === 'page'
|
||||||
? t['com.affine.toastMessage.edgelessMode']()
|
? t['com.affine.toastMessage.edgelessMode']()
|
||||||
: t['com.affine.toastMessage.pageMode']()
|
: t['com.affine.toastMessage.pageMode']()
|
||||||
);
|
);
|
||||||
}, [mode, setSetting, t]);
|
}, [currentMode, pageId, t, togglePageMode]);
|
||||||
const handleOnConfirm = useCallback(() => {
|
|
||||||
removeToTrash(pageId);
|
|
||||||
toast(t['com.affine.toastMessage.movedTrash']());
|
|
||||||
setOpenConfirm(false);
|
|
||||||
}, [pageId, removeToTrash, t]);
|
|
||||||
const menuItemStyle = {
|
const menuItemStyle = {
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
};
|
};
|
||||||
const { openPage } = useNavigateHelper();
|
|
||||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
const exportHandler = useExportPage(currentPage);
|
||||||
const setPageMode = useSetAtom(setPageModeAtom);
|
const setPageMode = useSetAtom(setPageModeAtom);
|
||||||
|
|
||||||
const duplicate = useCallback(async () => {
|
const duplicate = useCallback(async () => {
|
||||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
|
||||||
assertExists(currentPage);
|
|
||||||
const currentPageMeta = currentPage.meta;
|
const currentPageMeta = currentPage.meta;
|
||||||
const newPage = createPage();
|
const newPage = createPage();
|
||||||
await newPage.waitForLoaded();
|
await newPage.waitForLoaded();
|
||||||
|
|
||||||
const update = encodeStateAsUpdate(currentPage.spaceDoc);
|
const update = encodeStateAsUpdate(currentPage.spaceDoc);
|
||||||
applyUpdate(newPage.spaceDoc, update);
|
applyUpdate(newPage.spaceDoc, update);
|
||||||
|
|
||||||
setPageMeta(newPage.id, {
|
setPageMeta(newPage.id, {
|
||||||
tags: currentPageMeta.tags,
|
tags: currentPageMeta.tags,
|
||||||
favorite: currentPageMeta.favorite,
|
favorite: currentPageMeta.favorite,
|
||||||
});
|
});
|
||||||
setPageMode(newPage.id, mode);
|
setPageMode(newPage.id, currentMode);
|
||||||
setPageTitle(newPage.id, `${currentPageMeta.title}(1)`);
|
setPageTitle(newPage.id, `${currentPageMeta.title}(1)`);
|
||||||
openPage(blockSuiteWorkspace.id, newPage.id);
|
openPage(blockSuiteWorkspace.id, newPage.id);
|
||||||
}, [
|
}, [
|
||||||
blockSuiteWorkspace,
|
blockSuiteWorkspace.id,
|
||||||
createPage,
|
createPage,
|
||||||
mode,
|
currentMode,
|
||||||
|
currentPage.meta,
|
||||||
|
currentPage.spaceDoc,
|
||||||
openPage,
|
openPage,
|
||||||
pageId,
|
|
||||||
setPageMeta,
|
setPageMeta,
|
||||||
setPageMode,
|
setPageMode,
|
||||||
setPageTitle,
|
setPageTitle,
|
||||||
@@ -130,7 +142,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
preFix={
|
preFix={
|
||||||
<MenuIcon>
|
<MenuIcon>
|
||||||
{mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
{currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
}
|
}
|
||||||
data-testid="editor-option-menu-edgeless"
|
data-testid="editor-option-menu-edgeless"
|
||||||
@@ -138,7 +150,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
style={menuItemStyle}
|
style={menuItemStyle}
|
||||||
>
|
>
|
||||||
{t['Convert to ']()}
|
{t['Convert to ']()}
|
||||||
{mode === 'page'
|
{currentMode === 'page'
|
||||||
? t['com.affine.pageMode.edgeless']()
|
? t['com.affine.pageMode.edgeless']()
|
||||||
: t['com.affine.pageMode.page']()}
|
: t['com.affine.pageMode.page']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -194,13 +206,11 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
>
|
>
|
||||||
{t['Import']()}
|
{t['Import']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Export />
|
<Export exportHandler={exportHandler} />
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
<MoveToTrash
|
<MoveToTrash
|
||||||
data-testid="editor-option-menu-delete"
|
data-testid="editor-option-menu-delete"
|
||||||
onSelect={() => {
|
onSelect={handleOpenTrashModal}
|
||||||
setOpenConfirm(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -218,12 +228,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
>
|
>
|
||||||
<HeaderDropDownButton />
|
<HeaderDropDownButton />
|
||||||
</Menu>
|
</Menu>
|
||||||
<MoveToTrash.ConfirmModal
|
|
||||||
open={openConfirm}
|
|
||||||
title={pageMeta.title}
|
|
||||||
onConfirm={handleOnConfirm}
|
|
||||||
onOpenChange={setOpenConfirm}
|
|
||||||
/>
|
|
||||||
</FlexWrapper>
|
</FlexWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { Tooltip } from '@toeverything/components/tooltip';
|
import { Tooltip } from '@toeverything/components/tooltip';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { CSSProperties } from 'react';
|
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 type { BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
|
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
|
||||||
@@ -34,14 +35,17 @@ export const EditorModeSwitch = ({
|
|||||||
blockSuiteWorkspace,
|
blockSuiteWorkspace,
|
||||||
pageId,
|
pageId,
|
||||||
}: EditorModeSwitchProps) => {
|
}: EditorModeSwitchProps) => {
|
||||||
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
|
const t = useAFFiNEI18N();
|
||||||
const currentMode = setting?.mode ?? 'page';
|
|
||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
meta => meta.id === pageId
|
meta => meta.id === pageId
|
||||||
);
|
);
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
assertExists(pageMeta);
|
assertExists(pageMeta);
|
||||||
const { trash } = pageMeta;
|
const { trash } = pageMeta;
|
||||||
|
|
||||||
|
const { togglePageMode, switchToEdgelessMode, switchToPageMode } =
|
||||||
|
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||||
|
const currentMode = useAtomValue(currentModeAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trash) {
|
if (trash) {
|
||||||
return;
|
return;
|
||||||
@@ -53,21 +57,33 @@ export const EditorModeSwitch = ({
|
|||||||
: e.key === 's' && e.altKey
|
: e.key === 's' && e.altKey
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSetting(setting => {
|
togglePageMode(pageId);
|
||||||
if (setting?.mode !== 'page') {
|
toast(
|
||||||
toast(t['com.affine.toastMessage.pageMode']());
|
currentMode === 'page'
|
||||||
return { ...setting, mode: 'page' };
|
? t['com.affine.toastMessage.edgelessMode']()
|
||||||
} else {
|
: t['com.affine.toastMessage.pageMode']()
|
||||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
);
|
||||||
return { ...setting, mode: 'edgeless' };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', keydown, { capture: true });
|
document.addEventListener('keydown', keydown, { capture: true });
|
||||||
return () =>
|
return () =>
|
||||||
document.removeEventListener('keydown', keydown, { capture: true });
|
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 (
|
return (
|
||||||
<Tooltip content={<TooltipContent />}>
|
<Tooltip content={<TooltipContent />}>
|
||||||
@@ -81,28 +97,14 @@ export const EditorModeSwitch = ({
|
|||||||
active={currentMode === 'page'}
|
active={currentMode === 'page'}
|
||||||
hide={trash && currentMode !== 'page'}
|
hide={trash && currentMode !== 'page'}
|
||||||
trash={trash}
|
trash={trash}
|
||||||
onClick={() => {
|
onClick={onSwitchToPageMode}
|
||||||
setSetting(setting => {
|
|
||||||
if (setting?.mode !== 'page') {
|
|
||||||
toast(t['com.affine.toastMessage.pageMode']());
|
|
||||||
}
|
|
||||||
return { ...setting, mode: 'page' };
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<EdgelessSwitchItem
|
<EdgelessSwitchItem
|
||||||
data-testid="switch-edgeless-mode-button"
|
data-testid="switch-edgeless-mode-button"
|
||||||
active={currentMode === 'edgeless'}
|
active={currentMode === 'edgeless'}
|
||||||
hide={trash && currentMode !== 'edgeless'}
|
hide={trash && currentMode !== 'edgeless'}
|
||||||
trash={trash}
|
trash={trash}
|
||||||
onClick={() => {
|
onClick={onSwitchToEdgelessMode}
|
||||||
setSetting(setting => {
|
|
||||||
if (setting?.mode !== 'edgeless') {
|
|
||||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
|
||||||
}
|
|
||||||
return { ...setting, mode: 'edgeless' };
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</StyledEditorModeSwitch>
|
</StyledEditorModeSwitch>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Suspense, useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { allPageModeSelectAtom } from '../../../atoms';
|
import { allPageModeSelectAtom } from '../../../atoms';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
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 { useGetPageInfoById } from '../../../hooks/use-get-page-info';
|
||||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
@@ -134,7 +135,6 @@ export const BlockSuitePageList = ({
|
|||||||
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||||
const {
|
const {
|
||||||
toggleFavorite,
|
toggleFavorite,
|
||||||
removeToTrash,
|
|
||||||
restoreFromTrash,
|
restoreFromTrash,
|
||||||
permanentlyDeletePage,
|
permanentlyDeletePage,
|
||||||
cancelPublicPage,
|
cancelPublicPage,
|
||||||
@@ -144,6 +144,8 @@ export const BlockSuitePageList = ({
|
|||||||
usePageHelper(blockSuiteWorkspace);
|
usePageHelper(blockSuiteWorkspace);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const getPageInfo = useGetPageInfoById(blockSuiteWorkspace);
|
const getPageInfo = useGetPageInfoById(blockSuiteWorkspace);
|
||||||
|
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
|
||||||
|
|
||||||
const tagOptionMap = useMemo(
|
const tagOptionMap = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
@@ -246,10 +248,13 @@ export const BlockSuitePageList = ({
|
|||||||
onClickRestore: () => {
|
onClickRestore: () => {
|
||||||
restoreFromTrash(pageMeta.id);
|
restoreFromTrash(pageMeta.id);
|
||||||
},
|
},
|
||||||
removeToTrash: () => {
|
removeToTrash: () =>
|
||||||
removeToTrash(pageMeta.id);
|
setTrashModal({
|
||||||
toast(t['com.affine.toastMessage.successfullyDeleted']());
|
open: true,
|
||||||
},
|
pageId: pageMeta.id,
|
||||||
|
pageTitle: pageMeta.title,
|
||||||
|
}),
|
||||||
|
|
||||||
onRestorePage: () => {
|
onRestorePage: () => {
|
||||||
restoreFromTrash(pageMeta.id);
|
restoreFromTrash(pageMeta.id);
|
||||||
toast(
|
toast(
|
||||||
|
|||||||
@@ -133,7 +133,23 @@ globalStyle(`${root} [cmdk-item]`, {
|
|||||||
globalStyle(`${root} [cmdk-item][data-selected=true]`, {
|
globalStyle(`${root} [cmdk-item][data-selected=true]`, {
|
||||||
background: 'var(--affine-background-secondary-color)',
|
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}`, {
|
globalStyle(`${root} [cmdk-item][data-selected=true] ${itemIcon}`, {
|
||||||
color: 'var(--affine-icon-color)',
|
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)',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ const QuickSearchGroup = ({
|
|||||||
onOpenChange?.(false);
|
onOpenChange?.(false);
|
||||||
}}
|
}}
|
||||||
value={command.value}
|
value={command.value}
|
||||||
|
data-is-danger={
|
||||||
|
command.id === 'editor:page-move-to-trash' ||
|
||||||
|
command.id === 'editor:edgeless-move-to-trash'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.itemIcon}>{command.icon}</div>
|
<div className={styles.itemIcon}>{command.icon}</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import React, { useCallback, useMemo } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { pageSettingFamily } from '../../../../atoms';
|
import { pageSettingFamily } from '../../../../atoms';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
|
||||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||||
import { ReferencePage } from '../components/reference-page';
|
import { ReferencePage } from '../components/reference-page';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -44,8 +44,15 @@ export const PageOperations = ({
|
|||||||
inExcludeList: boolean;
|
inExcludeList: boolean;
|
||||||
addToExcludeList: (id: string) => void;
|
addToExcludeList: (id: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { removeToTrash } = useBlockSuiteMetaHelper(workspace);
|
|
||||||
const t = useAFFiNEI18N();
|
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<
|
const actions = useMemo<
|
||||||
Array<
|
Array<
|
||||||
| {
|
| {
|
||||||
@@ -97,9 +104,7 @@ export const PageOperations = ({
|
|||||||
</MenuIcon>
|
</MenuIcon>
|
||||||
),
|
),
|
||||||
name: t['com.affine.trashOperation.delete'](),
|
name: t['com.affine.trashOperation.delete'](),
|
||||||
click: () => {
|
click: onClickDelete,
|
||||||
removeToTrash(page.id);
|
|
||||||
},
|
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -107,10 +112,10 @@ export const PageOperations = ({
|
|||||||
inAllowList,
|
inAllowList,
|
||||||
t,
|
t,
|
||||||
inExcludeList,
|
inExcludeList,
|
||||||
|
onClickDelete,
|
||||||
removeFromAllowList,
|
removeFromAllowList,
|
||||||
page.id,
|
page.id,
|
||||||
addToExcludeList,
|
addToExcludeList,
|
||||||
removeToTrash,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
SidebarContainer,
|
SidebarContainer,
|
||||||
SidebarScrollableContainer,
|
SidebarScrollableContainer,
|
||||||
} from '@affine/component/app-sidebar';
|
} 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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
DeleteTemporarilyIcon,
|
DeleteTemporarilyIcon,
|
||||||
@@ -27,6 +27,7 @@ import { forwardRef, useCallback, useEffect, useMemo } from 'react';
|
|||||||
import { openWorkspaceListModalAtom } from '../../atoms';
|
import { openWorkspaceListModalAtom } from '../../atoms';
|
||||||
import { useHistoryAtom } from '../../atoms/history';
|
import { useHistoryAtom } from '../../atoms/history';
|
||||||
import { useAppSetting } from '../../atoms/settings';
|
import { useAppSetting } from '../../atoms/settings';
|
||||||
|
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||||
import type { AllWorkspace } from '../../shared';
|
import type { AllWorkspace } from '../../shared';
|
||||||
import { currentCollectionsAtom } from '../../utils/user-setting';
|
import { currentCollectionsAtom } from '../../utils/user-setting';
|
||||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||||
@@ -110,6 +111,20 @@ export const RootAppSidebar = ({
|
|||||||
openPage(page.id);
|
openPage(page.id);
|
||||||
}, [createPage, openPage]);
|
}, [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
|
// Listen to the "New Page" action from the menu
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
@@ -159,6 +174,12 @@ export const RootAppSidebar = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<MoveToTrash.ConfirmModal
|
||||||
|
open={trashConfirmOpen}
|
||||||
|
onConfirm={handleOnConfirm}
|
||||||
|
onOpenChange={onTrashConfirmOpenChange}
|
||||||
|
title={deletePageTitle}
|
||||||
|
/>
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<Menu
|
<Menu
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import {
|
|||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { setPageModeAtom } from '../../atoms';
|
||||||
|
import { currentModeAtom } from '../../atoms/mode';
|
||||||
import type { BlockSuiteWorkspace } from '../../shared';
|
import type { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
||||||
|
|
||||||
@@ -14,6 +17,27 @@ export function useBlockSuiteMetaHelper(
|
|||||||
usePageMetaHelper(blockSuiteWorkspace);
|
usePageMetaHelper(blockSuiteWorkspace);
|
||||||
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
||||||
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||||
|
const setPageMode = useSetAtom(setPageModeAtom);
|
||||||
|
const currentMode = useAtomValue(currentModeAtom);
|
||||||
|
|
||||||
|
const switchToPageMode = useCallback(
|
||||||
|
(pageId: string) => {
|
||||||
|
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(
|
const addToFavorite = useCallback(
|
||||||
(pageId: string) => {
|
(pageId: string) => {
|
||||||
@@ -115,6 +139,10 @@ export function useBlockSuiteMetaHelper(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
switchToPageMode,
|
||||||
|
switchToEdgelessMode,
|
||||||
|
togglePageMode,
|
||||||
|
|
||||||
publicPage,
|
publicPage,
|
||||||
cancelPublicPage,
|
cancelPublicPage,
|
||||||
|
|
||||||
|
|||||||
79
apps/core/src/hooks/affine/use-export-page.ts
Normal file
79
apps/core/src/hooks/affine/use-export-page.ts
Normal file
@@ -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<ExportType, keyof ContentParser>;
|
||||||
|
|
||||||
|
const contentParserWeakMap = new WeakMap<Page, ContentParser>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -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: <EdgelessIcon />,
|
||||||
|
// label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](),
|
||||||
|
// run() {
|
||||||
|
// document
|
||||||
|
// .querySelector<HTMLElement>('edgeless-toolbar')
|
||||||
|
// ?.shadowRoot?.querySelector<HTMLElement>(
|
||||||
|
// '.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' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
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' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
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' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
label: t['Export to PDF'](),
|
||||||
|
run() {
|
||||||
|
exportHandler('pdf');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: `editor:${mode}-export-to-html`,
|
||||||
|
preconditionStrategy,
|
||||||
|
category: `editor:${mode}`,
|
||||||
|
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
label: t['Export to HTML'](),
|
||||||
|
run() {
|
||||||
|
exportHandler('html');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: `editor:${mode}-export-to-png`,
|
||||||
|
preconditionStrategy,
|
||||||
|
category: `editor:${mode}`,
|
||||||
|
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
label: t['Export to PNG'](),
|
||||||
|
run() {
|
||||||
|
exportHandler('png');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: `editor:${mode}-export-to-markdown`,
|
||||||
|
preconditionStrategy,
|
||||||
|
category: `editor:${mode}`,
|
||||||
|
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
label: t['Export to Markdown'](),
|
||||||
|
run() {
|
||||||
|
exportHandler('markdown');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: `editor:${mode}-move-to-trash`,
|
||||||
|
preconditionStrategy,
|
||||||
|
category: `editor:${mode}`,
|
||||||
|
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
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' ? <PageIcon /> : <EdgelessIcon />,
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
27
apps/core/src/hooks/affine/use-trash-modal-helper.ts
Normal file
27
apps/core/src/hooks/affine/use-trash-modal-helper.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
const [pageMode, setPageMode] = useAtom(allPageModeSelectAtom);
|
const [pageListMode, setPageListMode] = useAtom(allPageModeSelectAtom);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
@@ -30,8 +30,8 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
t,
|
t,
|
||||||
workspace: currentWorkspace.blockSuiteWorkspace,
|
workspace: currentWorkspace.blockSuiteWorkspace,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
pageMode,
|
pageListMode,
|
||||||
setPageMode,
|
setPageListMode,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
|
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
|
||||||
@@ -54,7 +54,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
theme,
|
theme,
|
||||||
currentWorkspace.blockSuiteWorkspace,
|
currentWorkspace.blockSuiteWorkspace,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
pageMode,
|
pageListMode,
|
||||||
setPageMode,
|
setPageListMode,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type { Map as YMap } from 'yjs';
|
|||||||
import { getUIAdapter } from '../../adapters/workspace';
|
import { getUIAdapter } from '../../adapters/workspace';
|
||||||
import { setPageModeAtom } from '../../atoms';
|
import { setPageModeAtom } from '../../atoms';
|
||||||
import { currentModeAtom } from '../../atoms/mode';
|
import { currentModeAtom } from '../../atoms/mode';
|
||||||
|
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
||||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import { currentCollectionsAtom } from '../../utils/user-setting';
|
import { currentCollectionsAtom } from '../../utils/user-setting';
|
||||||
@@ -38,7 +39,7 @@ const DetailPageImpl = (): ReactElement => {
|
|||||||
const collectionManager = useCollectionManager(currentCollectionsAtom);
|
const collectionManager = useCollectionManager(currentCollectionsAtom);
|
||||||
const mode = useAtomValue(currentModeAtom);
|
const mode = useAtomValue(currentModeAtom);
|
||||||
const setPageMode = useSetAtom(setPageModeAtom);
|
const setPageMode = useSetAtom(setPageModeAtom);
|
||||||
|
useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode);
|
||||||
const onLoad = useCallback(
|
const onLoad = useCallback(
|
||||||
(page: Page, editor: EditorContainer) => {
|
(page: Page, editor: EditorContainer) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const AffineOperationCell: StoryFn<OperationCellProps> = ({
|
|||||||
}) => <OperationCell {...props} />;
|
}) => <OperationCell {...props} />;
|
||||||
|
|
||||||
AffineOperationCell.args = {
|
AffineOperationCell.args = {
|
||||||
title: 'Example Page',
|
|
||||||
favorite: false,
|
favorite: false,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
onToggleFavoritePage: () => toast('Toggle favorite page'),
|
onToggleFavoritePage: () => toast('Toggle favorite page'),
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const Basic: StoryFn = () => {
|
|||||||
workspace={localWorkspace}
|
workspace={localWorkspace}
|
||||||
onEnableAffineCloud={unimplemented}
|
onEnableAffineCloud={unimplemented}
|
||||||
togglePagePublic={unimplemented}
|
togglePagePublic={unimplemented}
|
||||||
|
exportHandler={unimplemented}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -122,6 +123,7 @@ export const AffineBasic: StoryFn = () => {
|
|||||||
workspace={affineWorkspace}
|
workspace={affineWorkspace}
|
||||||
onEnableAffineCloud={unimplemented}
|
onEnableAffineCloud={unimplemented}
|
||||||
togglePagePublic={unimplemented}
|
togglePagePublic={unimplemented}
|
||||||
|
exportHandler={unimplemented}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
blockSuiteEditorHeaderStyle,
|
blockSuiteEditorHeaderStyle,
|
||||||
blockSuiteEditorStyle,
|
blockSuiteEditorStyle,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
import { useRegisterBlocksuiteEditorCommands } from './use-register-blocksuite-editor-commands';
|
|
||||||
|
|
||||||
export type EditorProps = {
|
export type EditorProps = {
|
||||||
page: Page;
|
page: Page;
|
||||||
@@ -165,7 +164,6 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
|
|||||||
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
||||||
props: EditorProps & ErrorBoundaryProps
|
props: EditorProps & ErrorBoundaryProps
|
||||||
): ReactElement {
|
): ReactElement {
|
||||||
useRegisterBlocksuiteEditorCommands();
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallbackRender={useCallback(
|
fallbackRender={useCallback(
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import { EdgelessIcon } from '@blocksuite/icons';
|
|
||||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export function useRegisterBlocksuiteEditorCommands() {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
useEffect(() => {
|
|
||||||
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: <EdgelessIcon />,
|
|
||||||
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<HTMLElement>('edgeless-toolbar')
|
|
||||||
?.shadowRoot?.querySelector<HTMLElement>(
|
|
||||||
'.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child'
|
|
||||||
)
|
|
||||||
?.click();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
unsubs.forEach(unsub => unsub());
|
|
||||||
};
|
|
||||||
}, [t]);
|
|
||||||
}
|
|
||||||
@@ -131,7 +131,6 @@ export const AllPagesBody = ({
|
|||||||
active={!!favorite}
|
active={!!favorite}
|
||||||
/>
|
/>
|
||||||
<OperationCell
|
<OperationCell
|
||||||
title={title}
|
|
||||||
favorite={favorite}
|
favorite={favorite}
|
||||||
isPublic={isPublicPage}
|
isPublic={isPublicPage}
|
||||||
onOpenPageInNewTab={onOpenPageInNewTab}
|
onOpenPageInNewTab={onOpenPageInNewTab}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { FlexWrapper } from '../../..';
|
|||||||
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
|
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
|
||||||
|
|
||||||
export interface OperationCellProps {
|
export interface OperationCellProps {
|
||||||
title: string;
|
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
onOpenPageInNewTab: () => void;
|
onOpenPageInNewTab: () => void;
|
||||||
@@ -27,7 +26,6 @@ export interface OperationCellProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const OperationCell = ({
|
export const OperationCell = ({
|
||||||
title,
|
|
||||||
favorite,
|
favorite,
|
||||||
isPublic,
|
isPublic,
|
||||||
onOpenPageInNewTab,
|
onOpenPageInNewTab,
|
||||||
@@ -36,7 +34,6 @@ export const OperationCell = ({
|
|||||||
onDisablePublicSharing,
|
onDisablePublicSharing,
|
||||||
}: OperationCellProps) => {
|
}: OperationCellProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [openDisableShared, setOpenDisableShared] = useState(false);
|
const [openDisableShared, setOpenDisableShared] = useState(false);
|
||||||
|
|
||||||
const OperationMenu = (
|
const OperationMenu = (
|
||||||
@@ -77,12 +74,7 @@ export const OperationCell = ({
|
|||||||
{t['com.affine.openPageOperation.newTab']()}
|
{t['com.affine.openPageOperation.newTab']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MoveToTrash
|
<MoveToTrash data-testid="move-to-trash" onSelect={onRemoveToTrash} />
|
||||||
data-testid="move-to-trash"
|
|
||||||
onSelect={() => {
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@@ -99,15 +91,6 @@ export const OperationCell = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Menu>
|
</Menu>
|
||||||
</FlexWrapper>
|
</FlexWrapper>
|
||||||
<MoveToTrash.ConfirmModal
|
|
||||||
open={open}
|
|
||||||
title={title}
|
|
||||||
onConfirm={() => {
|
|
||||||
onRemoveToTrash();
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
/>
|
|
||||||
<DisablePublicSharing.DisablePublicSharingModal
|
<DisablePublicSharing.DisablePublicSharingModal
|
||||||
onConfirm={onDisablePublicSharing}
|
onConfirm={onDisablePublicSharing}
|
||||||
open={openDisableShared}
|
open={openDisableShared}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
|
||||||
import {
|
import {
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
ExportToHtmlIcon,
|
ExportToHtmlIcon,
|
||||||
@@ -9,264 +7,107 @@ import {
|
|||||||
ExportToPngIcon,
|
ExportToPngIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import { MenuIcon, MenuItem, MenuSub } from '@toeverything/components/menu';
|
import { MenuIcon, MenuItem, MenuSub } from '@toeverything/components/menu';
|
||||||
import { useSetAtom } from 'jotai';
|
import { type ReactNode, useMemo } from 'react';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { getContentParser } from './get-content-parser';
|
|
||||||
import { transitionStyle } from './index.css';
|
import { transitionStyle } from './index.css';
|
||||||
import type { CommonMenuItemProps } from './types';
|
|
||||||
|
|
||||||
export const ExportToPdfMenuItem = ({
|
interface ExportMenuItemProps<T> {
|
||||||
|
onSelect: () => void;
|
||||||
|
className?: string;
|
||||||
|
type: T;
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExportProps {
|
||||||
|
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportMenuItem<T>({
|
||||||
onSelect,
|
onSelect,
|
||||||
className,
|
className,
|
||||||
}: CommonMenuItemProps<{ type: 'pdf' }>) => {
|
type,
|
||||||
const t = useAFFiNEI18N();
|
icon,
|
||||||
const { currentEditor } = globalThis;
|
label,
|
||||||
const setPushNotification = useSetAtom(pushNotificationAtom);
|
}: ExportMenuItemProps<T>) {
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={className}
|
className={className}
|
||||||
data-testid="export-to-pdf"
|
data-testid={`export-to-${type}`}
|
||||||
onSelect={onClickDownloadPDF}
|
onSelect={onSelect}
|
||||||
block
|
block
|
||||||
preFix={
|
preFix={<MenuIcon>{icon}</MenuIcon>}
|
||||||
<MenuIcon>
|
|
||||||
<ExportToPdfIcon />
|
|
||||||
</MenuIcon>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{t['Export to PDF']()}
|
{label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const ExportToHtmlMenuItem = ({
|
export const ExportMenuItems = ({
|
||||||
onSelect,
|
exportHandler,
|
||||||
className,
|
className = transitionStyle,
|
||||||
}: CommonMenuItemProps<{ type: 'html' }>) => {
|
}: ExportProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { currentEditor } = globalThis;
|
const itemMap = useMemo(
|
||||||
const setPushNotification = useSetAtom(pushNotificationAtom);
|
() => [
|
||||||
const onClickExportHtml = useCallback(() => {
|
{
|
||||||
if (!currentEditor) {
|
component: ExportMenuItem,
|
||||||
return;
|
props: {
|
||||||
}
|
onSelect: () => exportHandler('pdf'),
|
||||||
const contentParser = getContentParser(currentEditor.page);
|
className: className,
|
||||||
contentParser
|
type: 'pdf',
|
||||||
.exportHtml()
|
icon: <ExportToPdfIcon />,
|
||||||
.then(() => {
|
label: t['Export to PDF'](),
|
||||||
onSelect?.({ type: 'html' });
|
},
|
||||||
setPushNotification({
|
},
|
||||||
title: t['com.affine.export.success.title'](),
|
{
|
||||||
message: t['com.affine.export.success.message'](),
|
component: ExportMenuItem,
|
||||||
type: 'success',
|
props: {
|
||||||
});
|
onSelect: () => exportHandler('html'),
|
||||||
})
|
className: className,
|
||||||
.catch(err => {
|
type: 'html',
|
||||||
console.error(err);
|
icon: <ExportToHtmlIcon />,
|
||||||
setPushNotification({
|
label: t['Export to HTML'](),
|
||||||
title: t['com.affine.export.error.title'](),
|
},
|
||||||
message: t['com.affine.export.error.message'](),
|
},
|
||||||
type: 'error',
|
{
|
||||||
});
|
component: ExportMenuItem,
|
||||||
});
|
props: {
|
||||||
onSelect?.({ type: 'html' });
|
onSelect: () => exportHandler('png'),
|
||||||
}, [currentEditor, onSelect, setPushNotification, t]);
|
className: className,
|
||||||
return (
|
type: 'png',
|
||||||
<>
|
icon: <ExportToPngIcon />,
|
||||||
<MenuItem
|
label: t['Export to PNG'](),
|
||||||
className={className}
|
},
|
||||||
data-testid="export-to-html"
|
},
|
||||||
onSelect={onClickExportHtml}
|
{
|
||||||
block
|
component: ExportMenuItem,
|
||||||
preFix={
|
props: {
|
||||||
<MenuIcon>
|
onSelect: () => exportHandler('markdown'),
|
||||||
<ExportToHtmlIcon />
|
className: className,
|
||||||
</MenuIcon>
|
type: 'markdown',
|
||||||
}
|
icon: <ExportToMarkdownIcon />,
|
||||||
>
|
label: t['Export to Markdown'](),
|
||||||
{t['Export to HTML']()}
|
},
|
||||||
</MenuItem>
|
},
|
||||||
</>
|
],
|
||||||
|
[className, exportHandler, t]
|
||||||
);
|
);
|
||||||
|
const items = itemMap.map(({ component: Component, props }) => (
|
||||||
|
<Component key={props.label} {...props} />
|
||||||
|
));
|
||||||
|
return <>{items}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExportToPngMenuItem = ({
|
export const Export = ({ exportHandler, className }: ExportProps) => {
|
||||||
onSelect,
|
|
||||||
className,
|
|
||||||
}: CommonMenuItemProps<{ type: 'png' }>) => {
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { currentEditor } = globalThis;
|
const items = (
|
||||||
const setPushNotification = useSetAtom(pushNotificationAtom);
|
<ExportMenuItems exportHandler={exportHandler} className={className} />
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<MenuItem
|
|
||||||
className={className}
|
|
||||||
data-testid="export-to-png"
|
|
||||||
onSelect={onClickDownloadPNG}
|
|
||||||
block
|
|
||||||
preFix={
|
|
||||||
<MenuIcon>
|
|
||||||
<ExportToPngIcon />
|
|
||||||
</MenuIcon>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t['Export to PNG']()}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<MenuItem
|
|
||||||
className={className}
|
|
||||||
data-testid="export-to-markdown"
|
|
||||||
onSelect={onClickExportMarkdown}
|
|
||||||
block
|
|
||||||
preFix={
|
|
||||||
<MenuIcon>
|
|
||||||
<ExportToMarkdownIcon />
|
|
||||||
</MenuIcon>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t['Export to Markdown']()}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<MenuSub
|
<MenuSub
|
||||||
items={
|
items={items}
|
||||||
<>
|
|
||||||
<ExportToPdfMenuItem
|
|
||||||
className={transitionStyle}
|
|
||||||
></ExportToPdfMenuItem>
|
|
||||||
<ExportToHtmlMenuItem
|
|
||||||
className={transitionStyle}
|
|
||||||
></ExportToHtmlMenuItem>
|
|
||||||
<ExportToPngMenuItem
|
|
||||||
className={transitionStyle}
|
|
||||||
></ExportToPngMenuItem>
|
|
||||||
<ExportToMarkdownMenuItem
|
|
||||||
className={transitionStyle}
|
|
||||||
></ExportToMarkdownMenuItem>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
triggerOptions={{
|
triggerOptions={{
|
||||||
className: transitionStyle,
|
className: transitionStyle,
|
||||||
preFix: (
|
preFix: (
|
||||||
|
|||||||
@@ -3,20 +3,19 @@ import { LinkIcon } from '@blocksuite/icons';
|
|||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { Divider } from '@toeverything/components/divider';
|
import { Divider } from '@toeverything/components/divider';
|
||||||
|
|
||||||
import {
|
import { ExportMenuItems } from '../page-list/operation-menu-items/export';
|
||||||
ExportToHtmlMenuItem,
|
|
||||||
ExportToMarkdownMenuItem,
|
|
||||||
ExportToPdfMenuItem,
|
|
||||||
ExportToPngMenuItem,
|
|
||||||
} from '../page-list/operation-menu-items/export';
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import type { ShareMenuProps } from './share-menu';
|
import type { ShareMenuProps } from './share-menu';
|
||||||
import { useSharingUrl } from './use-share-url';
|
import { useSharingUrl } from './use-share-url';
|
||||||
|
|
||||||
export const ShareExport = (props: ShareMenuProps) => {
|
export const ShareExport = ({
|
||||||
|
workspace,
|
||||||
|
currentPage,
|
||||||
|
exportHandler,
|
||||||
|
}: ShareMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspaceId = props.workspace.id;
|
const workspaceId = workspace.id;
|
||||||
const pageId = props.currentPage.id;
|
const pageId = currentPage.id;
|
||||||
const { onClickCopyLink } = useSharingUrl({
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
pageId,
|
pageId,
|
||||||
@@ -29,10 +28,10 @@ export const ShareExport = (props: ShareMenuProps) => {
|
|||||||
{t['com.affine.share-menu.ShareViaExport']()}
|
{t['com.affine.share-menu.ShareViaExport']()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ExportToPdfMenuItem className={styles.menuItemStyle} />
|
<ExportMenuItems
|
||||||
<ExportToHtmlMenuItem className={styles.menuItemStyle} />
|
exportHandler={exportHandler}
|
||||||
<ExportToPngMenuItem className={styles.menuItemStyle} />
|
className={styles.menuItemStyle}
|
||||||
<ExportToMarkdownMenuItem className={styles.menuItemStyle} />
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.columnContainerStyle}>
|
<div className={styles.columnContainerStyle}>
|
||||||
<div className={styles.descriptionStyle}>
|
<div className={styles.descriptionStyle}>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface ShareMenuProps<
|
|||||||
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
|
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
|
||||||
onEnableAffineCloud: () => void;
|
onEnableAffineCloud: () => void;
|
||||||
togglePagePublic: () => Promise<void>;
|
togglePagePublic: () => Promise<void>;
|
||||||
|
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShareMenu = (props: ShareMenuProps) => {
|
export const ShareMenu = (props: ShareMenuProps) => {
|
||||||
|
|||||||
@@ -615,5 +615,8 @@
|
|||||||
"com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace",
|
"com.affine.cmdk.affine.navigation.goto-workspace": "Go to Workspace",
|
||||||
"com.affine.cmdk.affine.category.affine.edgeless": "Edgeless",
|
"com.affine.cmdk.affine.category.affine.edgeless": "Edgeless",
|
||||||
"com.affine.cmdk.affine.category.affine.collections": "Collections",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ test('Show collections items in sidebar', async ({ page }) => {
|
|||||||
.getByTestId('collection-page-option')
|
.getByTestId('collection-page-option')
|
||||||
.getByText('Delete');
|
.getByText('Delete');
|
||||||
await deletePage.click();
|
await deletePage.click();
|
||||||
|
await page.getByTestId('confirm-delete-page').click();
|
||||||
expect(await collections.getByTestId('collection-page').count()).toBe(0);
|
expect(await collections.getByTestId('collection-page').count()).toBe(0);
|
||||||
await first.hover();
|
await first.hover();
|
||||||
await first.getByTestId('collection-options').click();
|
await first.getByTestId('collection-options').click();
|
||||||
|
|||||||
@@ -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) {
|
async function assertTitle(page: Page, text: string) {
|
||||||
const edgeless = page.locator('affine-edgeless-page');
|
const edgeless = page.locator('affine-edgeless-page');
|
||||||
if (!edgeless) {
|
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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user