feat(electron): track router history (#2336)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
Himself65
2023-05-14 23:13:30 -07:00
committed by GitHub
parent e5330b1917
commit 23b4f9ee12
7 changed files with 219 additions and 7 deletions

View File

@@ -1,3 +1,5 @@
import { platform } from 'node:os';
import { expect } from '@playwright/test';
import { test } from './fixture';
@@ -11,6 +13,73 @@ test('new page', async ({ page, workspace }) => {
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 }) => {
const root = page.locator('html');
{

View File

@@ -9,6 +9,7 @@ import {
enableCoverage,
istanbulTempDir,
test as base,
testResultDir,
} from '@affine-test/kit/playwright';
import fs from 'fs-extra';
import type { ElectronApplication, Page } from 'playwright';
@@ -90,6 +91,9 @@ export const test = base.extend<{
'.bin',
`electron${ext}`
),
recordVideo: {
dir: testResultDir,
},
colorScheme: 'light',
});
await use(electronApp);

View 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;
}

View File

@@ -23,8 +23,9 @@ import type { Page } from '@blocksuite/store';
import { useAtom, useAtomValue } from 'jotai';
import type { ReactElement } 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 FavoriteList from '../pure/workspace-slider-bar/favorite/favorite-list';
import { WorkspaceSelector } from '../pure/workspace-slider-bar/WorkspaceSelector';
@@ -115,10 +116,22 @@ export const RootAppSidebar = ({
}, [sidebarOpen, setSidebarOpen]);
const clientUpdateAvailable = useAtomValue(updateAvailableAtom);
const [history, setHistory] = useHistoryAtom();
const router = useMemo(() => {
return {
forward: () => {
setHistory(true);
},
back: () => {
setHistory(false);
},
history,
};
}, [history, setHistory]);
return (
<>
<AppSidebar>
<AppSidebar router={router}>
<SidebarContainer>
<WorkspaceSelector
currentWorkspace={currentWorkspace}

View File

@@ -27,6 +27,7 @@ import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import { useTrackRouterHistoryEffect } from '../atoms/history';
import {
publicWorkspaceAtom,
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
setUpLanguage(i18n);
}, [i18n]);
useTrackRouterHistoryEffect();
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const meta = useMemo(

View File

@@ -21,9 +21,10 @@ import {
updateAvailableAtom,
} from './index.jotai';
import { ResizeIndicator } from './resize-indicator';
import type { SidebarHeaderProps } from './sidebar-header';
import { SidebarHeader } from './sidebar-header';
export type AppSidebarProps = PropsWithChildren;
export type AppSidebarProps = PropsWithChildren<SidebarHeaderProps>;
function useEnableAnimation() {
const [enable, setEnable] = useState(false);
@@ -35,6 +36,11 @@ function useEnableAnimation() {
return enable;
}
export type History = {
stack: string[];
current: number;
};
export function AppSidebar(props: AppSidebarProps): ReactElement {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const appSidebarWidth = useAtomValue(appSidebarWidthAtom);
@@ -91,7 +97,7 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
data-enable-animation={enableAnimation && !isResizing}
>
<nav className={navStyle} ref={navRef} data-testid="app-sidebar">
<SidebarHeader />
<SidebarHeader router={props.router} />
<div className={navBodyStyle} data-testid="sliderBar-inner">
{props.children}
</div>

View File

@@ -7,10 +7,19 @@ import {
} from '@blocksuite/icons';
import { useAtom } from 'jotai';
import type { History } from '..';
import { navHeaderStyle, sidebarButtonStyle } from '../index.css';
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 environment = getEnvironment();
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
@@ -24,16 +33,26 @@ export const SidebarHeader = () => {
<>
<IconButton
size="middle"
data-testid="app-sidebar-arrow-button-back"
disabled={props.router?.history.current === 0}
onClick={() => {
window.history.back();
props.router?.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
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={() => {
window.history.forward();
props.router?.forward();
}}
>
<ArrowRightSmallIcon />