mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 04:04:27 +08:00
feat(core): add history shortcut (#4595)
This commit is contained in:
@@ -27,7 +27,9 @@ 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 { useGeneralShortcuts } from '../../hooks/affine/use-shortcuts';
|
||||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||||
|
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/use-shortcut-commands';
|
||||||
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';
|
||||||
@@ -105,6 +107,8 @@ export const RootAppSidebar = ({
|
|||||||
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
|
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
|
||||||
openWorkspaceListModalAtom
|
openWorkspaceListModalAtom
|
||||||
);
|
);
|
||||||
|
const generalShortcutsInfo = useGeneralShortcuts();
|
||||||
|
|
||||||
const onClickNewPage = useCallback(async () => {
|
const onClickNewPage = useCallback(async () => {
|
||||||
const page = createPage();
|
const page = createPage();
|
||||||
await page.waitForLoaded();
|
await page.waitForLoaded();
|
||||||
@@ -161,7 +165,7 @@ export const RootAppSidebar = ({
|
|||||||
const closeUserWorkspaceList = useCallback(() => {
|
const closeUserWorkspaceList = useCallback(() => {
|
||||||
setOpenUserWorkspaceList(false);
|
setOpenUserWorkspaceList(false);
|
||||||
}, [setOpenUserWorkspaceList]);
|
}, [setOpenUserWorkspaceList]);
|
||||||
|
useRegisterBlocksuiteEditorCommands(router.back, router.forward);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
@@ -173,6 +177,7 @@ export const RootAppSidebar = ({
|
|||||||
environment.isMacOs
|
environment.isMacOs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
generalShortcutsInfo={generalShortcutsInfo}
|
||||||
>
|
>
|
||||||
<MoveToTrash.ConfirmModal
|
<MoveToTrash.ConfirmModal
|
||||||
open={trashConfirmOpen}
|
open={trashConfirmOpen}
|
||||||
|
|||||||
@@ -81,9 +81,8 @@ export const useWinGeneralKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
// not implement yet
|
// not implement yet
|
||||||
// [t('appendDailyNote')]: 'Ctrl + Alt + A',
|
// [t('appendDailyNote')]: 'Ctrl + Alt + A',
|
||||||
[t('expandOrCollapseSidebar')]: ['Ctrl', '/'],
|
[t('expandOrCollapseSidebar')]: ['Ctrl', '/'],
|
||||||
// not implement yet
|
[t('goBack')]: ['Ctrl + ['],
|
||||||
// [t('goBack')]: 'Ctrl + [',
|
[t('goForward')]: ['Ctrl + ]'],
|
||||||
// [t('goForward')]: 'Ctrl + ]',
|
|
||||||
}),
|
}),
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
@@ -98,9 +97,8 @@ export const useMacGeneralKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
// not implement yet
|
// not implement yet
|
||||||
// [t('appendDailyNote')]: '⌘ + ⌥ + A',
|
// [t('appendDailyNote')]: '⌘ + ⌥ + A',
|
||||||
[t('expandOrCollapseSidebar')]: ['⌘', '/'],
|
[t('expandOrCollapseSidebar')]: ['⌘', '/'],
|
||||||
// not implement yet
|
[t('goBack')]: ['⌘ + ['],
|
||||||
// [t('goBack')]: '⌘ + [',
|
[t('goForward')]: ['⌘ + ]'],
|
||||||
// [t('goForward')]: '⌘ + ]',
|
|
||||||
}),
|
}),
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
PreconditionStrategy,
|
||||||
|
registerAffineCommand,
|
||||||
|
} from '@toeverything/infra/command';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useRegisterBlocksuiteEditorCommands(
|
||||||
|
back: () => unknown,
|
||||||
|
forward: () => unknown
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubs: Array<() => void> = [];
|
||||||
|
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:shortcut-history-go-back',
|
||||||
|
category: 'affine:general',
|
||||||
|
preconditionStrategy: PreconditionStrategy.Never,
|
||||||
|
icon: 'none',
|
||||||
|
label: 'go back',
|
||||||
|
keyBinding: {
|
||||||
|
binding: '$mod+[',
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
back();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
unsubs.push(
|
||||||
|
registerAffineCommand({
|
||||||
|
id: 'affine:shortcut-history-go-forward',
|
||||||
|
category: 'affine:general',
|
||||||
|
preconditionStrategy: PreconditionStrategy.Never,
|
||||||
|
icon: 'none',
|
||||||
|
label: 'go forward',
|
||||||
|
keyBinding: {
|
||||||
|
binding: '$mod+]',
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
forward();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubs.forEach(unsub => unsub());
|
||||||
|
};
|
||||||
|
}, [back, forward]);
|
||||||
|
}
|
||||||
@@ -110,7 +110,10 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
data-enable-animation={enableAnimation && !isResizing}
|
data-enable-animation={enableAnimation && !isResizing}
|
||||||
>
|
>
|
||||||
<nav className={navStyle} ref={navRef} data-testid="app-sidebar">
|
<nav className={navStyle} ref={navRef} data-testid="app-sidebar">
|
||||||
<SidebarHeader router={props.router} />
|
<SidebarHeader
|
||||||
|
router={props.router}
|
||||||
|
generalShortcutsInfo={props.generalShortcutsInfo}
|
||||||
|
/>
|
||||||
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
import { IconButton } from '@toeverything/components/button';
|
import { IconButton } from '@toeverything/components/button';
|
||||||
|
import { Tooltip } from '@toeverything/components/tooltip';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import type { History } from '..';
|
import type { History } from '..';
|
||||||
import {
|
import {
|
||||||
@@ -17,10 +20,32 @@ export type SidebarHeaderProps = {
|
|||||||
forward: () => unknown;
|
forward: () => unknown;
|
||||||
history: History;
|
history: History;
|
||||||
};
|
};
|
||||||
|
generalShortcutsInfo?: {
|
||||||
|
shortcuts: {
|
||||||
|
[title: string]: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarHeader = (props: SidebarHeaderProps) => {
|
export const SidebarHeader = (props: SidebarHeaderProps) => {
|
||||||
const open = useAtomValue(appSidebarOpenAtom);
|
const open = useAtomValue(appSidebarOpenAtom);
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
const shortcuts = props.generalShortcutsInfo?.shortcuts;
|
||||||
|
const shortcutsObject = useMemo(() => {
|
||||||
|
const goBack = t['com.affine.keyboardShortcuts.goBack']();
|
||||||
|
const goBackShortcut = shortcuts?.[goBack];
|
||||||
|
|
||||||
|
const goForward = t['com.affine.keyboardShortcuts.goForward']();
|
||||||
|
const goForwardShortcut = shortcuts?.[goForward];
|
||||||
|
return {
|
||||||
|
goBack,
|
||||||
|
goBackShortcut,
|
||||||
|
goForward,
|
||||||
|
goForwardShortcut,
|
||||||
|
};
|
||||||
|
}, [shortcuts, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={navHeaderStyle}
|
className={navHeaderStyle}
|
||||||
@@ -29,33 +54,43 @@ export const SidebarHeader = (props: SidebarHeaderProps) => {
|
|||||||
>
|
>
|
||||||
<SidebarSwitch show={open} />
|
<SidebarSwitch show={open} />
|
||||||
<div className={navHeaderNavigationButtons}>
|
<div className={navHeaderNavigationButtons}>
|
||||||
<IconButton
|
<Tooltip
|
||||||
className={navHeaderButton}
|
content={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
|
||||||
data-testid="app-sidebar-arrow-button-back"
|
side="bottom"
|
||||||
disabled={props.router?.history.current === 0}
|
|
||||||
onClick={() => {
|
|
||||||
props.router?.back();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ArrowLeftSmallIcon />
|
<IconButton
|
||||||
</IconButton>
|
className={navHeaderButton}
|
||||||
<IconButton
|
data-testid="app-sidebar-arrow-button-back"
|
||||||
className={navHeaderButton}
|
disabled={props.router?.history.current === 0}
|
||||||
data-testid="app-sidebar-arrow-button-forward"
|
onClick={() => {
|
||||||
disabled={
|
props.router?.back();
|
||||||
props.router
|
}}
|
||||||
? (props.router.history.stack.length > 0 &&
|
>
|
||||||
props.router.history.current ===
|
<ArrowLeftSmallIcon />
|
||||||
props.router.history.stack.length - 1) ||
|
</IconButton>
|
||||||
props.router.history.stack.length === 0
|
</Tooltip>
|
||||||
: true
|
<Tooltip
|
||||||
}
|
content={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
|
||||||
onClick={() => {
|
side="bottom"
|
||||||
props.router?.forward();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ArrowRightSmallIcon />
|
<IconButton
|
||||||
</IconButton>
|
className={navHeaderButton}
|
||||||
|
data-testid="app-sidebar-arrow-button-forward"
|
||||||
|
disabled={
|
||||||
|
props.router
|
||||||
|
? (props.router.history.stack.length > 0 &&
|
||||||
|
props.router.history.current ===
|
||||||
|
props.router.history.stack.length - 1) ||
|
||||||
|
props.router.history.stack.length === 0
|
||||||
|
: true
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
props.router?.forward();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRightSmallIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { platform } from 'node:os';
|
// import { platform } from 'node:os';
|
||||||
|
|
||||||
import { test } from '@affine-test/kit/electron';
|
import { test } from '@affine-test/kit/electron';
|
||||||
|
import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard';
|
||||||
import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic';
|
import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic';
|
||||||
import {
|
import {
|
||||||
clickSideBarCurrentWorkspaceBanner,
|
clickSideBarCurrentWorkspaceBanner,
|
||||||
clickSideBarSettingButton,
|
clickSideBarSettingButton,
|
||||||
} from '@affine-test/kit/utils/sidebar';
|
} from '@affine-test/kit/utils/sidebar';
|
||||||
import { expect } from '@playwright/test';
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const historyShortcut = async (page: Page, command: 'goBack' | 'goForward') => {
|
||||||
|
await withCtrlOrMeta(page, () =>
|
||||||
|
page.keyboard.press(command === 'goBack' ? '[' : ']', { delay: 50 })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
test('new page', async ({ page, workspace }) => {
|
test('new page', async ({ page, workspace }) => {
|
||||||
await page.getByTestId('new-page-button').click({
|
await page.getByTestId('new-page-button').click({
|
||||||
@@ -18,73 +25,85 @@ test('new page', async ({ page, workspace }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// macOS only
|
// macOS only
|
||||||
if (platform() === 'darwin') {
|
// if (platform() === 'darwin') {
|
||||||
test('app sidebar router forward/back', async ({ page }) => {
|
test('app sidebar router forward/back', async ({ page }) => {
|
||||||
await page.getByTestId('help-island').click();
|
await page.getByTestId('help-island').click();
|
||||||
await page.getByTestId('easy-guide').click();
|
await page.getByTestId('easy-guide').click();
|
||||||
await page.getByTestId('onboarding-modal-next-button').click();
|
await page.getByTestId('onboarding-modal-next-button').click();
|
||||||
await page.getByTestId('onboarding-modal-close-button').click();
|
await page.getByTestId('onboarding-modal-close-button').click();
|
||||||
{
|
{
|
||||||
// create pages
|
// create pages
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.getByTestId('new-page-button').click({
|
await page.getByTestId('new-page-button').click({
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await page.waitForSelector('v-line');
|
await page.waitForSelector('v-line');
|
||||||
const title = getBlockSuiteEditorTitle(page);
|
const title = getBlockSuiteEditorTitle(page);
|
||||||
await title.focus();
|
await title.focus();
|
||||||
await title.pressSequentially('test1', {
|
await title.pressSequentially('test1', {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.getByTestId('new-page-button').click({
|
await page.getByTestId('new-page-button').click({
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await page.waitForSelector('v-line');
|
await page.waitForSelector('v-line');
|
||||||
|
|
||||||
await title.focus();
|
await title.focus();
|
||||||
await title.pressSequentially('test2', {
|
await title.pressSequentially('test2', {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.getByTestId('new-page-button').click({
|
await page.getByTestId('new-page-button').click({
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await page.waitForSelector('v-line');
|
await page.waitForSelector('v-line');
|
||||||
await title.focus();
|
await title.focus();
|
||||||
await title.pressSequentially('test3', {
|
await title.pressSequentially('test3', {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const title = (await page
|
const title = (await page
|
||||||
.locator('.affine-doc-page-block-title')
|
.locator('.affine-doc-page-block-title')
|
||||||
.textContent()) as string;
|
.textContent()) as string;
|
||||||
expect(title.trim()).toBe('test3');
|
expect(title.trim()).toBe('test3');
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
|
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
|
||||||
await page.waitForTimeout(1000);
|
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
|
{
|
||||||
await page.waitForTimeout(1000);
|
const title = (await page
|
||||||
{
|
.locator('.affine-doc-page-block-title')
|
||||||
const title = (await page
|
.textContent()) as string;
|
||||||
.locator('.affine-doc-page-block-title')
|
expect(title.trim()).toBe('test1');
|
||||||
.textContent()) as string;
|
}
|
||||||
expect(title.trim()).toBe('test1');
|
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
||||||
}
|
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
{
|
||||||
await page.waitForTimeout(1000);
|
const title = (await page
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
.locator('.affine-doc-page-block-title')
|
||||||
await page.waitForTimeout(1000);
|
.textContent()) as string;
|
||||||
{
|
expect(title.trim()).toBe('test3');
|
||||||
const title = (await page
|
}
|
||||||
.locator('.affine-doc-page-block-title')
|
await historyShortcut(page, 'goBack');
|
||||||
.textContent()) as string;
|
await historyShortcut(page, 'goBack');
|
||||||
expect(title.trim()).toBe('test3');
|
{
|
||||||
}
|
const title = (await page
|
||||||
});
|
.locator('.affine-doc-page-block-title')
|
||||||
}
|
.textContent()) as string;
|
||||||
|
expect(title.trim()).toBe('test1');
|
||||||
|
}
|
||||||
|
await historyShortcut(page, 'goForward');
|
||||||
|
await historyShortcut(page, 'goForward');
|
||||||
|
{
|
||||||
|
const title = (await page
|
||||||
|
.locator('.affine-doc-page-block-title')
|
||||||
|
.textContent()) as string;
|
||||||
|
expect(title.trim()).toBe('test3');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// }
|
||||||
|
|
||||||
test('clientBorder value should disable by default on window', async ({
|
test('clientBorder value should disable by default on window', async ({
|
||||||
page,
|
page,
|
||||||
|
|||||||
Reference in New Issue
Block a user