feat(core): add editor commanads (#4514)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
JimmFly
2023-10-02 11:22:12 +08:00
committed by GitHub
parent aab1a1e50a
commit 69db99636b
29 changed files with 673 additions and 413 deletions

View File

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

View File

@@ -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 (
<>
<ShareMenu
@@ -26,6 +28,7 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
useIsSharedPage={useIsSharedPage}
onEnableAffineCloud={() => setOpen(true)}
togglePagePublic={async () => {}}
exportHandler={exportHandler}
/>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<EnableAffineCloudModal

View File

@@ -23,12 +23,15 @@ import {
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useRef, useState } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useRef } from 'react';
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 { 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 { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { toast } from '../../../utils';
@@ -43,72 +46,81 @@ type PageMenuProps = {
export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
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) => {
<MenuItem
preFix={
<MenuIcon>
{mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
{currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
</MenuIcon>
}
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']()}
</MenuItem>
@@ -194,13 +206,11 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
{t['Import']()}
</MenuItem>
<Export />
<Export exportHandler={exportHandler} />
<MenuSeparator />
<MoveToTrash
data-testid="editor-option-menu-delete"
onSelect={() => {
setOpenConfirm(true);
}}
onSelect={handleOpenTrashModal}
/>
</>
);
@@ -218,12 +228,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
<HeaderDropDownButton />
</Menu>
<MoveToTrash.ConfirmModal
open={openConfirm}
title={pageMeta.title}
onConfirm={handleOnConfirm}
onOpenChange={setOpenConfirm}
/>
</FlexWrapper>
</>
);

View File

@@ -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 (
<Tooltip content={<TooltipContent />}>
@@ -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}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={currentMode === 'edgeless'}
hide={trash && currentMode !== 'edgeless'}
trash={trash}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'edgeless') {
toast(t['com.affine.toastMessage.edgelessMode']());
}
return { ...setting, mode: 'edgeless' };
});
}}
onClick={onSwitchToEdgelessMode}
/>
</StyledEditorModeSwitch>
</Tooltip>

View File

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

View File

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

View File

@@ -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'
}
>
<div className={styles.itemIcon}>{command.icon}</div>
<div

View File

@@ -24,7 +24,7 @@ import React, { useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
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 { ReferencePage } from '../components/reference-page';
import * as styles from './styles.css';
@@ -44,8 +44,15 @@ export const PageOperations = ({
inExcludeList: boolean;
addToExcludeList: (id: string) => 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 = ({
</MenuIcon>
),
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 (

View File

@@ -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 = ({
)
}
>
<MoveToTrash.ConfirmModal
open={trashConfirmOpen}
onConfirm={handleOnConfirm}
onOpenChange={onTrashConfirmOpenChange}
title={deletePageTitle}
/>
<SidebarContainer>
<Menu
rootOptions={{