mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
refactor: support suspense mode in workspaces (#1304)
This commit is contained in:
@@ -8,22 +8,26 @@ import assert from 'node:assert';
|
||||
import { __unstableSchemas, builtInSchemas } from '@blocksuite/blocks/models';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { render, renderHook } from '@testing-library/react';
|
||||
import { createStore, Provider } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import routerMock from 'next-router-mock';
|
||||
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
||||
import React from 'react';
|
||||
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
import { BlockSuiteWorkspace, RemWorkspaceFlavour } from '../../shared';
|
||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
||||
import { useBlockSuiteWorkspaceName } from '../use-blocksuite-workspace-name';
|
||||
import { useLastOpenedWorkspace } from '../use-last-opened-workspace';
|
||||
import { usePageMeta, usePageMetaHelper } from '../use-page-meta';
|
||||
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../use-sync-router-with-current-workspace-and-page';
|
||||
import {
|
||||
useWorkspaces,
|
||||
useWorkspacesHelper,
|
||||
vitestRefreshWorkspaces,
|
||||
} from '../use-workspaces';
|
||||
currentWorkspaceAtom,
|
||||
useCurrentWorkspace,
|
||||
} from '../current/use-current-workspace';
|
||||
import { useBlockSuiteWorkspaceName } from '../use-blocksuite-workspace-name';
|
||||
import { usePageMeta, usePageMetaHelper } from '../use-page-meta';
|
||||
import {
|
||||
REDIRECT_TIMEOUT,
|
||||
useSyncRouterWithCurrentWorkspaceAndPage,
|
||||
} from '../use-sync-router-with-current-workspace-and-page';
|
||||
import { useWorkspaces, useWorkspacesHelper } from '../use-workspaces';
|
||||
|
||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
beforeAll(() => {
|
||||
@@ -32,9 +36,26 @@ beforeAll(() => {
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
async function getJotaiContext() {
|
||||
const store = createStore();
|
||||
const ProviderWrapper: React.FC<React.PropsWithChildren> =
|
||||
function ProviderWrapper({ children }) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
};
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toBe(0);
|
||||
return {
|
||||
store,
|
||||
ProviderWrapper,
|
||||
initialWorkspaces: workspaces,
|
||||
} as const;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vitestRefreshWorkspaces();
|
||||
dataCenter.isLoaded = true;
|
||||
return new Promise<void>(resolve => {
|
||||
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
||||
room: 'test',
|
||||
@@ -104,16 +125,50 @@ describe('usePageMetas', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkspacesHelper', () => {
|
||||
test('basic', async () => {
|
||||
const { ProviderWrapper, store } = await getJotaiContext();
|
||||
const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), {
|
||||
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);
|
||||
const workspacesHook = renderHook(() => useWorkspaces(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
await store.get(currentWorkspaceAtom);
|
||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkspaces', () => {
|
||||
test('basic', () => {
|
||||
const { result } = renderHook(() => useWorkspaces());
|
||||
test('basic', async () => {
|
||||
const { ProviderWrapper } = await getJotaiContext();
|
||||
const { result } = renderHook(() => useWorkspaces(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
test('mutation', () => {
|
||||
const { result } = renderHook(() => useWorkspacesHelper());
|
||||
result.current.createRemLocalWorkspace('test');
|
||||
const { result: result2 } = renderHook(() => useWorkspaces());
|
||||
test('mutation', async () => {
|
||||
const { ProviderWrapper, store } = await getJotaiContext();
|
||||
const { result } = renderHook(() => useWorkspacesHelper(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
await result.current.createLocalWorkspace('test');
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
console.log(workspaces);
|
||||
expect(workspaces.length).toEqual(1);
|
||||
const { result: result2 } = renderHook(() => useWorkspaces(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
expect(result2.current.length).toEqual(1);
|
||||
const firstWorkspace = result2.current[0];
|
||||
expect(firstWorkspace.flavour).toBe('local');
|
||||
@@ -124,8 +179,13 @@ describe('useWorkspaces', () => {
|
||||
|
||||
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
||||
test('from "/"', async () => {
|
||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
||||
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('/');
|
||||
@@ -134,6 +194,7 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
||||
renderHook(
|
||||
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
||||
{
|
||||
wrapper: ProviderWrapper,
|
||||
initialProps: {
|
||||
router: routerHook.result.current,
|
||||
},
|
||||
@@ -144,8 +205,14 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
||||
});
|
||||
|
||||
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
|
||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
||||
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`);
|
||||
@@ -154,29 +221,16 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
||||
renderHook(
|
||||
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
||||
{
|
||||
wrapper: ProviderWrapper,
|
||||
initialProps: {
|
||||
router: routerHook.result.current,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
|
||||
});
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT));
|
||||
|
||||
describe('useLastOpenedWorkspace', () => {
|
||||
test('basic', async () => {
|
||||
const workspaceHelperHook = renderHook(() => useWorkspacesHelper());
|
||||
workspaceHelperHook.result.current.createRemLocalWorkspace('test');
|
||||
const workspacesHook = renderHook(() => useWorkspaces());
|
||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace());
|
||||
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id);
|
||||
const lastOpenedWorkspace = renderHook(() => useLastOpenedWorkspace());
|
||||
expect(lastOpenedWorkspace.result.current[0]).toBe(null);
|
||||
const lastOpenedWorkspace2 = renderHook(() => useLastOpenedWorkspace());
|
||||
expect(lastOpenedWorkspace2.result.current[0]).toBe(
|
||||
workspacesHook.result.current[0].id
|
||||
);
|
||||
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,12 +11,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper';
|
||||
import { usePageMeta } from '../use-page-meta';
|
||||
import { vitestRefreshWorkspaces } from '../use-workspaces';
|
||||
|
||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
|
||||
beforeEach(() => {
|
||||
vitestRefreshWorkspaces();
|
||||
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
||||
room: 'test',
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms';
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { AffineWorkspace } from '../../shared';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { refreshDataCenter } from '../use-workspaces';
|
||||
|
||||
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||
return useCallback(
|
||||
@@ -14,7 +14,10 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||
public: isPublish,
|
||||
});
|
||||
await mutate(QueryKey.getWorkspaces);
|
||||
await refreshDataCenter();
|
||||
// force update
|
||||
jotaiStore.set(jotaiWorkspacesAtom, [
|
||||
...jotaiStore.get(jotaiWorkspacesAtom),
|
||||
]);
|
||||
},
|
||||
[workspace]
|
||||
);
|
||||
|
||||
@@ -1,7 +1,40 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function useCurrentPageId(): [
|
||||
string | null,
|
||||
(newId: string | null) => void
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
workspacesAtom,
|
||||
} from '../../atoms';
|
||||
import { RemWorkspace } from '../../shared';
|
||||
import { useWorkspace } from '../use-workspace';
|
||||
|
||||
export const currentWorkspaceAtom = atom<Promise<RemWorkspace | null>>(
|
||||
async get => {
|
||||
const id = get(currentWorkspaceIdAtom);
|
||||
const workspaces = await get(workspacesAtom);
|
||||
return workspaces.find(workspace => workspace.id === id) ?? null;
|
||||
}
|
||||
);
|
||||
|
||||
export function useCurrentWorkspace(): [
|
||||
RemWorkspace | null,
|
||||
(id: string | null) => void
|
||||
] {
|
||||
const [id, setId] = useAtom(currentWorkspaceIdAtom);
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||
const [, setId] = useAtom(currentWorkspaceIdAtom);
|
||||
const [, setPageId] = useAtom(currentPageIdAtom);
|
||||
return [
|
||||
useWorkspace(id),
|
||||
currentWorkspace,
|
||||
useCallback(
|
||||
(id: string | null) => {
|
||||
setPageId(null);
|
||||
|
||||
53
apps/web/src/hooks/use-create-first-workspace.ts
Normal file
53
apps/web/src/hooks/use-create-first-workspace.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
||||
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { jotaiWorkspacesAtom } from '../atoms';
|
||||
import { LocalPlugin } from '../plugins/local';
|
||||
import { RemWorkspaceFlavour } from '../shared';
|
||||
import { createEmptyBlockSuiteWorkspace } from '../utils';
|
||||
|
||||
export function useCreateFirstWorkspace() {
|
||||
const [jotaiWorkspaces, set] = useAtom(jotaiWorkspacesAtom);
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
/**
|
||||
* Create a first workspace, only just once for a browser
|
||||
*/
|
||||
async function createFirst() {
|
||||
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);
|
||||
const newPageId = nanoid();
|
||||
workspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => {
|
||||
assertEquals(pageId, newPageId);
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspace.id,
|
||||
flavour: RemWorkspaceFlavour.LOCAL,
|
||||
},
|
||||
]);
|
||||
});
|
||||
workspace.blockSuiteWorkspace.createPage(newPageId);
|
||||
}
|
||||
if (
|
||||
jotaiWorkspaces.length === 0 &&
|
||||
localStorage.getItem('first') !== 'true'
|
||||
) {
|
||||
localStorage.setItem('first', 'true');
|
||||
createFirst();
|
||||
}
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [jotaiWorkspaces.length, set]);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentPageId } from './current/use-current-page-id';
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
|
||||
const kLastOpenedWorkspaceKey = 'affine-last-opened-workspace';
|
||||
const kLastOpenedPageKey = 'affine-last-opened-page';
|
||||
|
||||
export function useLastOpenedWorkspace(): [
|
||||
string | null,
|
||||
string | null,
|
||||
() => void
|
||||
] {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [currentPageId] = useCurrentPageId();
|
||||
const [lastWorkspaceId, setLastWorkspaceId] = useState<string | null>(null);
|
||||
const [lastPageId, setLastPageId] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
const lastWorkspaceId = localStorage.getItem(kLastOpenedWorkspaceKey);
|
||||
if (lastWorkspaceId) {
|
||||
setLastWorkspaceId(lastWorkspaceId);
|
||||
}
|
||||
const lastPageId = localStorage.getItem(kLastOpenedPageKey);
|
||||
if (lastPageId) {
|
||||
setLastPageId(lastPageId);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
localStorage.setItem(kLastOpenedWorkspaceKey, currentWorkspace.id);
|
||||
}
|
||||
if (currentPageId) {
|
||||
localStorage.setItem(kLastOpenedPageKey, currentPageId);
|
||||
}
|
||||
}, [currentPageId, currentWorkspace]);
|
||||
const refresh = useCallback(() => {
|
||||
localStorage.removeItem(kLastOpenedWorkspaceKey);
|
||||
localStorage.removeItem(kLastOpenedPageKey);
|
||||
}, []);
|
||||
return [lastWorkspaceId, lastPageId, refresh];
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { currentPageIdAtom, jotaiStore } from '../atoms';
|
||||
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||
import { useCurrentPageId } from './current/use-current-page-id';
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
import { useWorkspaces, useWorkspacesIsLoaded } from './use-workspaces';
|
||||
import { useWorkspaces } from './use-workspaces';
|
||||
|
||||
export function findSuitablePageId(
|
||||
workspace: RemWorkspace,
|
||||
@@ -32,11 +33,11 @@ export function findSuitablePageId(
|
||||
}
|
||||
}
|
||||
|
||||
export const REDIRECT_TIMEOUT = 1000;
|
||||
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
||||
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
|
||||
const [currentPageId, setCurrentPageId] = useCurrentPageId();
|
||||
const workspaces = useWorkspaces();
|
||||
const isLoaded = useWorkspacesIsLoaded();
|
||||
useEffect(() => {
|
||||
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
|
||||
if (url.startsWith('/')) {
|
||||
@@ -66,7 +67,7 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
||||
};
|
||||
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
|
||||
useEffect(() => {
|
||||
if (!router.isReady || !isLoaded) {
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -130,19 +131,55 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
||||
}
|
||||
} else {
|
||||
if (!currentPageId && currentWorkspace) {
|
||||
if ('blockSuiteWorkspace' in currentWorkspace) {
|
||||
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
|
||||
if (targetId) {
|
||||
setCurrentPageId(targetId);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
workspaceId: currentWorkspace.id,
|
||||
pageId: targetId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
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: targetId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
const clearId = setTimeout(() => {
|
||||
if (jotaiStore.get(currentPageIdAtom) === 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();
|
||||
}, REDIRECT_TIMEOUT);
|
||||
return () => {
|
||||
clearTimeout(clearId);
|
||||
dispose.dispose();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +193,5 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
||||
setCurrentWorkspaceId,
|
||||
workspaces,
|
||||
router,
|
||||
isLoaded,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,172 +1,25 @@
|
||||
import { Workspace } from '@affine/datacenter';
|
||||
import { config, getEnvironment } from '@affine/env';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { lockMutex } from '../atoms';
|
||||
import { createLocalProviders } from '../blocksuite';
|
||||
import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms';
|
||||
import { WorkspacePlugins } from '../plugins';
|
||||
import { QueryKey } from '../plugins/affine/fetcher';
|
||||
import { kStoreKey } from '../plugins/local';
|
||||
import { LocalPlugin } from '../plugins/local';
|
||||
import { LocalWorkspace, RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||
import { createEmptyBlockSuiteWorkspace } from '../utils';
|
||||
|
||||
// fixme(himself65): refactor with jotai atom using async
|
||||
export const dataCenter = {
|
||||
workspaces: [] as RemWorkspace[],
|
||||
isLoaded: false,
|
||||
callbacks: new Set<() => void>(),
|
||||
};
|
||||
|
||||
export function vitestRefreshWorkspaces() {
|
||||
dataCenter.workspaces = [];
|
||||
dataCenter.callbacks.clear();
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var dataCenter: {
|
||||
workspaces: RemWorkspace[];
|
||||
isLoaded: boolean;
|
||||
callbacks: Set<() => void>;
|
||||
};
|
||||
}
|
||||
|
||||
globalThis.dataCenter = dataCenter;
|
||||
|
||||
function createRemLocalWorkspace(name: string) {
|
||||
const id = nanoid();
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
id,
|
||||
(_: string) => undefined
|
||||
);
|
||||
blockSuiteWorkspace.meta.setName(name);
|
||||
const workspace: LocalWorkspace = {
|
||||
flavour: RemWorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
||||
providers: [...createLocalProviders(blockSuiteWorkspace)],
|
||||
id,
|
||||
};
|
||||
if (config.enableIndexedDBProvider) {
|
||||
let ids: string[];
|
||||
try {
|
||||
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
|
||||
if (!Array.isArray(ids)) {
|
||||
localStorage.setItem(kStoreKey, '[]');
|
||||
ids = [];
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.setItem(kStoreKey, '[]');
|
||||
ids = [];
|
||||
}
|
||||
ids.push(id);
|
||||
localStorage.setItem(kStoreKey, JSON.stringify(ids));
|
||||
}
|
||||
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
|
||||
dataCenter.callbacks.forEach(cb => cb());
|
||||
return id;
|
||||
}
|
||||
|
||||
const emptyWorkspaces: RemWorkspace[] = [];
|
||||
|
||||
export async function refreshDataCenter(signal?: AbortSignal) {
|
||||
dataCenter.isLoaded = false;
|
||||
dataCenter.callbacks.forEach(cb => cb());
|
||||
if (getEnvironment().isServer) {
|
||||
return;
|
||||
}
|
||||
// fixme(himself65): `prefetchWorkspace` is not used
|
||||
// use `config.enablePlugin = ['affine', 'local']` instead
|
||||
// if (!config.prefetchWorkspace) {
|
||||
// console.info('prefetchNecessaryData: skip prefetching');
|
||||
// return;
|
||||
// }
|
||||
const plugins = Object.values(WorkspacePlugins).sort(
|
||||
(a, b) => a.loadPriority - b.loadPriority
|
||||
);
|
||||
// prefetch data in order
|
||||
for (const plugin of plugins) {
|
||||
console.info('prefetchNecessaryData: plugin', plugin.flavour);
|
||||
try {
|
||||
if (signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
const oldData = dataCenter.workspaces;
|
||||
await plugin.prefetchData(dataCenter, signal);
|
||||
const newData = dataCenter.workspaces;
|
||||
if (!Object.is(oldData, newData)) {
|
||||
console.info('prefetchNecessaryData: data changed');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('error prefetch data', plugin.flavour, e);
|
||||
}
|
||||
}
|
||||
dataCenter.isLoaded = true;
|
||||
dataCenter.callbacks.forEach(cb => cb());
|
||||
}
|
||||
|
||||
export function useWorkspaces(): RemWorkspace[] {
|
||||
return useSyncExternalStore(
|
||||
useCallback(onStoreChange => {
|
||||
dataCenter.callbacks.add(onStoreChange);
|
||||
return () => {
|
||||
dataCenter.callbacks.delete(onStoreChange);
|
||||
};
|
||||
}, []),
|
||||
useCallback(() => dataCenter.workspaces, []),
|
||||
useCallback(() => emptyWorkspaces, [])
|
||||
);
|
||||
}
|
||||
|
||||
export function useWorkspacesIsLoaded(): boolean {
|
||||
return useSyncExternalStore(
|
||||
useCallback(onStoreChange => {
|
||||
dataCenter.callbacks.add(onStoreChange);
|
||||
return () => {
|
||||
dataCenter.callbacks.delete(onStoreChange);
|
||||
};
|
||||
}, []),
|
||||
useCallback(() => dataCenter.isLoaded, []),
|
||||
useCallback(() => true, [])
|
||||
);
|
||||
}
|
||||
|
||||
export function useSyncWorkspaces() {
|
||||
return useSWR<Workspace[]>(QueryKey.getWorkspaces, {
|
||||
fallbackData: [],
|
||||
revalidateOnReconnect: true,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnMount: true,
|
||||
revalidateIfStale: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteWorkspace(workspaceId: string) {
|
||||
return lockMutex(async () => {
|
||||
console.warn('deleting workspace');
|
||||
const idx = dataCenter.workspaces.findIndex(
|
||||
workspace => workspace.id === workspaceId
|
||||
);
|
||||
if (idx === -1) {
|
||||
throw new Error('workspace not found');
|
||||
}
|
||||
try {
|
||||
const [workspace] = dataCenter.workspaces.splice(idx, 1);
|
||||
// @ts-expect-error
|
||||
await WorkspacePlugins[workspace.flavour].deleteWorkspace(workspace);
|
||||
dataCenter.callbacks.forEach(cb => cb());
|
||||
} catch (e) {
|
||||
console.error('error deleting workspace', e);
|
||||
}
|
||||
});
|
||||
return useAtomValue(workspacesAtom);
|
||||
}
|
||||
|
||||
export function useWorkspacesHelper() {
|
||||
return useMemo(
|
||||
() => ({
|
||||
createWorkspacePage: (workspaceId: string, pageId: string) => {
|
||||
const workspace = dataCenter.workspaces.find(
|
||||
const workspaces = useWorkspaces();
|
||||
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
|
||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||
return {
|
||||
createWorkspacePage: useCallback(
|
||||
(workspaceId: string, pageId: string) => {
|
||||
const workspace = workspaces.find(
|
||||
ws => ws.id === workspaceId
|
||||
) as LocalWorkspace;
|
||||
if (workspace && 'blockSuiteWorkspace' in workspace) {
|
||||
@@ -175,9 +28,46 @@ export function useWorkspacesHelper() {
|
||||
throw new Error('cannot create page. blockSuiteWorkspace not found');
|
||||
}
|
||||
},
|
||||
createRemLocalWorkspace,
|
||||
deleteWorkspace,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
[workspaces]
|
||||
),
|
||||
createLocalWorkspace: useCallback(
|
||||
async (name: string): Promise<string> => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
nanoid(),
|
||||
_ => undefined
|
||||
);
|
||||
blockSuiteWorkspace.meta.setName(name);
|
||||
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id,
|
||||
flavour: RemWorkspaceFlavour.LOCAL,
|
||||
},
|
||||
]);
|
||||
return id;
|
||||
},
|
||||
[set]
|
||||
),
|
||||
deleteWorkspace: useCallback(
|
||||
async (workspaceId: string) => {
|
||||
const targetJotaiWorkspace = jotaiWorkspaces.find(
|
||||
ws => ws.id === workspaceId
|
||||
);
|
||||
const targetWorkspace = workspaces.find(ws => ws.id === workspaceId);
|
||||
if (!targetJotaiWorkspace || !targetWorkspace) {
|
||||
throw new Error('page cannot be found');
|
||||
}
|
||||
|
||||
// delete workspace from plugin
|
||||
await WorkspacePlugins[targetWorkspace.flavour].CRUD.delete(
|
||||
// fixme: type casting
|
||||
targetWorkspace as any
|
||||
);
|
||||
// delete workspace from jotai storage
|
||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||
},
|
||||
[jotaiWorkspaces, set, workspaces]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user