mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00: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 { 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');
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
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 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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user