mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 22:07:09 +08:00
feat(electron): track router history (#2336)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { platform } from 'node:os';
|
||||||
|
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import { test } from './fixture';
|
import { test } from './fixture';
|
||||||
@@ -11,6 +13,73 @@ test('new page', async ({ page, workspace }) => {
|
|||||||
expect(flavour).toBe('local');
|
expect(flavour).toBe('local');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// macOS only
|
||||||
|
if (platform() === 'darwin') {
|
||||||
|
test('app sidebar router forward/back', async ({ page }) => {
|
||||||
|
await page.getByTestId('help-island').click();
|
||||||
|
await page.getByTestId('easy-guide').click();
|
||||||
|
await page.getByTestId('onboarding-modal-next-button').click();
|
||||||
|
await page.getByTestId('onboarding-modal-close-button').click();
|
||||||
|
{
|
||||||
|
// create pages
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.getByTestId('new-page-button').click({
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
await page.waitForSelector('v-line');
|
||||||
|
await page.focus('.affine-default-page-block-title');
|
||||||
|
await page.type('.affine-default-page-block-title', 'test1', {
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.getByTestId('new-page-button').click({
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
await page.waitForSelector('v-line');
|
||||||
|
await page.focus('.affine-default-page-block-title');
|
||||||
|
await page.type('.affine-default-page-block-title', 'test2', {
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.getByTestId('new-page-button').click({
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
await page.waitForSelector('v-line');
|
||||||
|
await page.focus('.affine-default-page-block-title');
|
||||||
|
await page.type('.affine-default-page-block-title', 'test3', {
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const title = (await page
|
||||||
|
.locator('.affine-default-page-block-title')
|
||||||
|
.textContent()) as string;
|
||||||
|
expect(title.trim()).toBe('test3');
|
||||||
|
}
|
||||||
|
|
||||||
|
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.waitForTimeout(1000);
|
||||||
|
{
|
||||||
|
const title = (await page
|
||||||
|
.locator('.affine-default-page-block-title')
|
||||||
|
.textContent()) as string;
|
||||||
|
expect(title.trim()).toBe('test1');
|
||||||
|
}
|
||||||
|
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
{
|
||||||
|
const title = (await page
|
||||||
|
.locator('.affine-default-page-block-title')
|
||||||
|
.textContent()) as string;
|
||||||
|
expect(title.trim()).toBe('test3');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('app theme', async ({ page, electronApp }) => {
|
test('app theme', async ({ page, electronApp }) => {
|
||||||
const root = page.locator('html');
|
const root = page.locator('html');
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
enableCoverage,
|
enableCoverage,
|
||||||
istanbulTempDir,
|
istanbulTempDir,
|
||||||
test as base,
|
test as base,
|
||||||
|
testResultDir,
|
||||||
} from '@affine-test/kit/playwright';
|
} from '@affine-test/kit/playwright';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import type { ElectronApplication, Page } from 'playwright';
|
import type { ElectronApplication, Page } from 'playwright';
|
||||||
@@ -90,6 +91,9 @@ export const test = base.extend<{
|
|||||||
'.bin',
|
'.bin',
|
||||||
`electron${ext}`
|
`electron${ext}`
|
||||||
),
|
),
|
||||||
|
recordVideo: {
|
||||||
|
dir: testResultDir,
|
||||||
|
},
|
||||||
colorScheme: 'light',
|
colorScheme: 'light',
|
||||||
});
|
});
|
||||||
await use(electronApp);
|
await use(electronApp);
|
||||||
|
|||||||
99
apps/web/src/atoms/history.ts
Normal file
99
apps/web/src/atoms/history.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
export type History = {
|
||||||
|
stack: string[];
|
||||||
|
current: number;
|
||||||
|
skip: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MAX_HISTORY = 50;
|
||||||
|
|
||||||
|
export const historyBaseAtom = atom<History>({
|
||||||
|
stack: [],
|
||||||
|
current: 0,
|
||||||
|
skip: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// fixme(himself65): don't use hooks, use atom lifecycle instead
|
||||||
|
export function useTrackRouterHistoryEffect() {
|
||||||
|
const setBase = useSetAtom(historyBaseAtom);
|
||||||
|
const router = useRouter();
|
||||||
|
useEffect(() => {
|
||||||
|
const callback = (url: string) => {
|
||||||
|
setBase(prev => {
|
||||||
|
console.log('push', url, prev.skip, prev.stack.length, prev.current);
|
||||||
|
if (prev.skip) {
|
||||||
|
return {
|
||||||
|
stack: [...prev.stack],
|
||||||
|
current: prev.current,
|
||||||
|
skip: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (prev.current < prev.stack.length - 1) {
|
||||||
|
const newStack = prev.stack.slice(0, prev.current);
|
||||||
|
newStack.push(url);
|
||||||
|
if (newStack.length > MAX_HISTORY) {
|
||||||
|
newStack.shift();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stack: newStack,
|
||||||
|
current: newStack.length - 1,
|
||||||
|
skip: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const newStack = [...prev.stack, url];
|
||||||
|
if (newStack.length > MAX_HISTORY) {
|
||||||
|
newStack.shift();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stack: newStack,
|
||||||
|
current: newStack.length - 1,
|
||||||
|
skip: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
router.events.on('routeChangeComplete', callback);
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeComplete', callback);
|
||||||
|
};
|
||||||
|
}, [router.events, setBase]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHistoryAtom() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [base, setBase] = useAtom(historyBaseAtom);
|
||||||
|
return [
|
||||||
|
base,
|
||||||
|
useCallback(
|
||||||
|
(forward: boolean) => {
|
||||||
|
setBase(prev => {
|
||||||
|
if (forward) {
|
||||||
|
const target = Math.min(prev.stack.length - 1, prev.current + 1);
|
||||||
|
const url = prev.stack[target];
|
||||||
|
void router.push(url);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
current: target,
|
||||||
|
skip: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const target = Math.max(0, prev.current - 1);
|
||||||
|
const url = prev.stack[target];
|
||||||
|
void router.push(url);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
current: target,
|
||||||
|
skip: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[router, setBase]
|
||||||
|
),
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
@@ -23,8 +23,9 @@ import type { Page } from '@blocksuite/store';
|
|||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useHistoryAtom } from '../../atoms/history';
|
||||||
import type { AllWorkspace } from '../../shared';
|
import type { AllWorkspace } from '../../shared';
|
||||||
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
import FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
|
||||||
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
|
||||||
@@ -115,10 +116,22 @@ export const RootAppSidebar = ({
|
|||||||
}, [sidebarOpen, setSidebarOpen]);
|
}, [sidebarOpen, setSidebarOpen]);
|
||||||
|
|
||||||
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
|
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
|
||||||
|
const [history, setHistory] = useHistoryAtom();
|
||||||
|
const router = useMemo(() => {
|
||||||
|
return {
|
||||||
|
forward: () => {
|
||||||
|
setHistory(true);
|
||||||
|
},
|
||||||
|
back: () => {
|
||||||
|
setHistory(false);
|
||||||
|
},
|
||||||
|
history,
|
||||||
|
};
|
||||||
|
}, [history, setHistory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppSidebar>
|
<AppSidebar router={router}>
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<WorkspaceSelector
|
<WorkspaceSelector
|
||||||
currentWorkspace={currentWorkspace}
|
currentWorkspace={currentWorkspace}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type { FC, PropsWithChildren, ReactElement } from 'react';
|
|||||||
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
||||||
|
import { useTrackRouterHistoryEffect } from '../atoms/history';
|
||||||
import {
|
import {
|
||||||
publicWorkspaceAtom,
|
publicWorkspaceAtom,
|
||||||
publicWorkspaceIdAtom,
|
publicWorkspaceIdAtom,
|
||||||
@@ -166,6 +167,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
|||||||
// todo(himself65): this is a hack, we should use a better way to set the language
|
// todo(himself65): this is a hack, we should use a better way to set the language
|
||||||
setUpLanguage(i18n);
|
setUpLanguage(i18n);
|
||||||
}, [i18n]);
|
}, [i18n]);
|
||||||
|
useTrackRouterHistoryEffect();
|
||||||
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
|
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
|
||||||
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||||
const meta = useMemo(
|
const meta = useMemo(
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import {
|
|||||||
updateAvailableAtom,
|
updateAvailableAtom,
|
||||||
} from './index.jotai';
|
} from './index.jotai';
|
||||||
import { ResizeIndicator } from './resize-indicator';
|
import { ResizeIndicator } from './resize-indicator';
|
||||||
|
import type { SidebarHeaderProps } from './sidebar-header';
|
||||||
import { SidebarHeader } from './sidebar-header';
|
import { SidebarHeader } from './sidebar-header';
|
||||||
|
|
||||||
export type AppSidebarProps = PropsWithChildren;
|
export type AppSidebarProps = PropsWithChildren<SidebarHeaderProps>;
|
||||||
|
|
||||||
function useEnableAnimation() {
|
function useEnableAnimation() {
|
||||||
const [enable, setEnable] = useState(false);
|
const [enable, setEnable] = useState(false);
|
||||||
@@ -35,6 +36,11 @@ function useEnableAnimation() {
|
|||||||
return enable;
|
return enable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type History = {
|
||||||
|
stack: string[];
|
||||||
|
current: number;
|
||||||
|
};
|
||||||
|
|
||||||
export function AppSidebar(props: AppSidebarProps): ReactElement {
|
export function AppSidebar(props: AppSidebarProps): ReactElement {
|
||||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||||
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
|
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
|
||||||
@@ -91,7 +97,7 @@ 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 />
|
<SidebarHeader router={props.router} />
|
||||||
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,10 +7,19 @@ import {
|
|||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
import type { History } from '..';
|
||||||
import { navHeaderStyle, sidebarButtonStyle } from '../index.css';
|
import { navHeaderStyle, sidebarButtonStyle } from '../index.css';
|
||||||
import { appSidebarOpenAtom } from '../index.jotai';
|
import { appSidebarOpenAtom } from '../index.jotai';
|
||||||
|
|
||||||
export const SidebarHeader = () => {
|
export type SidebarHeaderProps = {
|
||||||
|
router?: {
|
||||||
|
back: () => unknown;
|
||||||
|
forward: () => unknown;
|
||||||
|
history: History;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarHeader = (props: SidebarHeaderProps) => {
|
||||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||||
const environment = getEnvironment();
|
const environment = getEnvironment();
|
||||||
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
||||||
@@ -24,16 +33,26 @@ export const SidebarHeader = () => {
|
|||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="middle"
|
size="middle"
|
||||||
|
data-testid="app-sidebar-arrow-button-back"
|
||||||
|
disabled={props.router?.history.current === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.history.back();
|
props.router?.back();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeftSmallIcon />
|
<ArrowLeftSmallIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="middle"
|
size="middle"
|
||||||
|
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
|
||||||
|
: false
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.history.forward();
|
props.router?.forward();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowRightSmallIcon />
|
<ArrowRightSmallIcon />
|
||||||
|
|||||||
Reference in New Issue
Block a user