mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 17:46:18 +08:00
fix(core): avoid page full refresh (#3341)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
+11
-109
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -52,12 +52,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
};
|
||||
|
||||
setupListener(docId?: string) {
|
||||
logger.debug(
|
||||
'WorkspaceSQLiteDB:setupListener',
|
||||
this.workspaceId,
|
||||
docId,
|
||||
this.getWorkspaceName()
|
||||
);
|
||||
logger.debug('WorkspaceSQLiteDB:setupListener', this.workspaceId, docId);
|
||||
const doc = this.getDoc(docId);
|
||||
if (doc) {
|
||||
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { BlockHub } from '@blocksuite/blocks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { atom } from 'jotai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getOrCreateWorkspace } from './manager';
|
||||
|
||||
const rootWorkspaceMetadataV1Schema = z.object({
|
||||
id: z.string(),
|
||||
flavour: z.nativeEnum(WorkspaceFlavour),
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { isBrowser, isDesktop } from '@affine/env/constant';
|
||||
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
createAffineProviders,
|
||||
createLocalProviders,
|
||||
} from '@affine/workspace/providers';
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { DocProviderCreator, StoreOptions } from '@blocksuite/store';
|
||||
import {
|
||||
@@ -18,6 +14,7 @@ import type { Transaction } from 'yjs';
|
||||
|
||||
import { createStaticStorage } from '../blob/local-static-storage';
|
||||
import { createSQLiteStorage } from '../blob/sqlite-blob-storage';
|
||||
import { createAffineProviders, createLocalProviders } from '../providers';
|
||||
|
||||
function setEditorFlags(workspace: Workspace) {
|
||||
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export interface DatasourceDocAdapter {
|
||||
// request diff update from other clients
|
||||
queryDocState: (
|
||||
guid: string,
|
||||
options?: {
|
||||
stateVector?: Uint8Array;
|
||||
targetClientId?: number;
|
||||
}
|
||||
) => Promise<Uint8Array | false>;
|
||||
|
||||
// send update to the datasource
|
||||
sendDocUpdate: (guid: string, update: Uint8Array) => Promise<void>;
|
||||
|
||||
// listen to update from the datasource. Returns a function to unsubscribe.
|
||||
// this is optional because some datasource might not support it
|
||||
onDocUpdate?(
|
||||
callback: (guid: string, update: Uint8Array) => void
|
||||
): () => void;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import type { PassiveDocProvider } from '@blocksuite/store';
|
||||
import {
|
||||
applyUpdate,
|
||||
type Doc,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateVectorFromUpdate,
|
||||
} from 'yjs';
|
||||
|
||||
import type { DatasourceDocAdapter } from './datasource-doc-adapter';
|
||||
|
||||
const selfUpdateOrigin = 'lazy-provider-self-origin';
|
||||
|
||||
function getDoc(doc: Doc, guid: string): Doc | undefined {
|
||||
if (doc.guid === guid) {
|
||||
return doc;
|
||||
}
|
||||
for (const subdoc of doc.subdocs) {
|
||||
const found = getDoc(subdoc, guid);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy provider that connects to a datasource and synchronizes a root document.
|
||||
*/
|
||||
export const createLazyProvider = (
|
||||
rootDoc: Doc,
|
||||
datasource: DatasourceDocAdapter
|
||||
): Omit<PassiveDocProvider, 'flavour'> => {
|
||||
let connected = false;
|
||||
const pendingMap = new Map<string, Uint8Array[]>(); // guid -> pending-updates
|
||||
const disposableMap = new Map<string, Set<() => void>>();
|
||||
let datasourceUnsub: (() => void) | undefined;
|
||||
|
||||
async function syncDoc(doc: Doc) {
|
||||
const guid = doc.guid;
|
||||
// perf: optimize me
|
||||
const currentUpdate = encodeStateAsUpdate(doc);
|
||||
|
||||
const remoteUpdate = await datasource.queryDocState(guid, {
|
||||
stateVector: encodeStateVectorFromUpdate(currentUpdate),
|
||||
});
|
||||
|
||||
const updates = [currentUpdate];
|
||||
pendingMap.set(guid, []);
|
||||
|
||||
if (remoteUpdate) {
|
||||
applyUpdate(doc, remoteUpdate, selfUpdateOrigin);
|
||||
const newUpdate = encodeStateAsUpdate(
|
||||
doc,
|
||||
encodeStateVectorFromUpdate(remoteUpdate)
|
||||
);
|
||||
updates.push(newUpdate);
|
||||
await datasource.sendDocUpdate(guid, newUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
function setupDocListener(doc: Doc) {
|
||||
const disposables = new Set<() => void>();
|
||||
disposableMap.set(doc.guid, disposables);
|
||||
const updateHandler = async (update: Uint8Array, origin: unknown) => {
|
||||
if (origin === selfUpdateOrigin) {
|
||||
return;
|
||||
}
|
||||
datasource.sendDocUpdate(doc.guid, update).catch(console.error);
|
||||
};
|
||||
|
||||
const subdocLoadHandler = (event: { loaded: Set<Doc> }) => {
|
||||
event.loaded.forEach(subdoc => {
|
||||
connectDoc(subdoc).catch(console.error);
|
||||
});
|
||||
};
|
||||
|
||||
doc.on('update', updateHandler);
|
||||
doc.on('subdocs', subdocLoadHandler);
|
||||
// todo: handle destroy?
|
||||
disposables.add(() => {
|
||||
doc.off('update', updateHandler);
|
||||
doc.off('subdocs', subdocLoadHandler);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDatasourceListeners() {
|
||||
datasourceUnsub = datasource.onDocUpdate?.((guid, update) => {
|
||||
const doc = getDoc(rootDoc, guid);
|
||||
if (doc) {
|
||||
applyUpdate(doc, update);
|
||||
//
|
||||
if (pendingMap.has(guid)) {
|
||||
pendingMap.get(guid)?.forEach(update => applyUpdate(doc, update));
|
||||
pendingMap.delete(guid);
|
||||
}
|
||||
} else {
|
||||
// This case happens when the father doc is not yet updated,
|
||||
// so that the child doc is not yet created.
|
||||
// We need to put it into cache so that it can be applied later.
|
||||
console.warn('idb: doc not found', guid);
|
||||
pendingMap.set(guid, (pendingMap.get(guid) ?? []).concat(update));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// when a subdoc is loaded, we need to sync it with the datasource and setup listeners
|
||||
async function connectDoc(doc: Doc) {
|
||||
setupDocListener(doc);
|
||||
await syncDoc(doc);
|
||||
await Promise.all(
|
||||
[...doc.subdocs]
|
||||
.filter(subdoc => subdoc.shouldLoad)
|
||||
.map(subdoc => connectDoc(subdoc))
|
||||
);
|
||||
}
|
||||
|
||||
function disposeAll() {
|
||||
disposableMap.forEach(disposables => {
|
||||
disposables.forEach(dispose => dispose());
|
||||
});
|
||||
disposableMap.clear();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
connected = true;
|
||||
|
||||
// root doc should be already loaded,
|
||||
// but we want to populate the cache for later update events
|
||||
connectDoc(rootDoc).catch(console.error);
|
||||
setupDatasourceListeners();
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
connected = false;
|
||||
disposeAll();
|
||||
datasourceUnsub?.();
|
||||
datasourceUnsub = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
get connected() {
|
||||
return connected;
|
||||
},
|
||||
passive: true,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
SQLiteDBDownloadProvider,
|
||||
SQLiteProvider,
|
||||
} from '@affine/env/workspace';
|
||||
import { getDoc } from '@affine/y-provider';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { DocProviderCreator } from '@blocksuite/store';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
@@ -19,6 +20,28 @@ type SubDocsEvent = {
|
||||
loaded: Set<Doc>;
|
||||
};
|
||||
|
||||
// workaround: there maybe new updates before SQLite is connected
|
||||
// we need to exchange them with the SQLite db
|
||||
// will be removed later when we have lazy load doc provider
|
||||
const syncDiff = async (rootDoc: Doc, subdocId?: string) => {
|
||||
try {
|
||||
const workspaceId = rootDoc.guid;
|
||||
const doc = subdocId ? getDoc(rootDoc, subdocId) : rootDoc;
|
||||
if (!doc) {
|
||||
logger.error('doc not found', workspaceId, subdocId);
|
||||
return;
|
||||
}
|
||||
const update = await window.apis?.db.getDocAsUpdates(workspaceId, subdocId);
|
||||
const diff = Y.encodeStateAsUpdate(
|
||||
doc,
|
||||
Y.encodeStateVectorFromUpdate(update)
|
||||
);
|
||||
await window.apis.db.applyDocUpdate(workspaceId, diff, subdocId);
|
||||
} catch (err) {
|
||||
logger.error('failed to sync diff', err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A provider that is responsible for syncing updates the workspace with the local SQLite database.
|
||||
*/
|
||||
@@ -74,6 +97,9 @@ export const createSQLiteProvider: DocProviderCreator = (
|
||||
};
|
||||
|
||||
function trackDoc(doc: Doc) {
|
||||
syncDiff(rootDoc, rootDoc !== doc ? doc.guid : undefined).catch(
|
||||
logger.error
|
||||
);
|
||||
doc.on('update', createOrHandleUpdate(doc));
|
||||
doc.on('subdocs', createOrGetHandleSubDocs(doc));
|
||||
doc.subdocs.forEach(doc => {
|
||||
@@ -93,6 +119,9 @@ export const createSQLiteProvider: DocProviderCreator = (
|
||||
let connected = false;
|
||||
|
||||
const connect = () => {
|
||||
if (connected) {
|
||||
return;
|
||||
}
|
||||
logger.info('connecting sqlite provider', id);
|
||||
trackDoc(rootDoc);
|
||||
|
||||
@@ -161,7 +190,7 @@ export const createSQLiteDBDownloadProvider: DocProviderCreator = (
|
||||
});
|
||||
|
||||
async function syncUpdates(doc: Doc) {
|
||||
logger.info('syncing updates from sqlite', id);
|
||||
logger.info('syncing updates from sqlite', doc.guid);
|
||||
const subdocId = doc.guid === id ? undefined : doc.guid;
|
||||
const updates = await apis.db.getDocAsUpdates(id, subdocId);
|
||||
|
||||
@@ -173,7 +202,10 @@ export const createSQLiteDBDownloadProvider: DocProviderCreator = (
|
||||
Y.applyUpdate(doc, updates, sqliteOrigin);
|
||||
}
|
||||
|
||||
const mergedUpdates = Y.encodeStateAsUpdate(doc);
|
||||
const mergedUpdates = Y.encodeStateAsUpdate(
|
||||
doc,
|
||||
Y.encodeStateVectorFromUpdate(updates)
|
||||
);
|
||||
|
||||
// also apply updates to sqlite
|
||||
await apis.db.applyDocUpdate(id, mergedUpdates, subdocId);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './lazy-provider';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
@@ -42,6 +42,7 @@ const createAndPinCollection = async (
|
||||
await page.getByTestId('collection-bar-option-pin').click();
|
||||
await page.waitForTimeout(100);
|
||||
};
|
||||
|
||||
test('Show collections items in sidebar', async ({ page }) => {
|
||||
await createAndPinCollection(page);
|
||||
const collections = page.getByTestId('collections');
|
||||
|
||||
@@ -15,10 +15,9 @@ test('goto not found page', async ({ page }) => {
|
||||
test('goto not found workspace', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitEditorLoad(page);
|
||||
const currentUrl = page.url();
|
||||
// if doesn't wait for timeout, data won't be saved into indexedDB
|
||||
await page.waitForTimeout(1000);
|
||||
await page.goto(new URL('/workspace/invalid/all', webUrl).toString());
|
||||
await waitEditorLoad(page);
|
||||
expect(page.url()).toEqual(currentUrl);
|
||||
await page.waitForTimeout(1000);
|
||||
expect(page.url()).toBe(new URL('/404', webUrl).toString());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user