fix(core): avoid page full refresh (#3341)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -5,117 +5,13 @@ import { AffineContext } from '@affine/component/context';
|
|||||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||||
import { CacheProvider } from '@emotion/react';
|
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 type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { lazy, memo, Suspense, useEffect } 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';
|
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 i18n = createI18n();
|
||||||
const cache = createEmotionCache();
|
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() {
|
export const App = memo(function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.lang = i18n.language;
|
document.documentElement.lang = i18n.language;
|
||||||
@@ -144,9 +44,11 @@ export const App = memo(function App() {
|
|||||||
<CacheProvider value={cache}>
|
<CacheProvider value={cache}>
|
||||||
<AffineContext>
|
<AffineContext>
|
||||||
<DebugProvider>
|
<DebugProvider>
|
||||||
<Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}>
|
<RouterProvider
|
||||||
<RouterProvider router={router} />
|
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||||
</Suspense>
|
router={router}
|
||||||
|
future={future}
|
||||||
|
/>
|
||||||
</DebugProvider>
|
</DebugProvider>
|
||||||
</AffineContext>
|
</AffineContext>
|
||||||
</CacheProvider>
|
</CacheProvider>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { router } from '../router';
|
||||||
|
|
||||||
export type History = {
|
export type History = {
|
||||||
stack: string[];
|
stack: string[];
|
||||||
current: number;
|
current: number;
|
||||||
@@ -11,11 +13,62 @@ export type History = {
|
|||||||
|
|
||||||
export const MAX_HISTORY = 50;
|
export const MAX_HISTORY = 50;
|
||||||
|
|
||||||
export const historyBaseAtom = atomWithStorage<History>('router-history', {
|
const historyBaseAtom = atomWithStorage<History>(
|
||||||
stack: [],
|
'router-history',
|
||||||
current: 0,
|
{
|
||||||
skip: false,
|
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() {
|
export function useHistoryAtom() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
ToolContainer,
|
ToolContainer,
|
||||||
WorkspaceFallback,
|
WorkspaceFallback,
|
||||||
} from '@affine/component/workspace';
|
} from '@affine/component/workspace';
|
||||||
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import {
|
import {
|
||||||
rootBlockHubAtom,
|
rootBlockHubAtom,
|
||||||
@@ -30,14 +29,11 @@ import {
|
|||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||||
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
|
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
|
||||||
import {
|
import { currentWorkspaceIdAtom } from '@toeverything/plugin-infra/manager';
|
||||||
currentPageIdAtom,
|
|
||||||
currentWorkspaceIdAtom,
|
|
||||||
} from '@toeverything/plugin-infra/manager';
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||||
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
import { lazy, Suspense, useCallback, useMemo } from 'react';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||||
import {
|
import {
|
||||||
@@ -111,20 +107,6 @@ export const CurrentWorkspaceContext = ({
|
|||||||
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
|
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||||
const exist = metadata.find(m => m.id === workspaceId);
|
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) {
|
if (metadata.length === 0) {
|
||||||
return <WorkspaceFallback key="no-workspace" />;
|
return <WorkspaceFallback key="no-workspace" />;
|
||||||
}
|
}
|
||||||
@@ -171,24 +153,10 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
|||||||
|
|
||||||
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
|
const { openPage } = useNavigateHelper();
|
||||||
const { jumpToPage, openPage } = useNavigateHelper();
|
|
||||||
|
|
||||||
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
|
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 [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
|
||||||
const helper = useBlockSuiteWorkspaceHelper(
|
const helper = useBlockSuiteWorkspaceHelper(
|
||||||
currentWorkspace.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||||
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
|
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
|
||||||
import { useAtomValue } from 'jotai';
|
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||||
import { lazy, useEffect, useRef } from 'react';
|
import { lazy } from 'react';
|
||||||
|
import type { LoaderFunction } from 'react-router-dom';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { redirect } from 'react-router-dom';
|
||||||
import { useWorkspace } from '../hooks/use-workspace';
|
|
||||||
|
|
||||||
const AllWorkspaceModals = lazy(() =>
|
const AllWorkspaceModals = lazy(() =>
|
||||||
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
||||||
@@ -14,81 +12,35 @@ const AllWorkspaceModals = lazy(() =>
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
type WorkspaceLoaderProps = {
|
const logger = new DebugLogger('index-page');
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const WorkspaceLoader = (props: WorkspaceLoaderProps): null => {
|
export const loader: LoaderFunction = async () => {
|
||||||
useWorkspace(props.id);
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = new DebugLogger('index-page');
|
|
||||||
|
|
||||||
export const Component = () => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{meta.map(({ id }) => (
|
|
||||||
<WorkspaceLoader id={id} key={id} />
|
|
||||||
))}
|
|
||||||
<AllWorkspaceModals />
|
<AllWorkspaceModals />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
import { useCollectionManager } from '@affine/component/page-list';
|
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 { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
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 { getUIAdapter } from '../../adapters/workspace';
|
||||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
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 { jumpToPage } = useNavigateHelper();
|
||||||
|
const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const setting = useCollectionManager(currentWorkspace.id);
|
const setting = useCollectionManager(currentWorkspace.id);
|
||||||
const onClickPage = useCallback(
|
const onClickPage = useCallback(
|
||||||
@@ -23,6 +49,18 @@ const AllPage = () => {
|
|||||||
},
|
},
|
||||||
[currentWorkspace, jumpToPage]
|
[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);
|
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -42,9 +80,5 @@ const AllPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
return (
|
return <AllPage />;
|
||||||
<WorkspaceLayout>
|
|
||||||
<AllPage />
|
|
||||||
</WorkspaceLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,18 +7,21 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
|
|||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { Page } from '@blocksuite/store';
|
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 { useAtomValue } from 'jotai';
|
||||||
import { useAtom } from 'jotai/react';
|
import { useAtom } from 'jotai/react';
|
||||||
import { type ReactElement, useCallback, useEffect } from 'react';
|
import { type ReactElement, useCallback, useEffect } from 'react';
|
||||||
|
import type { LoaderFunction } from 'react-router-dom';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { getUIAdapter } from '../../adapters/workspace';
|
import { getUIAdapter } from '../../adapters/workspace';
|
||||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
|
||||||
|
|
||||||
const WorkspaceDetailPageImpl = (): ReactElement => {
|
const DetailPageImpl = (): ReactElement => {
|
||||||
const { openPage, jumpToSubPath } = useNavigateHelper();
|
const { openPage, jumpToSubPath } = useNavigateHelper();
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
@@ -68,7 +71,7 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WorkspaceDetailPage = (): ReactElement => {
|
export const DetailPage = (): ReactElement => {
|
||||||
const { workspaceId, pageId } = useParams();
|
const { workspaceId, pageId } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -92,6 +95,13 @@ const WorkspaceDetailPage = (): ReactElement => {
|
|||||||
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
navigate('/404');
|
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) {
|
if (!currentPageId || !page) {
|
||||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
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 = () => {
|
export const Component = () => {
|
||||||
return (
|
return <DetailPage />;
|
||||||
<WorkspaceLayout>
|
|
||||||
<WorkspaceDetailPage />
|
|
||||||
</WorkspaceLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
29
apps/core/src/pages/workspace/index.tsx
Normal file
29
apps/core/src/pages/workspace/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,9 +6,8 @@ import { getUIAdapter } from '../../adapters/workspace';
|
|||||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
|
||||||
|
|
||||||
const TrashPage = () => {
|
export const TrashPage = () => {
|
||||||
const { jumpToPage } = useNavigateHelper();
|
const { jumpToPage } = useNavigateHelper();
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const onClickPage = useCallback(
|
const onClickPage = useCallback(
|
||||||
@@ -44,9 +43,5 @@ const TrashPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
return (
|
return <TrashPage />;
|
||||||
<WorkspaceLayout>
|
|
||||||
<TrashPage />
|
|
||||||
</WorkspaceLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
37
apps/core/src/router.ts
Normal file
37
apps/core/src/router.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -52,12 +52,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setupListener(docId?: string) {
|
setupListener(docId?: string) {
|
||||||
logger.debug(
|
logger.debug('WorkspaceSQLiteDB:setupListener', this.workspaceId, docId);
|
||||||
'WorkspaceSQLiteDB:setupListener',
|
|
||||||
this.workspaceId,
|
|
||||||
docId,
|
|
||||||
this.getWorkspaceName()
|
|
||||||
);
|
|
||||||
const doc = this.getDoc(docId);
|
const doc = this.getDoc(docId);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
||||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
|
||||||
import type { BlockHub } from '@blocksuite/blocks';
|
import type { BlockHub } from '@blocksuite/blocks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { getOrCreateWorkspace } from './manager';
|
||||||
|
|
||||||
const rootWorkspaceMetadataV1Schema = z.object({
|
const rootWorkspaceMetadataV1Schema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
flavour: z.nativeEnum(WorkspaceFlavour),
|
flavour: z.nativeEnum(WorkspaceFlavour),
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { isBrowser, isDesktop } from '@affine/env/constant';
|
import { isBrowser, isDesktop } from '@affine/env/constant';
|
||||||
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
|
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
|
||||||
createAffineProviders,
|
|
||||||
createLocalProviders,
|
|
||||||
} from '@affine/workspace/providers';
|
|
||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||||
import type { DocProviderCreator, StoreOptions } from '@blocksuite/store';
|
import type { DocProviderCreator, StoreOptions } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +14,7 @@ import type { Transaction } from 'yjs';
|
|||||||
|
|
||||||
import { createStaticStorage } from '../blob/local-static-storage';
|
import { createStaticStorage } from '../blob/local-static-storage';
|
||||||
import { createSQLiteStorage } from '../blob/sqlite-blob-storage';
|
import { createSQLiteStorage } from '../blob/sqlite-blob-storage';
|
||||||
|
import { createAffineProviders, createLocalProviders } from '../providers';
|
||||||
|
|
||||||
function setEditorFlags(workspace: Workspace) {
|
function setEditorFlags(workspace: Workspace) {
|
||||||
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
|
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,
|
SQLiteDBDownloadProvider,
|
||||||
SQLiteProvider,
|
SQLiteProvider,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
|
import { getDoc } from '@affine/y-provider';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { DocProviderCreator } from '@blocksuite/store';
|
import type { DocProviderCreator } from '@blocksuite/store';
|
||||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
@@ -19,6 +20,28 @@ type SubDocsEvent = {
|
|||||||
loaded: Set<Doc>;
|
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.
|
* 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) {
|
function trackDoc(doc: Doc) {
|
||||||
|
syncDiff(rootDoc, rootDoc !== doc ? doc.guid : undefined).catch(
|
||||||
|
logger.error
|
||||||
|
);
|
||||||
doc.on('update', createOrHandleUpdate(doc));
|
doc.on('update', createOrHandleUpdate(doc));
|
||||||
doc.on('subdocs', createOrGetHandleSubDocs(doc));
|
doc.on('subdocs', createOrGetHandleSubDocs(doc));
|
||||||
doc.subdocs.forEach(doc => {
|
doc.subdocs.forEach(doc => {
|
||||||
@@ -93,6 +119,9 @@ export const createSQLiteProvider: DocProviderCreator = (
|
|||||||
let connected = false;
|
let connected = false;
|
||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
|
if (connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
logger.info('connecting sqlite provider', id);
|
logger.info('connecting sqlite provider', id);
|
||||||
trackDoc(rootDoc);
|
trackDoc(rootDoc);
|
||||||
|
|
||||||
@@ -161,7 +190,7 @@ export const createSQLiteDBDownloadProvider: DocProviderCreator = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function syncUpdates(doc: Doc) {
|
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 subdocId = doc.guid === id ? undefined : doc.guid;
|
||||||
const updates = await apis.db.getDocAsUpdates(id, subdocId);
|
const updates = await apis.db.getDocAsUpdates(id, subdocId);
|
||||||
|
|
||||||
@@ -173,7 +202,10 @@ export const createSQLiteDBDownloadProvider: DocProviderCreator = (
|
|||||||
Y.applyUpdate(doc, updates, sqliteOrigin);
|
Y.applyUpdate(doc, updates, sqliteOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedUpdates = Y.encodeStateAsUpdate(doc);
|
const mergedUpdates = Y.encodeStateAsUpdate(
|
||||||
|
doc,
|
||||||
|
Y.encodeStateVectorFromUpdate(updates)
|
||||||
|
);
|
||||||
|
|
||||||
// also apply updates to sqlite
|
// also apply updates to sqlite
|
||||||
await apis.db.applyDocUpdate(id, mergedUpdates, subdocId);
|
await apis.db.applyDocUpdate(id, mergedUpdates, subdocId);
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './lazy-provider';
|
export * from './lazy-provider';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const createAndPinCollection = async (
|
|||||||
await page.getByTestId('collection-bar-option-pin').click();
|
await page.getByTestId('collection-bar-option-pin').click();
|
||||||
await page.waitForTimeout(100);
|
await page.waitForTimeout(100);
|
||||||
};
|
};
|
||||||
|
|
||||||
test('Show collections items in sidebar', async ({ page }) => {
|
test('Show collections items in sidebar', async ({ page }) => {
|
||||||
await createAndPinCollection(page);
|
await createAndPinCollection(page);
|
||||||
const collections = page.getByTestId('collections');
|
const collections = page.getByTestId('collections');
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ test('goto not found page', async ({ page }) => {
|
|||||||
test('goto not found workspace', async ({ page }) => {
|
test('goto not found workspace', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await waitEditorLoad(page);
|
await waitEditorLoad(page);
|
||||||
const currentUrl = page.url();
|
|
||||||
// if doesn't wait for timeout, data won't be saved into indexedDB
|
// if doesn't wait for timeout, data won't be saved into indexedDB
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.goto(new URL('/workspace/invalid/all', webUrl).toString());
|
await page.goto(new URL('/workspace/invalid/all', webUrl).toString());
|
||||||
await waitEditorLoad(page);
|
await page.waitForTimeout(1000);
|
||||||
expect(page.url()).toEqual(currentUrl);
|
expect(page.url()).toBe(new URL('/404', webUrl).toString());
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user