refactor: workspace loading logic (#1966)

This commit is contained in:
Himself65
2023-04-16 16:02:41 -05:00
committed by GitHub
parent caa292e097
commit 7bbe67af43
88 changed files with 2684 additions and 2268 deletions

View File

@@ -78,6 +78,7 @@ const nextConfig = {
},
reactStrictMode: true,
transpilePackages: [
'jotai-devtools',
'@affine/component',
'@affine/i18n',
'@affine/debug',

View File

@@ -36,6 +36,7 @@
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
"jotai": "^2.0.4",
"jotai-devtools": "^0.4.0",
"lit": "^2.7.2",
"lottie-web": "^5.11.0",
"next-themes": "^0.2.1",

View File

@@ -1,40 +1,68 @@
import { DebugLogger } from '@affine/debug';
import { atomWithSyncStorage } from '@affine/jotai';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
rootCurrentEditorAtom,
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null);
/**
* @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
*/
export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
} satisfies RootWorkspaceMetadata)
);
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
}
setAtom(metadata => {
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
};
/**
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export const currentPageIdAtom = rootCurrentPageIdAtom;
/**
* @deprecated Use `rootCurrentEditorAtom` directly instead.
*/
export const currentEditorAtom = rootCurrentEditorAtom;
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom(false);
export const openQuickSearchModalAtom = atom(false);
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(jotaiWorkspacesAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
export { workspacesAtom } from './root';
type View = { id: string; mode: 'page' | 'edgeless' };

View File

@@ -0,0 +1,78 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
/**
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
logger.info('workspaces', workspaces);
workspaces.forEach(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
});
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
/**
* This will throw an error if the workspace is not found,
* should not be used on the root component,
* use `rootCurrentWorkspaceIdAtom` instead
*/
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async get => {
const metadata = get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {
throw new Error(
'current workspace id is null. this should not happen. If you see this error, please report it to the developer.'
);
}
const targetWorkspace = metadata.find(meta => meta.id === targetId);
if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
const workspace = await WorkspacePlugins[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
return workspace;
}
);
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
//#endregion

View File

@@ -3,6 +3,7 @@
*/
import 'fake-indexeddb/auto';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store';
import matchers from '@testing-library/jest-dom/matchers';
import type { RenderResult } from '@testing-library/react';
@@ -12,11 +13,9 @@ import type { FC, PropsWithChildren } from 'react';
import { beforeEach, describe, expect, test } from 'vitest';
import { workspacesAtom } from '../../atoms';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider';
import type { BlockSuiteWorkspace } from '../../shared';
import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard';
@@ -42,24 +41,26 @@ const initPinBoard = async () => {
// - pinboard2
// - noPinboardPage
const mutationHook = renderHook(() => useWorkspacesHelper(), {
const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const rootPageIds = ['hasPinboardPage', 'noPinboardPage'];
const pinboardPageIds = ['pinboard1', 'pinboard2'];
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
await store.get(currentWorkspaceAtom);
store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(workspacesAtom);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
currentWorkspaceHook.result.current[1](id);
const currentWorkspace = await store.get(currentWorkspaceAtom);
const currentWorkspace = await store.get(rootCurrentWorkspaceAtom);
const blockSuiteWorkspace =
currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace;
mutationHook.rerender();
// create root pinboard
mutationHook.result.current.createWorkspacePage(id, 'rootPinboard');
blockSuiteWorkspace.meta.setPageMeta('rootPinboard', {
@@ -73,7 +74,7 @@ const initPinBoard = async () => {
subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [],
});
});
// create children to firs parent
// create children to first parent
pinboardPageIds.forEach(pinboardId => {
mutationHook.result.current.createWorkspacePage(id, pinboardId);
blockSuiteWorkspace.meta.setPageMeta(pinboardId, {

View File

@@ -3,23 +3,27 @@
*/
import 'fake-indexeddb/auto';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { render, renderHook } from '@testing-library/react';
import { createStore, getDefaultStore, Provider } from 'jotai';
import { createStore, getDefaultStore, Provider, useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback } from 'react';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { workspacesAtom } from '../../atoms';
import { useCurrentPageId } from '../../hooks/current/use-current-page-id';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider';
import { pathGenerator } from '../../shared';
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
@@ -45,21 +49,22 @@ describe('WorkSpaceSliderBar', () => {
const onOpenWorkspaceListModalFn = vi.fn();
const onOpenQuickSearchModalFn = vi.fn();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'test1');
await store.get(currentWorkspaceAtom);
store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
let i = 0;
const Component = () => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId();
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
assertExists(currentWorkspace);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace

View File

@@ -1,3 +1,4 @@
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import type { PageBlockModel } from '@blocksuite/blocks';
import { PlusIcon } from '@blocksuite/icons';
@@ -5,6 +6,7 @@ import { assertEquals, nanoid } from '@blocksuite/store';
import { Command } from 'cmdk';
import type { NextRouter } from 'next/router';
import type React from 'react';
import { useCallback } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
import { useRouterHelper } from '../../../hooks/use-router-helper';
@@ -35,25 +37,25 @@ export const Footer: React.FC<FooterProps> = ({
return (
<Command.Item
data-testid="quick-search-add-new-page"
onSelect={async () => {
onClose();
onSelect={useCallback(() => {
const id = nanoid();
const page = await createPage(id);
const page = createPage(id);
assertEquals(page.id, id);
await jumpToPage(blockSuiteWorkspace.id, page.id);
if (!query) {
return;
initPage(page);
const block = page.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
} else {
console.warn('No page block found');
}
const newPage = blockSuiteWorkspace.getPage(page.id);
if (newPage) {
const block = newPage.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
}
}
}}
blockSuiteWorkspace.setPageMeta(page.id, {
title: query,
});
onClose();
void jumpToPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
>
<StyledModalFooterContent>
<PlusIcon />

View File

@@ -5,7 +5,10 @@ import 'fake-indexeddb/auto';
import assert from 'node:assert';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { PageBlockModel } from '@blocksuite/blocks';
@@ -38,11 +41,7 @@ import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
} from '../use-recent-views';
import {
REDIRECT_TIMEOUT,
useSyncRouterWithCurrentWorkspaceAndPage,
} from '../use-sync-router-with-current-workspace-and-page';
import { useWorkspaces, useWorkspacesHelper } from '../use-workspaces';
import { useAppHelper, useWorkspaces } from '../use-workspaces';
vi.mock(
'../../components/blocksuite/header/editor-mode-switch/CustomLottie',
@@ -167,23 +166,24 @@ describe('usePageMetas', async () => {
describe('useWorkspacesHelper', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), {
const workspaceHelperHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const id = await workspaceHelperHook.result.current.createLocalWorkspace(
'test'
);
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(1);
expect(workspaces[0].id).toBe(id);
expect(workspaces.length).toBe(2);
expect(workspaces[1].id).toBe(id);
const workspacesHook = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper,
});
store.set(rootCurrentWorkspaceIdAtom, workspacesHook.result.current[1].id);
await store.get(currentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id);
currentWorkspaceHook.result.current[1](workspacesHook.result.current[1].id);
});
});
@@ -198,115 +198,35 @@ describe('useWorkspaces', () => {
test('mutation', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const { result } = renderHook(() => useWorkspacesHelper(), {
const { result } = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
{
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
}
await result.current.createLocalWorkspace('test');
const workspaces = await store.get(workspacesAtom);
console.log(workspaces);
expect(workspaces.length).toEqual(1);
{
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(2);
}
const { result: result2 } = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper,
});
expect(result2.current.length).toEqual(1);
const firstWorkspace = result2.current[0];
expect(result2.current.length).toEqual(2);
const firstWorkspace = result2.current[1];
expect(firstWorkspace.flavour).toBe('local');
assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL);
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
});
});
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
test('from "/"', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(currentWorkspaceAtom);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'page0');
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push('/');
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe('/');
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
});
test('from empty workspace', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
mutationHook.rerender();
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT + 50));
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/all`);
});
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'page0');
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT + 50));
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
});
});
describe('useRecentlyViewed', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceId = blockSuiteWorkspace.id;
const pageId = 'page0';
store.set(jotaiWorkspacesAtom, [
store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,

View File

@@ -1,4 +1,4 @@
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { useCallback } from 'react';
import useSWR from 'swr';
@@ -16,7 +16,7 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
});
await mutate(QueryKey.getWorkspaces);
// fixme: remove force update
jotaiStore.set(jotaiWorkspacesAtom, ws => [...ws]);
rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
},
[mutate, workspace.id]
);

View File

@@ -1,43 +1,12 @@
import type { Page } from '@blocksuite/store';
import { atom, useAtom, useAtomValue } from 'jotai';
import { currentPageIdAtom } from '../../atoms';
import { currentWorkspaceAtom } from './use-current-workspace';
export const currentPageAtom = atom<Promise<Page | null>>(async get => {
const id = get(currentPageIdAtom);
const workspace = await get(currentWorkspaceAtom);
if (!workspace || !id) {
return Promise.resolve(null);
}
const page = workspace.blockSuiteWorkspace.getPage(id);
if (page) {
return page;
} else {
return new Promise(resolve => {
const dispose = workspace.blockSuiteWorkspace.slots.pageAdded.on(
pageId => {
if (pageId === id) {
resolve(page);
dispose.dispose();
}
}
);
});
}
});
export function useCurrentPage(): Page | null {
return useAtomValue(currentPageAtom);
}
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useAtom } from 'jotai';
/**
* @deprecated
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export function useCurrentPageId(): [
string | null,
(newId: string | null) => void
] {
return useAtom(currentPageIdAtom);
return useAtom(rootCurrentPageIdAtom);
}

View File

@@ -1,21 +1,15 @@
import { atomWithSyncStorage } from '@affine/jotai';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
workspacesAtom,
} from '../../atoms';
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import type { AllWorkspace } from '../../shared';
export const currentWorkspaceAtom = atom<Promise<AllWorkspace | null>>(
async get => {
const id = get(currentWorkspaceIdAtom);
const workspaces = await get(workspacesAtom);
return workspaces.find(workspace => workspace.id === id) ?? null;
}
);
/**
* @deprecated use `rootCurrentWorkspaceAtom` instead
*/
export const currentWorkspaceAtom = rootCurrentWorkspaceAtom;
export const lastWorkspaceIdAtom = atomWithSyncStorage<string | null>(
'last_workspace_id',
@@ -26,7 +20,7 @@ export function useCurrentWorkspace(): [
AllWorkspace | null,
(id: string | null) => void
] {
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [, setId] = useAtom(currentWorkspaceIdAtom);
const [, setPageId] = useAtom(currentPageIdAtom);
const setLast = useSetAtom(lastWorkspaceIdAtom);

View File

@@ -1,30 +0,0 @@
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { useEffect } from 'react';
import { WorkspacePlugins } from '../plugins';
export function useCreateFirstWorkspace() {
// may not need use effect at all, right?
useEffect(() => {
return jotaiStore.sub(jotaiWorkspacesAtom, () => {
const workspaces = jotaiStore.get(jotaiWorkspacesAtom);
if (workspaces.length === 0) {
createFirst();
}
/**
* Create a first workspace, only just once for a browser
*/
async function createFirst() {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
for (const Plugin of Plugins) {
await Plugin.Events['app:first-init']?.();
}
}
});
}, []);
}

View File

@@ -0,0 +1,84 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect, useRef } from 'react';
import { rootCurrentWorkspaceAtom } from '../atoms/root';
export const HALT_PROBLEM_TIMEOUT = 1000;
export function useRouterAndWorkspaceWithPageIdDefense(router: NextRouter) {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [currentPageId, setCurrentPageId] = useAtom(rootCurrentPageIdAtom);
const fallbackModeRef = useRef(false);
useEffect(() => {
if (!router.isReady) {
return;
}
const { workspaceId, pageId } = router.query;
if (typeof pageId !== 'string') {
console.warn('pageId is not a string', pageId);
return;
}
if (typeof workspaceId !== 'string') {
console.warn('workspaceId is not a string', workspaceId);
return;
}
if (currentWorkspace?.id !== workspaceId) {
console.warn('workspaceId is not currentWorkspace', workspaceId);
return;
}
if (currentPageId !== pageId && !fallbackModeRef.current) {
console.log('set current page id', pageId);
setCurrentPageId(pageId);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId,
pageId,
},
});
}
}, [currentPageId, currentWorkspace.id, router, setCurrentPageId]);
useEffect(() => {
if (fallbackModeRef.current) {
return;
}
const id = setTimeout(() => {
if (currentPageId) {
const page =
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
const firstOne =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0);
if (firstOne) {
console.warn(
'cannot find page',
currentPageId,
'so redirect to',
firstOne.id
);
setCurrentPageId(firstOne.id);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: firstOne.id,
},
});
fallbackModeRef.current = true;
}
}
}
}, HALT_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [
currentPageId,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
router,
setCurrentPageId,
]);
}

View File

@@ -0,0 +1,48 @@
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useRouterWithWorkspaceIdDefense(router: NextRouter) {
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspaceId) {
return;
}
const exist = metadata.find(m => m.id === currentWorkspaceId);
if (!exist) {
// clean up
setCurrentWorkspaceId(null);
setCurrentPageId(null);
const firstOne = metadata.at(0);
if (!firstOne) {
throw new Error('no workspace');
}
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
...router.query,
workspaceId: firstOne.id,
},
});
}
}, [
currentWorkspaceId,
metadata,
router,
router.isReady,
setCurrentPageId,
setCurrentWorkspaceId,
]);
}

View File

@@ -0,0 +1,18 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentPageId(router: NextRouter) {
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const pageId = router.query.pageId;
if (typeof pageId === 'string') {
console.log('set page id', pageId);
setCurrentPageId(pageId);
}
}, [router.isReady, router.query.pageId, setCurrentPageId]);
}

View File

@@ -1,215 +0,0 @@
import { jotaiStore } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
import { currentPageIdAtom } from '../atoms';
import type { AllWorkspace } from '../shared';
import { WorkspaceSubPath } from '../shared';
import { useCurrentPageId } from './current/use-current-page-id';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { RouteLogic, useRouterHelper } from './use-router-helper';
import { useWorkspaces } from './use-workspaces';
export function findSuitablePageId(
workspace: AllWorkspace,
targetId: string
): string | null {
switch (workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
return (
workspace.blockSuiteWorkspace.meta.pageMetas.find(
page => page.id === targetId
)?.id ??
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
null
);
}
case WorkspaceFlavour.LOCAL: {
return (
workspace.blockSuiteWorkspace.meta.pageMetas.find(
page => page.id === targetId
)?.id ??
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
null
);
}
case WorkspaceFlavour.PUBLIC: {
return null;
}
}
}
export const REDIRECT_TIMEOUT = 1000;
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useCurrentPageId();
const workspaces = useWorkspaces();
const { jumpToSubPath } = useRouterHelper(router);
useEffect(() => {
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
if (url.startsWith('/')) {
const path = url.split('/');
if (path.length === 4 && path[1] === 'workspace') {
if (
path[3] === 'all' ||
path[3] === 'setting' ||
path[3] === 'trash' ||
path[3] === 'favorite' ||
path[3] === 'shared'
) {
return;
}
setCurrentWorkspaceId(path[2]);
if (currentWorkspace && 'blockSuiteWorkspace' in currentWorkspace) {
if (currentWorkspace.blockSuiteWorkspace.getPage(path[3])) {
setCurrentPageId(path[3]);
}
}
}
}
};
router.events.on('routeChangeStart', listener);
return () => {
router.events.off('routeChangeStart', listener);
};
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
useEffect(() => {
if (!router.isReady) {
return;
}
if (
router.pathname === '/workspace/[workspaceId]/[pageId]' ||
router.pathname === '/'
) {
const targetPageId = router.query.pageId;
const targetWorkspaceId = router.query.workspaceId;
if (currentWorkspace && currentPageId) {
if (
currentWorkspace.id === targetWorkspaceId &&
currentPageId === targetPageId
) {
return;
}
}
if (
typeof targetPageId !== 'string' ||
typeof targetWorkspaceId !== 'string'
) {
if (router.asPath === '/') {
const first = workspaces.at(0);
if (first && 'blockSuiteWorkspace' in first) {
const targetWorkspaceId = first.id;
const targetPageId =
first.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (targetPageId) {
setCurrentWorkspaceId(targetWorkspaceId);
setCurrentPageId(targetPageId);
router.push(`/workspace/${targetWorkspaceId}/${targetPageId}`);
}
}
}
return;
}
if (!currentWorkspace) {
const targetWorkspace = workspaces.find(
workspace => workspace.id === targetPageId
);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
router.push({
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});
return;
} else {
const first = workspaces.at(0);
if (first) {
setCurrentWorkspaceId(first.id);
router.push({
query: {
...router.query,
workspaceId: first.id,
},
});
return;
}
}
} else {
if (!currentPageId && currentWorkspace) {
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
if (targetId) {
setCurrentPageId(targetId);
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: targetId,
},
});
return;
} else {
const dispose =
currentWorkspace.blockSuiteWorkspace.slots.pageAdded.on(
pageId => {
if (pageId === targetPageId) {
dispose.dispose();
setCurrentPageId(pageId);
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: targetPageId,
},
});
}
}
);
const clearId = setTimeout(() => {
const pageId = jotaiStore.get(currentPageIdAtom);
if (pageId === null) {
const id =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (id) {
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: id,
},
});
setCurrentPageId(id);
dispose.dispose();
return;
}
}
jumpToSubPath(
currentWorkspace.blockSuiteWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
dispose.dispose();
}, REDIRECT_TIMEOUT);
return () => {
clearTimeout(clearId);
dispose.dispose();
};
}
}
}
}
}, [
currentPageId,
currentWorkspace,
router.query.workspaceId,
router.query.pageId,
setCurrentPageId,
setCurrentWorkspaceId,
workspaces,
router,
jumpToSubPath,
]);
}

View File

@@ -0,0 +1,51 @@
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (currentWorkspaceId) {
return;
}
const targetWorkspace = metadata.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
} else {
const targetWorkspace = metadata.at(0);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
}
}
}, [currentWorkspaceId, metadata, router, setCurrentWorkspaceId]);
}

View File

@@ -1,53 +0,0 @@
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { useWorkspaces } from './use-workspaces';
export function useSyncRouterWithCurrentWorkspace(router: NextRouter) {
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
const workspaces = useWorkspaces();
useEffect(() => {
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
if (url.startsWith('/')) {
const path = url.split('/');
if (path.length === 4 && path[1] === 'workspace') {
setCurrentWorkspaceId(path[2]);
}
}
};
router.events.on('routeChangeStart', listener);
return () => {
router.events.off('routeChangeStart', listener);
};
}, [currentWorkspace, router, setCurrentWorkspaceId]);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (!currentWorkspace) {
const targetWorkspace = workspaces.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
} else {
const targetWorkspace = workspaces.at(0);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
}
}
}
}, [currentWorkspace, router, setCurrentWorkspaceId, workspaces]);
}

View File

@@ -1,12 +1,13 @@
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { WorkspaceFlavour } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspacePlugins } from '../plugins';
import { useRouterHelper } from './use-router-helper';
/**
* Transform workspace from one flavour to another
@@ -14,9 +15,8 @@ import { useRouterHelper } from './use-router-helper';
* The logic here is to delete the old workspace and create a new one.
*/
export function useTransformWorkspace() {
const set = useSetAtom(jotaiWorkspacesAtom);
const router = useRouter();
const helper = useRouterHelper(router);
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
@@ -35,9 +35,9 @@ export function useTransformWorkspace() {
});
return [...workspaces];
});
await helper.jumpToWorkspace(newId);
setCurrentWorkspaceId(newId);
return newId;
},
[helper, set]
[set, setCurrentWorkspaceId]
);
}

View File

@@ -1,5 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -18,10 +18,13 @@ export function useWorkspaces(): AllWorkspace[] {
const logger = new DebugLogger('use-workspaces');
export function useWorkspacesHelper() {
/**
* This hook has the permission to all workspaces. Be careful when using it.
*/
export function useAppHelper() {
const workspaces = useWorkspaces();
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return {
createWorkspacePage: useCallback(
(workspaceId: string, pageId: string) => {

View File

@@ -1,21 +1,24 @@
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import { config, DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import { setUpLanguage, useTranslation } from '@affine/i18n';
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootStore,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists, nanoid } from '@blocksuite/store';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { FC, PropsWithChildren } from 'react';
import { lazy, Suspense, useCallback, useEffect } from 'react';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import {
currentWorkspaceIdAtom,
openQuickSearchModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
@@ -23,18 +26,19 @@ import {
import { HelpIsland } from '../components/pure/help-island';
import { PageLoading } from '../components/pure/loading';
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useRouterTitle } from '../hooks/use-router-title';
import { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
import {
useSidebarFloating,
useSidebarResizing,
useSidebarStatus,
useSidebarWidth,
} from '../hooks/use-sidebar-status';
import { useSyncRouterWithCurrentPageId } from '../hooks/use-sync-router-with-current-page-id';
import { useSyncRouterWithCurrentWorkspaceId } from '../hooks/use-sync-router-with-current-workspace-id';
import { useWorkspaces } from '../hooks/use-workspaces';
import { WorkspacePlugins } from '../plugins';
import { ModalProvider } from '../providers/ModalProvider';
@@ -114,6 +118,53 @@ const logger = new DebugLogger('workspace-layout');
const affineGlobalChannel = createAffineGlobalChannel(
WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD
);
export const AllWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const workspaces = useWorkspaces();
useEffect(() => {
const providers = workspaces
// ignore current workspace
.filter(workspace => workspace.id !== currentWorkspaceId)
.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
}, [currentWorkspaceId, workspaces]);
return <>{children}</>;
};
export const CurrentWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const router = useRouter();
const workspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
useSyncRouterWithCurrentWorkspaceId(router);
useSyncRouterWithCurrentPageId(router);
useRouterWithWorkspaceIdDefense(router);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
if (!router.isReady) {
return <PageLoading text="Router is loading" />;
}
if (!workspaceId) {
return <PageLoading text="Finding workspace id" />;
}
if (!exist) {
return <PageLoading text="Workspace not found" />;
}
return <>{children}</>;
};
export const WorkspaceLayout: FC<PropsWithChildren> =
function WorkspacesSuspense({ children }) {
const { i18n } = useTranslation();
@@ -122,10 +173,9 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n);
}, [i18n]);
useCreateFirstWorkspace();
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
useEffect(() => {
logger.info('mount');
const controller = new AbortController();
@@ -134,7 +184,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
.map(({ CRUD }) => CRUD.list);
async function fetch() {
const jotaiWorkspaces = jotaiStore.get(jotaiWorkspacesAtom);
const jotaiWorkspaces = rootStore.get(rootWorkspacesMetadataAtom);
const items = [];
for (const list of lists) {
try {
@@ -180,22 +230,31 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
return (
<>
{/* fixme(himself65): don't re-render whole modals */}
<ModalProvider key={currentWorkspaceId} />
<Suspense fallback={<PageLoading />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Suspense>
<AllWorkspaceContext>
<CurrentWorkspaceContext>
<ModalProvider key={currentWorkspaceId} />
</CurrentWorkspaceContext>
</AllWorkspaceContext>
<CurrentWorkspaceContext>
<Suspense fallback={<PageLoading text="Finding current workspace" />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Suspense>
</CurrentWorkspaceContext>
</>
);
};
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId();
const workspaces = useWorkspaces();
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
logger.info('workspaces: ', workspaces);
}, [workspaces]);
logger.info('currentWorkspace: ', currentWorkspace);
}, [currentWorkspace]);
useEffect(() => {
if (currentWorkspace) {
@@ -203,38 +262,82 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
}
}, [currentWorkspace]);
useEffect(() => {
const providers = workspaces.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
}, [workspaces]);
useEffect(() => {
if (currentWorkspace) {
currentWorkspace.providers.forEach(provider => {
if (provider.background) {
return;
}
provider.connect();
});
return () => {
currentWorkspace.providers.forEach(provider => {
if (provider.background) {
return;
}
provider.disconnect();
});
};
}
}, [currentWorkspace]);
const router = useRouter();
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
setIsLoading(false);
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
if (!router.query.pageId) {
setCurrentPageId(pageId);
void jumpToPage(currentWorkspace.id, pageId);
}
}
// no matter the workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router, setCurrentPageId]);
useEffect(() => {
if (!currentWorkspace) {
return;
}
const page = currentWorkspace.blockSuiteWorkspace.getPage(
DEFAULT_HELLO_WORLD_PAGE_ID
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(
DEFAULT_HELLO_WORLD_PAGE_ID,
{
jumpOnce: false,
}
);
setCurrentPageId(currentPageId);
void jumpToPage(currentWorkspace.id, page.id);
}
}, [
currentPageId,
currentWorkspace,
jumpToPage,
router.query.pageId,
setCurrentPageId,
]);
const { openPage } = useRouterHelper(router);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
@@ -339,7 +442,9 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
</StyledSpacer>
<MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}>
<MainContainer className="main-container">
{children}
<Suspense fallback={<PageLoading text="Page is Loading" />}>
{isLoading ? <PageLoading text="Page is Loading" /> : children}
</Suspense>
<StyledToolWrapper>
{/* fixme(himself65): remove this */}
<div id="toolWrapper" style={{ marginBottom: '12px' }}>

View File

@@ -2,14 +2,15 @@ import '@affine/component/theme/global.css';
import { config, setupGlobal } from '@affine/env';
import { createI18n, I18nextProvider } from '@affine/i18n';
import { jotaiStore } from '@affine/workspace/atom';
import { rootStore } from '@affine/workspace/atom';
import type { EmotionCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { Provider } from 'jotai';
import { DevTools } from 'jotai-devtools';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import type { PropsWithChildren, ReactElement } from 'react';
import React, { Suspense, useEffect, useMemo } from 'react';
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
@@ -30,6 +31,15 @@ const EmptyLayout = (page: ReactElement) => page;
const clientSideEmotionCache = createEmotionCache();
const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
return (
<>
{process.env.DEBUG_JOTAI === 'true' && <DevTools />}
{children}
</>
);
};
const App = function App({
Component,
pageProps,
@@ -55,10 +65,12 @@ const App = function App({
<Suspense fallback={<PageLoading key="RootPageLoading" />}>
<ProviderComposer
contexts={useMemo(
() => [
<Provider key="JotaiProvider" store={jotaiStore} />,
<ThemeProvider key="ThemeProvider" />,
],
() =>
[
<Provider key="JotaiProvider" store={rootStore} />,
<DebugProvider key="DebugProvider" />,
<ThemeProvider key="ThemeProvider" />,
].filter(Boolean),
[]
)}
>

View File

@@ -1,9 +1,9 @@
import { initPage } from '@affine/env/blocksuite';
import { useRouter } from 'next/router';
import { lazy, Suspense } from 'react';
import { StyledPage, StyledWrapper } from '../../layouts/styles';
import type { NextPageWithLayout } from '../../shared';
import { initPage } from '../../utils';
const Editor = lazy(() =>
import('../../components/__debug__/client/Editor').then(module => ({

View File

@@ -5,18 +5,18 @@ import React, { Suspense, useEffect } from 'react';
import { PageLoading } from '../components/pure/loading';
import { useLastWorkspaceId } from '../hooks/affine/use-last-leave-workspace-id';
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces } from '../hooks/use-workspaces';
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared';
const logger = new DebugLogger('IndexPage');
const logger = new DebugLogger('index-page');
const IndexPageInner = () => {
const router = useRouter();
const { jumpToPage, jumpToSubPath } = useRouterHelper(router);
const workspaces = useWorkspaces();
const lastWorkspaceId = useLastWorkspaceId();
const helper = useAppHelper();
useEffect(() => {
if (!router.isReady) {
@@ -32,13 +32,12 @@ const IndexPageInner = () => {
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
return;
void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
} else {
const clearId = setTimeout(() => {
dispose.dispose();
logger.debug('Found target workspace. Jump to all pages');
jumpToSubPath(
void jumpToSubPath(
targetWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
@@ -47,7 +46,7 @@ const IndexPageInner = () => {
const dispose =
targetWorkspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => {
clearTimeout(clearId);
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
});
return () => {
clearTimeout(clearId);
@@ -55,19 +54,16 @@ const IndexPageInner = () => {
};
}
} else {
logger.debug('No target workspace. jump to all pages');
// fixme: should create new workspace
jumpToSubPath('ERROR', WorkspaceSubPath.ALL, RouteLogic.REPLACE);
console.warn('No target workspace. This should not happen in production');
}
}, [jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
}, [helper, jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
return <PageLoading key="IndexPageInfinitePageLoading" />;
};
const IndexPage: NextPage = () => {
useCreateFirstWorkspace();
return (
<Suspense fallback={<PageLoading />}>
<Suspense fallback={<PageLoading text="Loading all workspaces" />}>
<IndexPageInner />
</Suspense>
);

View File

@@ -1,4 +1,5 @@
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import { PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
@@ -25,7 +26,6 @@ import {
PublicWorkspaceLayout,
} from '../../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
import { initPage } from '../../../utils';
export const NavContainer = styled('div')(({ theme }) => {
return {

View File

@@ -1,20 +1,23 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-blocksuite-workspace-page';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback, useEffect } from 'react';
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
import { useCurrentPageId } from '../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { usePageMeta, usePageMetaHelper } from '../../../hooks/use-page-meta';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
import { useRouterAndWorkspaceWithPageIdDefense } from '../../../hooks/use-router-and-workspace-with-page-id-defense';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
@@ -32,7 +35,7 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
const WorkspaceDetail: React.FC = () => {
const router = useRouter();
const { openPage } = useRouterHelper(router);
const [currentPageId] = useCurrentPageId();
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace ?? null;
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
@@ -80,7 +83,7 @@ const WorkspaceDetail: React.FC = () => {
return <PageLoading />;
}
if (!currentPageId) {
return <PageLoading />;
return <PageLoading text="Loading page." />;
}
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
@@ -104,14 +107,17 @@ const WorkspaceDetail: React.FC = () => {
const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
useSyncRouterWithCurrentWorkspaceAndPage(router);
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
useRouterAndWorkspaceWithPageIdDefense(router);
const page = useBlockSuiteWorkspacePage(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
if (!router.isReady) {
return <PageLoading />;
} else if (
typeof router.query.pageId !== 'string' ||
typeof router.query.workspaceId !== 'string'
) {
throw new Error('Invalid router query');
return <PageLoading text="Router is loading" />;
} else if (!currentPageId || !page) {
return <PageLoading text="Page is loading" />;
}
return <WorkspaceDetail />;
};

View File

@@ -1,11 +1,10 @@
import { useTranslation } from '@affine/i18n';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { FolderIcon } from '@blocksuite/icons';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import {
QueryParamError,
@@ -15,56 +14,17 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
import { ensureRootPinboard } from '../../../utils';
const AllPage: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
if (currentWorkspace.flavour !== WorkspaceFlavour.LOCAL) {
// only create a new page for local workspace
// just ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
jumpToPage(currentWorkspace.id, pageId);
}
// no matter workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router]);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const FavouritePage: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const FavouritePage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -18,9 +18,9 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
@@ -33,7 +33,7 @@ const SettingPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
useEffect(() => {});
const onChangeTab = useCallback(
@@ -92,7 +92,7 @@ const SettingPage: NextPageWithLayout = () => {
}
}, [currentTab, router, setCurrentTab]);
const helper = useWorkspacesHelper();
const helper = useAppHelper();
const onDeleteWorkspace = useCallback(() => {
assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const SharedPages: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const SharedPages: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const TrashPage: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const TrashPage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -1,5 +1,5 @@
import { getLoginStorage } from '@affine/workspace/affine/login';
import { jotaiStore } from '@affine/workspace/atom';
import { rootStore } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -43,7 +43,7 @@ export const fetcher = async (
if (typeof key !== 'string') {
throw new TypeError('key must be a string');
}
const workspaces = await jotaiStore.get(workspacesAtom);
const workspaces = await rootStore.get(workspacesAtom);
const workspace = workspaces.find(({ id }) => id === workspaceId);
assertExists(workspace);
const storage = await workspace.blockSuiteWorkspace.blobs;

View File

@@ -1,4 +1,5 @@
import { prefixUrl } from '@affine/env';
import { initPage } from '@affine/env/blocksuite';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
@@ -8,7 +9,7 @@ import {
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -26,7 +27,7 @@ import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh
import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider';
import { BlockSuiteWorkspace } from '../../shared';
import { affineApis } from '../../shared/apis';
import { initPage, toast } from '../../utils';
import { toast } from '../../utils';
import type { WorkspacePlugin } from '..';
import { QueryKey } from './fetcher';
@@ -81,20 +82,20 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
if (response) {
setLoginStorage(response);
const user = parseIdToken(response.token);
jotaiStore.set(currentAffineUserAtom, user);
rootStore.set(currentAffineUserAtom, user);
} else {
toast('Login failed');
}
},
'workspace:revoke': async () => {
jotaiStore.set(jotaiWorkspacesAtom, workspaces =>
rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
)
);
storage.removeItem(kAffineLocal);
clearLoginStorage();
jotaiStore.set(currentAffineUserAtom, null);
rootStore.set(currentAffineUserAtom, null);
},
},
CRUD: {

View File

@@ -27,9 +27,7 @@ export const WorkspacePlugins = {
[WorkspaceFlavour.PUBLIC]: {
flavour: WorkspaceFlavour.PUBLIC,
loadPriority: LoadPriority.LOW,
Events: {
'app:first-init': async () => {},
},
Events: {} as Partial<AppEvents>,
// todo: implement this
CRUD: {
get: unimplemented,

View File

@@ -1,17 +1,23 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { CRUD } from '@affine/workspace/local/crud';
import {
DEFAULT_HELLO_WORLD_PAGE_ID,
DEFAULT_WORKSPACE_NAME,
} from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import { createIndexedDBProvider } from '@affine/workspace/providers';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import React from 'react';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { PageDetailEditor } from '../../components/page-detail-editor';
import { initPage } from '../../utils';
import type { WorkspacePlugin } from '..';
const logger = new DebugLogger('use-create-first-workspace');
@@ -20,25 +26,29 @@ export const LocalPlugin: WorkspacePlugin<WorkspaceFlavour.LOCAL> = {
flavour: WorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW,
Events: {
'app:first-init': async () => {
'app:init': () => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(),
(_: string) => undefined
);
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
const workspace = await LocalPlugin.CRUD.get(id);
assertExists(workspace);
assertEquals(workspace.id, id);
// todo: use a better way to set initial workspace
jotaiStore.set(jotaiWorkspacesAtom, ws => [
...ws,
{
id: workspace.id,
flavour: WorkspaceFlavour.LOCAL,
},
]);
logger.debug('create first workspace', workspace);
const page = blockSuiteWorkspace.createPage(DEFAULT_HELLO_WORLD_PAGE_ID);
blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
blockSuiteWorkspace.setPageMeta(page.id, {
jumpOnce: true,
});
const provider = createIndexedDBProvider(blockSuiteWorkspace);
provider.connect();
provider.callbacks.add(() => {
provider.disconnect();
});
ensureRootPinboard(blockSuiteWorkspace);
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
logger.debug('create first workspace');
return [blockSuiteWorkspace.id];
},
},
CRUD,

View File

@@ -1,8 +1,8 @@
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { arrayMove } from '@dnd-kit/sortable';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import type { ReactElement } from 'react';
import { lazy, Suspense, useCallback, useTransition } from 'react';
import {
@@ -15,7 +15,7 @@ import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
import { useCurrentUser } from '../hooks/current/use-current-user';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared';
const WorkspaceListModal = lazy(() =>
@@ -41,10 +41,10 @@ export function Modals() {
const { jumpToSubPath } = useRouterHelper(router);
const user = useCurrentUser();
const workspaces = useWorkspaces();
const setWorkspaces = useSetAtom(jotaiWorkspacesAtom);
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const [, setCurrentWorkspace] = useCurrentWorkspace();
const { createLocalWorkspace } = useWorkspacesHelper();
const { createLocalWorkspace } = useAppHelper();
const [transitioning, transition] = useTransition();
return (
@@ -122,13 +122,10 @@ export function Modals() {
);
}
export const ModalProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
export const ModalProvider = (): ReactElement => {
return (
<>
<Modals />
{children}
</>
);
};

View File

@@ -7,7 +7,7 @@ import {
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import type { LoginResponse } from '@affine/workspace/affine/login';
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
import { jotaiStore } from '@affine/workspace/atom';
import { rootStore } from '@affine/workspace/atom';
const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>;
@@ -19,7 +19,7 @@ const debugLogger = new DebugLogger('affine-debug-apis');
if (!globalThis.AFFINE_APIS) {
globalThis.AFFINE_APIS = affineApis;
globalThis.setLogin = (response: LoginResponse) => {
jotaiStore.set(currentAffineUserAtom, parseIdToken(response.token));
rootStore.set(currentAffineUserAtom, parseIdToken(response.token));
setLoginStorage(response);
};
const loginMockUser1 = async () => {

View File

@@ -1,5 +1,6 @@
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type { AffinePublicWorkspace } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { NextPage } from 'next';
import type { ReactElement, ReactNode } from 'react';
@@ -11,7 +12,7 @@ export type AffineOfficialWorkspace =
| LocalWorkspace
| AffinePublicWorkspace;
export type AllWorkspace = AffineOfficialWorkspace;
export type AllWorkspace = WorkspaceRegistry[keyof WorkspaceRegistry];
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
P,

View File

@@ -1,86 +0,0 @@
import { DebugLogger } from '@affine/debug';
import markdown from '@affine/templates/Welcome-to-AFFiNE.md';
import { ContentParser } from '@blocksuite/blocks/content-parser';
import type { Page } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import type { BlockSuiteWorkspace } from '../shared';
const demoTitle = markdown
.split('\n')
.splice(0, 1)
.join('')
.replaceAll('#', '')
.trim();
const demoText = markdown.split('\n').slice(1).join('\n');
const logger = new DebugLogger('init-page');
export function initPage(page: Page): void {
logger.debug('initEmptyPage', page.id);
// Add page block and surface block at root level
const isFirstPage = page.meta.init === true;
if (isFirstPage) {
page.workspace.setPageMeta(page.id, {
init: false,
});
_initPageWithDemoMarkdown(page);
} else {
_initEmptyPage(page);
}
page.resetHistory();
}
export function _initEmptyPage(page: Page, title?: string): void {
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(title ?? ''),
});
page.addBlock('affine:surface', {}, null);
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
}
export function _initPageWithDemoMarkdown(page: Page): void {
logger.debug('initPageWithDefaultMarkdown', page.id);
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(demoTitle),
});
page.addBlock('affine:surface', {}, null);
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
const contentParser = new ContentParser(page);
contentParser.importMarkdown(demoText, frameId).then(() => {
document.dispatchEvent(
new CustomEvent('markdown:imported', {
detail: {
workspaceId: page.workspace.id,
pageId: page.id,
},
})
);
});
page.workspace.setPageMeta(page.id, { demoTitle });
}
export function ensureRootPinboard(blockSuiteWorkspace: BlockSuiteWorkspace) {
const metas = blockSuiteWorkspace.meta.pageMetas;
const rootMeta = metas.find(m => m.isRootPinboard);
if (rootMeta) {
return rootMeta.id;
}
const rootPinboardPage = blockSuiteWorkspace.createPage(nanoid());
const title = `${blockSuiteWorkspace.meta.name}'s Pinboard`;
_initEmptyPage(rootPinboardPage, title);
blockSuiteWorkspace.meta.setPageMeta(rootPinboardPage.id, {
isRootPinboard: true,
title,
});
return rootPinboardPage.id;
}

View File

@@ -1,4 +1,3 @@
export * from './blocksuite';
export * from './create-emotion-cache';
export * from './string2color';
export * from './toast';