fix(core): avoid page full refresh (#3341)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
Alex Yang
2023-07-24 02:02:35 -07:00
committed by GitHub
parent ccb0df10e4
commit e6e98975ed
18 changed files with 276 additions and 433 deletions

View File

@@ -5,117 +5,13 @@ import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import type { RouterState } from '@remix-run/router';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/manager';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense, useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { RouterProvider } from 'react-router-dom';
import { historyBaseAtom, MAX_HISTORY } from './atoms/history';
import { router } from './router';
import createEmotionCache from './utils/create-emotion-cache';
const router = createBrowserRouter([
{
path: '/',
lazy: () => import('./pages/index'),
},
{
path: '/404',
lazy: () => import('./pages/404'),
},
{
path: '/workspace/:workspaceId/all',
lazy: () => import('./pages/workspace/all-page'),
},
{
path: '/workspace/:workspaceId/trash',
lazy: () => import('./pages/workspace/trash-page'),
},
{
path: '/workspace/:workspaceId/:pageId',
lazy: () => import('./pages/workspace/detail-page'),
},
]);
//#region atoms bootstrap
currentWorkspaceIdAtom.onMount = set => {
const callback = (state: RouterState) => {
const value = state.location.pathname.split('/')[2];
if (value) {
set(value);
localStorage.setItem('last_workspace_id', value);
}
};
callback(router.state);
const unsubscribe = router.subscribe(callback);
return () => {
unsubscribe();
};
};
currentPageIdAtom.onMount = set => {
const callback = (state: RouterState) => {
const value = state.location.pathname.split('/')[3];
if (value) {
set(value);
}
};
callback(router.state);
const unsubscribe = router.subscribe(callback);
return () => {
unsubscribe();
};
};
historyBaseAtom.onMount = set => {
const unsubscribe = router.subscribe(state => {
set(prev => {
const url = state.location.pathname;
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,
};
}
}
});
});
return () => {
unsubscribe();
};
};
//#endregion
const i18n = createI18n();
const cache = createEmotionCache();
@@ -132,6 +28,10 @@ const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
);
};
const future = {
v7_startTransition: true,
} as const;
export const App = memo(function App() {
useEffect(() => {
document.documentElement.lang = i18n.language;
@@ -144,9 +44,11 @@ export const App = memo(function App() {
<CacheProvider value={cache}>
<AffineContext>
<DebugProvider>
<Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}>
<RouterProvider router={router} />
</Suspense>
<RouterProvider
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
router={router}
future={future}
/>
</DebugProvider>
</AffineContext>
</CacheProvider>

View File

@@ -1,8 +1,10 @@
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { router } from '../router';
export type History = {
stack: string[];
current: number;
@@ -11,11 +13,62 @@ export type History = {
export const MAX_HISTORY = 50;
export const historyBaseAtom = atomWithStorage<History>('router-history', {
stack: [],
current: 0,
skip: false,
});
const historyBaseAtom = atomWithStorage<History>(
'router-history',
{
stack: [],
current: 0,
skip: false,
},
createJSONStorage(() => sessionStorage)
);
historyBaseAtom.onMount = set => {
const unsubscribe = router.subscribe(state => {
set(prev => {
const url = state.location.pathname;
// if stack top is the same as current, skip
if (prev.stack[prev.current] === url) {
return prev;
}
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,
};
}
}
});
});
return () => {
unsubscribe();
};
};
export function useHistoryAtom() {
const navigate = useNavigate();

View File

@@ -10,7 +10,6 @@ import {
ToolContainer,
WorkspaceFallback,
} from '@affine/component/workspace';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootBlockHubAtom,
@@ -30,14 +29,11 @@ import {
} from '@dnd-kit/core';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/manager';
import { currentWorkspaceIdAtom } from '@toeverything/plugin-infra/manager';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { lazy, Suspense, useCallback, useMemo } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { WorkspaceAdapters } from '../adapters/workspace';
import {
@@ -111,20 +107,6 @@ export const CurrentWorkspaceContext = ({
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
const navigate = useNavigate();
// fixme(himself65): this is not a good way to handle this,
// need a better way to check whether this workspace really exist.
useEffect(() => {
const id = setTimeout(() => {
if (!exist) {
navigate('/');
globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
}
}, globalThis.HALTING_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [exist, metadata.length, navigate]);
if (metadata.length === 0) {
return <WorkspaceFallback key="no-workspace" />;
}
@@ -171,24 +153,10 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const { jumpToPage, openPage } = useNavigateHelper();
const { openPage } = useNavigateHelper();
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
useEffect(() => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
setCurrentPageId(currentPageId);
jumpToPage(currentWorkspace.id, page.id);
}
}, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace

View File

@@ -1,12 +1,10 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
import { useAtomValue } from 'jotai';
import { lazy, useEffect, useRef } from 'react';
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { useWorkspace } from '../hooks/use-workspace';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { lazy } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
const AllWorkspaceModals = lazy(() =>
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
@@ -14,81 +12,35 @@ const AllWorkspaceModals = lazy(() =>
}))
);
type WorkspaceLoaderProps = {
id: string;
};
const logger = new DebugLogger('index-page');
const WorkspaceLoader = (props: WorkspaceLoaderProps): null => {
useWorkspace(props.id);
export const loader: LoaderFunction = async () => {
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id');
const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
if (target) {
const targetWorkspace = getWorkspace(target.id);
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
({ trash }) => !trash
);
const pageId =
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
nonTrashPages.at(0)?.id;
if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
} else {
logger.debug('Found target workspace. Jump to all page');
return redirect(`/workspace/${targetWorkspace.id}/all`);
}
}
return null;
};
const logger = new DebugLogger('index-page');
export const Component = () => {
const meta = useAtomValue(rootWorkspacesMetadataAtom);
const navigateHelper = useNavigateHelper();
const jumpOnceRef = useRef(false);
useEffect(() => {
if (jumpOnceRef.current) {
return;
}
const lastId = localStorage.getItem('last_workspace_id');
const lastPageId = localStorage.getItem('last_page_id');
const target =
(lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
if (target) {
const targetWorkspace = getWorkspace(target.id);
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
({ trash }) => !trash
);
const pageId =
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
nonTrashPages.at(0)?.id;
if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
navigateHelper.jumpToPage(
targetWorkspace.id,
pageId,
RouteLogic.REPLACE
);
jumpOnceRef.current = true;
} else {
const clearId = setTimeout(() => {
dispose.dispose();
logger.debug('Found target workspace. Jump to all pages');
navigateHelper.jumpToSubPath(
targetWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
jumpOnceRef.current = true;
}, 1000);
const dispose = targetWorkspace.slots.pageAdded.once(pageId => {
clearTimeout(clearId);
navigateHelper.jumpToPage(
targetWorkspace.id,
pageId,
RouteLogic.REPLACE
);
jumpOnceRef.current = true;
});
return () => {
clearTimeout(clearId);
dispose.dispose();
jumpOnceRef.current = false;
};
}
} else {
console.warn('No workspace found');
}
return;
}, [meta, navigateHelper]);
return (
<>
{meta.map(({ id }) => (
<WorkspaceLoader id={id} key={id} />
))}
<AllWorkspaceModals />
</>
);

View File

@@ -1,15 +1,41 @@
import { useCollectionManager } from '@affine/component/page-list';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { useCallback } from 'react';
import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/plugin-infra/__internal__/workspace';
import {
currentPageIdAtom,
rootStore,
} from '@toeverything/plugin-infra/manager';
import { useAtom } from 'jotai/react';
import { useCallback, useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
const AllPage = () => {
export const loader: LoaderFunction = async args => {
const workspaceId = args.params.workspaceId;
assertExists(workspaceId);
const workspaceAtom = getActiveBlockSuiteWorkspaceAtom(workspaceId);
const workspace = await rootStore.get(workspaceAtom);
const page = workspace.getPage(
`${workspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
workspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
return redirect(`/workspace/${workspace.id}/${page.id}`);
}
return null;
};
export const AllPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
const setting = useCollectionManager(currentWorkspace.id);
const onClickPage = useCallback(
@@ -23,6 +49,18 @@ const AllPage = () => {
},
[currentWorkspace, jumpToPage]
);
useEffect(() => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(
`${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
jumpOnce: false,
});
setCurrentPageId(currentPageId);
jumpToPage(currentWorkspace.id, page.id);
}
}, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
return (
<>
@@ -42,9 +80,5 @@ const AllPage = () => {
};
export const Component = () => {
return (
<WorkspaceLayout>
<AllPage />
</WorkspaceLayout>
);
return <AllPage />;
};

View File

@@ -7,18 +7,21 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import {
currentPageIdAtom,
rootStore,
} from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai';
import { useAtom } from 'jotai/react';
import { type ReactElement, useCallback, useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
const WorkspaceDetailPageImpl = (): ReactElement => {
const DetailPageImpl = (): ReactElement => {
const { openPage, jumpToSubPath } = useNavigateHelper();
const currentPageId = useAtomValue(currentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
@@ -68,7 +71,7 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
);
};
const WorkspaceDetailPage = (): ReactElement => {
export const DetailPage = (): ReactElement => {
const { workspaceId, pageId } = useParams();
const location = useLocation();
const navigate = useNavigate();
@@ -92,6 +95,13 @@ const WorkspaceDetailPage = (): ReactElement => {
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
navigate('/404');
} else {
// fixme: cleanup jumpOnce in the right time
if (page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.setPageMeta(currentPageId, {
jumpOnce: false,
});
}
}
}
}
@@ -110,13 +120,17 @@ const WorkspaceDetailPage = (): ReactElement => {
if (!currentPageId || !page) {
return <PageDetailSkeleton key="current-page-is-null" />;
}
return <WorkspaceDetailPageImpl />;
return <DetailPageImpl />;
};
export const loader: LoaderFunction = args => {
if (args.params.pageId) {
localStorage.setItem('last_page_id', args.params.pageId);
rootStore.set(currentPageIdAtom, args.params.pageId);
}
return null;
};
export const Component = () => {
return (
<WorkspaceLayout>
<WorkspaceDetailPage />
</WorkspaceLayout>
);
return <DetailPage />;
};

View File

@@ -0,0 +1,29 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentWorkspaceIdAtom,
rootStore,
} from '@toeverything/plugin-infra/manager';
import type { ReactElement } from 'react';
import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
export const loader: LoaderFunction = async args => {
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
if (!meta.some(({ id }) => id === args.params.workspaceId)) {
return redirect('/404');
}
if (args.params.workspaceId) {
localStorage.setItem('last_workspace_id', args.params.workspaceId);
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
}
return null;
};
export const Component = (): ReactElement => {
return (
<WorkspaceLayout>
<Outlet />
</WorkspaceLayout>
);
};

View File

@@ -6,9 +6,8 @@ import { getUIAdapter } from '../../adapters/workspace';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { WorkspaceLayout } from '../../layouts/workspace-layout';
const TrashPage = () => {
export const TrashPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const onClickPage = useCallback(
@@ -44,9 +43,5 @@ const TrashPage = () => {
};
export const Component = () => {
return (
<WorkspaceLayout>
<TrashPage />
</WorkspaceLayout>
);
return <TrashPage />;
};

37
apps/core/src/router.ts Normal file
View File

@@ -0,0 +1,37 @@
import { createBrowserRouter } from 'react-router-dom';
export const router = createBrowserRouter(
[
{
path: '/',
lazy: () => import('./pages/index'),
},
{
path: '/workspace/:workspaceId',
lazy: () => import('./pages/workspace/index'),
children: [
{
path: 'all',
lazy: () => import('./pages/workspace/all-page'),
},
{
path: 'trash',
lazy: () => import('./pages/workspace/trash-page'),
},
{
path: ':pageId',
lazy: () => import('./pages/workspace/detail-page'),
},
],
},
{
path: '/404',
lazy: () => import('./pages/404'),
},
],
{
future: {
v7_normalizeFormMethod: true,
},
}
);