refactor: rootWorkspacesMetadataAtom loading logic (#2882)

This commit is contained in:
Alex Yang
2023-06-29 16:48:12 +08:00
parent df7f782e05
commit f7b07f4216
22 changed files with 436 additions and 353 deletions

View File

@@ -9,6 +9,7 @@ import type {
AffineLegacyCloudWorkspace, AffineLegacyCloudWorkspace,
LocalIndexedDBDownloadProvider, LocalIndexedDBDownloadProvider,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { import {
LoadPriority, LoadPriority,
ReleaseType, ReleaseType,
@@ -49,7 +50,6 @@ import {
WorkspaceHeader, WorkspaceHeader,
WorkspaceSettingDetail, WorkspaceSettingDetail,
} from '../shared'; } from '../shared';
import type { WorkspaceAdapter } from '../type';
import { QueryKey } from './fetcher'; import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage); const storage = createJSONStorage(() => localStorage);
@@ -126,7 +126,7 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
console.warn('Legacy cloud is disabled'); console.warn('Legacy cloud is disabled');
return; return;
} }
rootStore.set(rootWorkspacesMetadataAtom, workspaces => await rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter( workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
) )

View File

@@ -6,6 +6,7 @@ import {
PageNotFoundError, PageNotFoundError,
} from '@affine/env/constant'; } from '@affine/env/constant';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace'; import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { import {
LoadPriority, LoadPriority,
ReleaseType, ReleaseType,
@@ -26,7 +27,6 @@ import {
WorkspaceHeader, WorkspaceHeader,
WorkspaceSettingDetail, WorkspaceSettingDetail,
} from '../shared'; } from '../shared';
import type { WorkspaceAdapter } from '../type';
const logger = new DebugLogger('use-create-first-workspace'); const logger = new DebugLogger('use-create-first-workspace');

View File

@@ -1,21 +0,0 @@
import type {
AppEvents,
WorkspaceCRUD,
WorkspaceUISchema,
} from '@affine/env/workspace';
import type {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/env/workspace';
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
Events: Partial<AppEvents>;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
}

View File

@@ -1,5 +1,9 @@
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import type { AppEvents, WorkspaceUISchema } from '@affine/env/workspace'; import type {
AppEvents,
WorkspaceAdapter,
WorkspaceUISchema,
} from '@affine/env/workspace';
import { import {
LoadPriority, LoadPriority,
ReleaseType, ReleaseType,
@@ -8,7 +12,6 @@ import {
import { AffineAdapter } from './affine'; import { AffineAdapter } from './affine';
import { LocalAdapter } from './local'; import { LocalAdapter } from './local';
import type { WorkspaceAdapter } from './type';
const unimplemented = () => { const unimplemented = () => {
throw new Error('Not implemented'); throw new Error('Not implemented');

View File

@@ -4,11 +4,15 @@
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import { initEmptyPage } from '@affine/env/blocksuite'; import { initEmptyPage } from '@affine/env/blocksuite';
import type { LocalIndexedDBBackgroundProvider } from '@affine/env/workspace'; import type {
LocalIndexedDBBackgroundProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import { import {
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom, rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom'; } from '@affine/workspace/atom';
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers'; import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
import { import {
@@ -63,6 +67,13 @@ describe('page mode atom', () => {
describe('currentWorkspace atom', () => { describe('currentWorkspace atom', () => {
test('should be defined', async () => { test('should be defined', async () => {
const store = createStore(); const store = createStore();
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
let id: string; let id: string;
{ {
const workspace = createEmptyBlockSuiteWorkspace( const workspace = createEmptyBlockSuiteWorkspace(
@@ -92,7 +103,7 @@ describe('currentWorkspace atom', () => {
const workspaceId = await WorkspaceAdapters[ const workspaceId = await WorkspaceAdapters[
WorkspaceFlavour.LOCAL WorkspaceFlavour.LOCAL
].CRUD.create(workspace); ].CRUD.create(workspace);
store.set(rootWorkspacesMetadataAtom, [ await store.set(rootWorkspacesMetadataAtom, [
{ {
id: workspaceId, id: workspaceId,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
@@ -103,7 +114,7 @@ describe('currentWorkspace atom', () => {
} }
store.set( store.set(
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
store.get(rootWorkspacesMetadataAtom)[0].id (await store.get(rootWorkspacesMetadataAtom))[0].id
); );
const workspace = await store.get(rootCurrentWorkspaceAtom); const workspace = await store.get(rootCurrentWorkspaceAtom);
expect(workspace).toBeDefined(); expect(workspace).toBeDefined();

View File

@@ -1,84 +1,8 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadataV2 } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomFamily, atomWithStorage } from 'jotai/utils'; import { atomFamily, atomWithStorage } from 'jotai/utils';
import { WorkspaceAdapters } from '../adapters/workspace';
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal'; import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadataV2[] {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
// new workspace should all support sub-doc feature
version: WorkspaceVersion.SubDoc,
} satisfies RootWorkspaceMetadataV2)
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
}
const abortController = new AbortController();
if (!environment.isServer) {
// next tick to make sure the hydration is correct
setTimeout(() => {
setAtom(metadata => {
if (abortController.signal.aborted) return metadata;
if (
metadata.length === 0 &&
localStorage.getItem('is-first-open') === null
) {
localStorage.setItem('is-first-open', 'false');
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
}, 0);
}
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
window.apis?.workspace
.list()
.then(workspaceIDs => {
if (abortController.signal.aborted) return;
const newMetadata = workspaceIDs.map(w => ({
id: w[0],
flavour: WorkspaceFlavour.LOCAL,
version: undefined,
}));
setAtom(metadata => {
return [
...metadata,
...newMetadata.filter(m => !metadata.find(m2 => m2.id === m.id)),
];
});
})
.catch(err => {
console.error(err);
});
}
return () => {
abortController.abort();
};
};
// modal atoms // modal atoms
export const openWorkspacesModalAtom = atom(false); export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false); export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);

View File

@@ -1,6 +1,9 @@
//#region async atoms that to load the real workspace data //#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import type { WorkspaceRegistry } from '@affine/env/workspace'; import type {
WorkspaceAdapter,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { import {
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
@@ -23,7 +26,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
const flavours: string[] = Object.values(WorkspaceAdapters).map( const flavours: string[] = Object.values(WorkspaceAdapters).map(
plugin => plugin.flavour plugin => plugin.flavour
); );
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom) const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom))
.filter( .filter(
workspace => flavours.includes(workspace.flavour) workspace => flavours.includes(workspace.flavour)
// TODO: remove this when we remove the legacy cloud // TODO: remove this when we remove the legacy cloud
@@ -33,7 +36,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
? workspace.flavour !== WorkspaceFlavour.AFFINE ? workspace.flavour !== WorkspaceFlavour.AFFINE
: true : true
); );
if (jotaiWorkspaces.some(meta => meta.version === undefined)) { if (jotaiWorkspaces.some(meta => !('version' in meta))) {
// wait until all workspaces have migrated to v2 // wait until all workspaces have migrated to v2
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject); signal.addEventListener('abort', reject);
@@ -44,12 +47,11 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
} }
const workspaces = await Promise.all( const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => { jotaiWorkspaces.map(workspace => {
const plugin = const adapter = WorkspaceAdapters[
WorkspaceAdapters[ workspace.flavour
workspace.flavour as keyof typeof WorkspaceAdapters ] as WorkspaceAdapter<WorkspaceFlavour>;
]; assertExists(adapter);
assertExists(plugin); const { CRUD } = adapter;
const { CRUD } = plugin;
return CRUD.get(workspace.id).then(workspace => { return CRUD.get(workspace.id).then(workspace => {
if (workspace === null) { if (workspace === null) {
console.warn( console.warn(
@@ -93,7 +95,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>( export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async (get, { signal }) => { async (get, { signal }) => {
const { WorkspaceAdapters } = await import('../adapters/workspace'); const { WorkspaceAdapters } = await import('../adapters/workspace');
const metadata = get(rootWorkspacesMetadataAtom); const metadata = await get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom); const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) { if (targetId === null) {
throw new Error( throw new Error(
@@ -105,7 +107,7 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
throw new Error(`cannot find the workspace with id ${targetId}.`); throw new Error(`cannot find the workspace with id ${targetId}.`);
} }
if (!targetWorkspace.version) { if (!('version' in targetWorkspace)) {
// wait until the workspace has migrated to v2 // wait until the workspace has migrated to v2
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject); signal.addEventListener('abort', reject);
@@ -115,9 +117,12 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
}); });
} }
const workspace = await WorkspaceAdapters[targetWorkspace.flavour].CRUD.get( const adapter = WorkspaceAdapters[
targetWorkspace.id targetWorkspace.flavour
); ] as WorkspaceAdapter<WorkspaceFlavour>;
assertExists(adapter);
const workspace = await adapter.CRUD.get(targetWorkspace.id);
if (!workspace) { if (!workspace) {
throw new Error( throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.` `cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`

View File

@@ -1,10 +1,12 @@
import { migrateToSubdoc } from '@affine/env/blocksuite'; import { migrateToSubdoc } from '@affine/env/blocksuite';
import { isDesktop, isServer } from '@affine/env/constant';
import { setupGlobal } from '@affine/env/global'; import { setupGlobal } from '@affine/env/global';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace'; import type {
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { workspaceAdaptersAtom } from '@affine/workspace/atom';
import { import {
migrateLocalBlobStorage, migrateLocalBlobStorage,
upgradeV1ToV2, upgradeV1ToV2,
@@ -17,19 +19,27 @@ import { WorkspaceAdapters } from '../adapters/workspace';
setupGlobal(); setupGlobal();
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log('Runtime Preset', runtimeConfig); console.log('Runtime Preset', runtimeConfig);
} }
if (runtimeConfig.enablePlugin && !isServer) { if (runtimeConfig.enablePlugin && !environment.isServer) {
import('@affine/copilot'); import('@affine/copilot');
} }
if (!isServer) { if (!environment.isServer) {
import('@affine/bookmark-block'); import('@affine/bookmark-block');
} }
if (!isDesktop && !isServer) { if (!environment.isDesktop && !environment.isServer) {
// Polyfill Electron // Polyfill Electron
const unimplemented = () => { const unimplemented = () => {
throw new Error('AFFiNE Plugin Web will be supported in the future'); throw new Error('AFFiNE Plugin Web will be supported in the future');
@@ -52,65 +62,76 @@ if (!isDesktop && !isServer) {
}); });
} }
rootStore.sub(rootWorkspacesMetadataAtom, () => { if (environment.isBrowser) {
const metadata = rootStore.get(rootWorkspacesMetadataAtom); const value = localStorage.getItem('jotai-workspaces');
metadata.forEach(oldMeta => { if (value) {
if (!oldMeta.version) { try {
const adapter = WorkspaceAdapters[oldMeta.flavour]; const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
assertExists(adapter); const promises: Promise<void>[] = [];
const upgrade = async () => { metadata.forEach(oldMeta => {
const workspace = await adapter.CRUD.get(oldMeta.id); if (!('version' in oldMeta)) {
if (!workspace) { const adapter = WorkspaceAdapters[oldMeta.flavour];
console.warn('cannot find workspace', oldMeta.id); assertExists(adapter);
return; const upgrade = async () => {
} const workspace = await adapter.CRUD.get(oldMeta.id);
if (workspace.flavour !== WorkspaceFlavour.LOCAL) { if (!workspace) {
console.warn('not supported'); console.warn('cannot find workspace', oldMeta.id);
return; return;
} }
const doc = workspace.blockSuiteWorkspace.doc; if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
const provider = createIndexedDBDownloadProvider(workspace.id, doc, { console.warn('not supported');
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness, return;
}) as LocalIndexedDBDownloadProvider; }
provider.sync(); const doc = workspace.blockSuiteWorkspace.doc;
await provider.whenReady; const provider = createIndexedDBDownloadProvider(
const newDoc = migrateToSubdoc(doc); workspace.id,
if (doc === newDoc) { doc,
console.log('doc not changed'); {
rootStore.set(rootWorkspacesMetadataAtom, metadata => awareness:
metadata.map(newMeta => workspace.blockSuiteWorkspace.awarenessStore.awareness,
newMeta.id === oldMeta.id }
? { ) as LocalIndexedDBDownloadProvider;
...newMeta, provider.sync();
version: WorkspaceVersion.SubDoc, await provider.whenReady;
} const newDoc = migrateToSubdoc(doc);
: newMeta if (doc === newDoc) {
) console.log('doc not changed');
); return;
return; }
} const newWorkspace = upgradeV1ToV2(workspace);
const newWorkspace = upgradeV1ToV2(workspace);
const newId = await adapter.CRUD.create( const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace newWorkspace.blockSuiteWorkspace
); );
await adapter.CRUD.delete(workspace as any); await adapter.CRUD.delete(workspace as any);
await migrateLocalBlobStorage(workspace.id, newId); await migrateLocalBlobStorage(workspace.id, newId);
rootStore.set(rootWorkspacesMetadataAtom, metadata => [ };
...metadata
.map(newMeta => (newMeta.id === oldMeta.id ? null : newMeta))
.filter((meta): meta is RootWorkspaceMetadata => !!meta),
{
id: newId,
flavour: oldMeta.flavour,
version: WorkspaceVersion.SubDoc,
},
]);
};
// create a new workspace and push it to metadata // create a new workspace and push it to metadata
upgrade().catch(console.error); promises.push(upgrade());
}
});
Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
window.dispatchEvent(new CustomEvent('migration-done'));
});
} catch (e) {
console.error('error when migrating data', e);
} }
}); }
}); }
declare global {
// global Events
interface WindowEventMap {
'migration-done': CustomEvent;
}
}

View File

@@ -21,6 +21,7 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
import { NoSsr } from '@mui/material';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
@@ -154,10 +155,12 @@ export const RootAppSidebar = ({
hasBackground={!appSettings.disableBlurBackground} hasBackground={!appSettings.disableBlurBackground}
> >
<SidebarContainer> <SidebarContainer>
<WorkspaceSelector <NoSsr>
currentWorkspace={currentWorkspace} <WorkspaceSelector
onClick={onOpenWorkspaceListModal} currentWorkspace={currentWorkspace}
/> onClick={onOpenWorkspaceListModal}
/>
</NoSsr>
<QuickSearchInput <QuickSearchInput
data-testid="slider-bar-quick-search-button" data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal} onClick={onOpenQuickSearchModal}

View File

@@ -5,8 +5,12 @@ import 'fake-indexeddb/auto';
import assert from 'node:assert'; import assert from 'node:assert';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom'; import {
rootCurrentWorkspaceIdAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import type { PageBlockModel } from '@blocksuite/blocks'; import type { PageBlockModel } from '@blocksuite/blocks';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
@@ -22,6 +26,7 @@ import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
import type React from 'react'; import type React from 'react';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { WorkspaceAdapters } from '../../adapters/workspace';
import { workspacesAtom } from '../../atoms'; import { workspacesAtom } from '../../atoms';
import { rootCurrentWorkspaceAtom } from '../../atoms/root'; import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import { BlockSuiteWorkspace } from '../../shared'; import { BlockSuiteWorkspace } from '../../shared';
@@ -53,12 +58,19 @@ beforeEach(() => {
async function getJotaiContext() { async function getJotaiContext() {
const store = createStore(); const store = createStore();
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
const ProviderWrapper: React.FC<React.PropsWithChildren> = const ProviderWrapper: React.FC<React.PropsWithChildren> =
function ProviderWrapper({ children }) { function ProviderWrapper({ children }) {
return <Provider store={store}>{children}</Provider>; return <Provider store={store}>{children}</Provider>;
}; };
const workspaces = await store.get(workspacesAtom); const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(0); expect(workspaces.length).toBe(1);
return { return {
store, store,
ProviderWrapper, ProviderWrapper,
@@ -182,7 +194,13 @@ describe('useWorkspaces', () => {
const { result } = renderHook(() => useWorkspaces(), { const { result } = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
expect(result.current).toEqual([]); expect(result.current).toEqual([
{
id: expect.stringContaining(''),
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: expect.anything(),
},
]);
}); });
test('mutation', async () => { test('mutation', async () => {
@@ -192,20 +210,20 @@ describe('useWorkspaces', () => {
}); });
{ {
const workspaces = await store.get(workspacesAtom); const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(0); expect(workspaces.length).toEqual(1);
} }
await result.current.createLocalWorkspace('test'); await result.current.createLocalWorkspace('test');
{ {
const workspaces = await store.get(workspacesAtom); const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1); expect(workspaces.length).toEqual(2);
} }
const { result: result2 } = renderHook(() => useWorkspaces(), { const { result: result2 } = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper, wrapper: ProviderWrapper,
}); });
expect(result2.current.length).toEqual(1); expect(result2.current.length).toEqual(2);
const firstWorkspace = result2.current[0]; const secondWorkspace = result2.current[1];
expect(firstWorkspace.flavour).toBe('local'); expect(secondWorkspace.flavour).toBe('local');
assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL); assert(secondWorkspace.flavour === WorkspaceFlavour.LOCAL);
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test'); expect(secondWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
}); });
}); });

View File

@@ -19,7 +19,7 @@ export function useToggleWorkspacePublish(
}); });
await mutate(QueryKey.getWorkspaces); await mutate(QueryKey.getWorkspaces);
// fixme: remove force update // fixme: remove force update
rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]); await rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
}, },
[mutate, workspace.id] [mutate, workspace.id]
); );

View File

@@ -25,7 +25,7 @@ export function useTransformWorkspace() {
workspace.blockSuiteWorkspace workspace.blockSuiteWorkspace
); );
await WorkspaceAdapters[from].CRUD.delete(workspace as any); await WorkspaceAdapters[from].CRUD.delete(workspace as any);
set(workspaces => { await set(workspaces => {
const idx = workspaces.findIndex(ws => ws.id === workspace.id); const idx = workspaces.findIndex(ws => ws.id === workspace.id);
workspaces.splice(idx, 1, { workspaces.splice(idx, 1, {
id: newId, id: newId,

View File

@@ -29,7 +29,7 @@ export function useAppHelper() {
addLocalWorkspace: useCallback( addLocalWorkspace: useCallback(
async (workspaceId: string): Promise<string> => { async (workspaceId: string): Promise<string> => {
saveWorkspaceToLocalStorage(workspaceId); saveWorkspaceToLocalStorage(workspaceId);
set(workspaces => [ await set(workspaces => [
...workspaces, ...workspaces,
{ {
id: workspaceId, id: workspaceId,
@@ -50,7 +50,7 @@ export function useAppHelper() {
); );
blockSuiteWorkspace.meta.setName(name); blockSuiteWorkspace.meta.setName(name);
const id = await LocalAdapter.CRUD.create(blockSuiteWorkspace); const id = await LocalAdapter.CRUD.create(blockSuiteWorkspace);
set(workspaces => [ await set(workspaces => [
...workspaces, ...workspaces,
{ {
id, id,
@@ -79,7 +79,7 @@ export function useAppHelper() {
targetWorkspace as any targetWorkspace as any
); );
// delete workspace from jotai storage // delete workspace from jotai storage
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId)); await set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
}, },
[jotaiWorkspaces, set, workspaces] [jotaiWorkspaces, set, workspaces]
), ),

View File

@@ -8,13 +8,9 @@ import {
ToolContainer, ToolContainer,
WorkspaceFallback, WorkspaceFallback,
} from '@affine/component/workspace'; } from '@affine/component/workspace';
import { DebugLogger } from '@affine/debug';
import { initEmptyPage, initPageWithPreloading } from '@affine/env/blocksuite'; import { initEmptyPage, initPageWithPreloading } from '@affine/env/blocksuite';
import { DEFAULT_HELLO_WORLD_PAGE_ID, isDesktop } from '@affine/env/constant'; import { DEFAULT_HELLO_WORLD_PAGE_ID, isDesktop } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { setUpLanguage, useI18N } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
import { import {
rootCurrentPageIdAtom, rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
@@ -33,7 +29,6 @@ import {
useSensors, useSensors,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper'; import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -144,12 +139,6 @@ export const Setting: FC = () => {
); );
}; };
const logger = new DebugLogger('workspace-layout');
const affineGlobalChannel = createAffineGlobalChannel(
WorkspaceAdapters[WorkspaceFlavour.AFFINE].CRUD
);
export const AllWorkspaceContext = ({ export const AllWorkspaceContext = ({
children, children,
}: PropsWithChildren): ReactElement => { }: PropsWithChildren): ReactElement => {
@@ -226,14 +215,6 @@ export const CurrentWorkspaceContext = ({
export const WorkspaceLayout: FC<PropsWithChildren> = export const WorkspaceLayout: FC<PropsWithChildren> =
function WorkspacesSuspense({ children }) { function WorkspacesSuspense({ children }) {
const i18n = useI18N();
useEffect(() => {
document.documentElement.lang = i18n.language;
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n)?.catch(error => {
console.error(error);
});
}, [i18n]);
useTrackRouterHistoryEffect(); useTrackRouterHistoryEffect();
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom); const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom); const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
@@ -241,67 +222,6 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
() => jotaiWorkspaces.find(x => x.id === currentWorkspaceId), () => jotaiWorkspaces.find(x => x.id === currentWorkspaceId),
[currentWorkspaceId, jotaiWorkspaces] [currentWorkspaceId, jotaiWorkspaces]
); );
const set = useSetAtom(rootWorkspacesMetadataAtom);
useEffect(() => {
logger.info('mount');
const controller = new AbortController();
const lists = Object.values(WorkspaceAdapters)
.sort((a, b) => a.loadPriority - b.loadPriority)
.map(({ CRUD }) => CRUD.list);
async function fetch() {
const jotaiWorkspaces = rootStore.get(rootWorkspacesMetadataAtom);
const items = [];
for (const list of lists) {
try {
const item = await list();
if (jotaiWorkspaces.length) {
item.sort((a, b) => {
return (
jotaiWorkspaces.findIndex(x => x.id === a.id) -
jotaiWorkspaces.findIndex(x => x.id === b.id)
);
});
}
items.push(
...item.map(x => ({
id: x.id,
flavour: x.flavour,
version: undefined,
}))
);
} catch (e) {
logger.error('list data error:', e);
}
}
if (controller.signal.aborted) {
return;
}
set([...items]);
logger.info('mount first data:', items);
}
fetch().catch(e => {
logger.error('fetch error:', e);
});
return () => {
controller.abort();
logger.info('unmount');
};
}, [set]);
useEffect(() => {
const flavour = jotaiWorkspaces.find(
x => x.id === currentWorkspaceId
)?.flavour;
if (flavour === WorkspaceFlavour.AFFINE) {
affineGlobalChannel.connect();
return () => {
affineGlobalChannel.disconnect();
};
}
return;
}, [currentWorkspaceId, jotaiWorkspaces]);
const Provider = const Provider =
(meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider; (meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider;
@@ -335,31 +255,35 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const router = useRouter(); const router = useRouter();
const { jumpToPage } = useRouterHelper(router); const { jumpToPage } = useRouterHelper(router);
// fixme(himself65):
// we should move the page into jotai atom since it's an async value
//#region init workspace //#region init workspace
if (currentWorkspace.blockSuiteWorkspace.isEmpty) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (currentWorkspace.blockSuiteWorkspace.meta._proxy.isEmpty !== true) {
// this is a new workspace, so we should redirect to the new page // this is a new workspace, so we should redirect to the new page
const pageId = DEFAULT_HELLO_WORLD_PAGE_ID; const pageId = DEFAULT_HELLO_WORLD_PAGE_ID;
const page = currentWorkspace.blockSuiteWorkspace.createPage({ if (currentWorkspace.blockSuiteWorkspace.getPage(pageId) === null) {
id: pageId, const page = currentWorkspace.blockSuiteWorkspace.createPage({
}); id: pageId,
assertEquals(page.id, pageId);
if (runtimeConfig.enablePreloading) {
initPageWithPreloading(page).catch(error => {
console.error('import error:', error);
});
} else {
initEmptyPage(page).catch(error => {
console.error('init empty page error', error);
});
}
if (!router.query.pageId) {
setCurrentPageId(pageId);
jumpToPage(currentWorkspace.id, pageId).catch(err => {
console.error(err);
}); });
assertEquals(page.id, pageId);
if (runtimeConfig.enablePreloading) {
initPageWithPreloading(page).catch(error => {
console.error('import error:', error);
});
} else {
initEmptyPage(page).catch(error => {
console.error('init empty page error', error);
});
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
currentWorkspace.blockSuiteWorkspace.meta._proxy.isEmpty = false;
if (!router.query.pageId) {
setCurrentPageId(pageId);
jumpToPage(currentWorkspace.id, pageId).catch(err => {
console.error(err);
});
}
} }
} }
//#endregion //#endregion

View File

@@ -5,14 +5,14 @@ import '../bootstrap';
import { AffineContext } from '@affine/component/context'; import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace'; import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, I18nextProvider } from '@affine/i18n'; import { createI18n, I18nextProvider, setUpLanguage } from '@affine/i18n';
import type { EmotionCache } from '@emotion/cache'; import type { EmotionCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { PropsWithChildren, ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import React, { lazy, Suspense } from 'react'; import React, { lazy, Suspense, useEffect } from 'react';
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary'; import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
import { MessageCenter } from '../components/pure/message-center'; import { MessageCenter } from '../components/pure/message-center';
@@ -49,6 +49,13 @@ const App = function App({
}: AppPropsWithLayout & { }: AppPropsWithLayout & {
emotionCache?: EmotionCache; emotionCache?: EmotionCache;
}) { }) {
useEffect(() => {
document.documentElement.lang = i18n.language;
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n)?.catch(error => {
console.error(error);
});
}, []);
const getLayout = Component.getLayout || EmptyLayout; const getLayout = Component.getLayout || EmptyLayout;
return ( return (
@@ -56,20 +63,20 @@ const App = function App({
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<MessageCenter /> <MessageCenter />
<AffineErrorBoundary router={useRouter()}> <AffineErrorBoundary router={useRouter()}>
<Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}> <AffineContext>
<AffineContext> <Head>
<Head> <title>AFFiNE</title>
<title>AFFiNE</title> <meta
<meta name="viewport"
name="viewport" content="initial-scale=1, width=device-width"
content="initial-scale=1, width=device-width" />
/> </Head>
</Head> <DebugProvider>
<DebugProvider> <Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}>
{getLayout(<Component {...pageProps} />)} {getLayout(<Component {...pageProps} />)}
</DebugProvider> </Suspense>
</AffineContext> </DebugProvider>
</Suspense> </AffineContext>
</AffineErrorBoundary> </AffineErrorBoundary>
</I18nextProvider> </I18nextProvider>
</CacheProvider> </CacheProvider>

View File

@@ -119,14 +119,13 @@ const MigrationInner = () => {
const ids = useAtomValue(workspaceIdsAtom); const ids = useAtomValue(workspaceIdsAtom);
const [id, setId] = useAtom(targetIdAtom); const [id, setId] = useAtom(targetIdAtom);
const router = useRouter(); const router = useRouter();
const onWriteIntoProduction = useCallback(() => { const onWriteIntoProduction = useCallback(async () => {
assertExists(id); assertExists(id);
const metadata: RootWorkspaceMetadataV1 = { const metadata: RootWorkspaceMetadataV1 = {
id, id,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
version: undefined,
}; };
rootStore.set(rootWorkspacesMetadataAtom, [metadata]); await rootStore.set(rootWorkspacesMetadataAtom, [metadata]);
router.push('/').catch(console.error); router.push('/').catch(console.error);
}, [id, router]); }, [id, router]);
const writeIntoProductionNode = id && ( const writeIntoProductionNode = id && (

View File

@@ -116,11 +116,11 @@ export const AllWorkspaceModals = (): ReactElement => {
(activeId, overId) => { (activeId, overId) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId); const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId); const newIndex = workspaces.findIndex(w => w.id === overId);
transition(() => transition(() => {
setWorkspaces(workspaces => setWorkspaces(workspaces =>
arrayMove(workspaces, oldIndex, newIndex) arrayMove(workspaces, oldIndex, newIndex)
) ).catch(console.error);
); });
}, },
[setWorkspaces, workspaces] [setWorkspaces, workspaces]
)} )}

View File

@@ -206,3 +206,14 @@ export interface AppEvents {
// request to revoke access to workspace plugin // request to revoke access to workspace plugin
'workspace:revoke': () => Promise<void>; 'workspace:revoke': () => Promise<void>;
} }
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// The Adapter will be loaded according to the priority
loadPriority: LoadPriority;
Events: Partial<AppEvents>;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
}

View File

@@ -52,7 +52,7 @@ export function createAffineGlobalChannel(
// If the workspace is not in the current workspace list, remove it // If the workspace is not in the current workspace list, remove it
if (workspaceIndex === -1) { if (workspaceIndex === -1) {
rootStore.set(rootWorkspacesMetadataAtom, workspaces => { await rootStore.set(rootWorkspacesMetadataAtom, workspaces => {
const idx = workspaces.findIndex(workspace => workspace.id === id); const idx = workspaces.findIndex(workspace => workspace.id === id);
workspaces.splice(idx, 1); workspaces.splice(idx, 1);
return [...workspaces]; return [...workspaces];

View File

@@ -1,28 +1,54 @@
import { isBrowser } from '@affine/env/constant'; import type { WorkspaceAdapter } from '@affine/env/workspace';
import type { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { WorkspaceVersion } from '@affine/env/workspace';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import Router from 'next/router'; import Router from 'next/router';
import { z } from 'zod';
export type RootWorkspaceMetadataV2 = { const rootWorkspaceMetadataV1Schema = z.object({
id: string; id: z.string(),
flavour: WorkspaceFlavour; flavour: z.nativeEnum(WorkspaceFlavour),
version: WorkspaceVersion; });
};
export type RootWorkspaceMetadataV1 = { const rootWorkspaceMetadataV2Schema = rootWorkspaceMetadataV1Schema.extend({
id: string; version: z.nativeEnum(WorkspaceVersion),
flavour: WorkspaceFlavour; });
// force type check
version: undefined; const rootWorkspaceMetadataArraySchema = z.array(
}; z.union([rootWorkspaceMetadataV1Schema, rootWorkspaceMetadataV2Schema])
);
export type RootWorkspaceMetadataV2 = z.infer<
typeof rootWorkspaceMetadataV2Schema
>;
export type RootWorkspaceMetadataV1 = z.infer<
typeof rootWorkspaceMetadataV1Schema
>;
export type RootWorkspaceMetadata = export type RootWorkspaceMetadata =
| RootWorkspaceMetadataV1 | RootWorkspaceMetadataV1
| RootWorkspaceMetadataV2; | RootWorkspaceMetadataV2;
export const workspaceAdaptersAtom = atom<
Record<
WorkspaceFlavour,
Pick<
WorkspaceAdapter<WorkspaceFlavour>,
'CRUD' | 'Events' | 'flavour' | 'loadPriority'
>
>
>(
null as unknown as Record<
WorkspaceFlavour,
Pick<
WorkspaceAdapter<WorkspaceFlavour>,
'CRUD' | 'Events' | 'flavour' | 'loadPriority'
>
>
);
// #region root atoms // #region root atoms
// root primitive atom that stores the necessary data for the whole app // root primitive atom that stores the necessary data for the whole app
// be careful when you use this atom, // be careful when you use this atom,
@@ -32,20 +58,170 @@ export type RootWorkspaceMetadata =
* this atom stores the metadata of all workspaces, * this atom stores the metadata of all workspaces,
* which is `id` and `flavor`, that is enough to load the real workspace data * which is `id` and `flavor`, that is enough to load the real workspace data
*/ */
export const rootWorkspacesMetadataAtom = atomWithStorage< const METADATA_STORAGE_KEY = 'jotai-workspaces';
RootWorkspaceMetadata[] const rootWorkspacesMetadataPrimitiveAtom = atom<
RootWorkspaceMetadata[] | null
>(null);
const rootWorkspacesMetadataPromiseAtom = atom<
Promise<RootWorkspaceMetadata[]>
>(async (get, { signal }) => {
const WorkspaceAdapters = get(workspaceAdaptersAtom);
assertExists(WorkspaceAdapters, 'workspace adapter should be defined');
const maybeMetadata = get(rootWorkspacesMetadataPrimitiveAtom);
if (maybeMetadata !== null) {
return maybeMetadata;
}
const createFirst = (): RootWorkspaceMetadataV2[] => {
if (signal.aborted) {
return [];
}
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
// new workspace should all support sub-doc feature
version: WorkspaceVersion.SubDoc,
} satisfies RootWorkspaceMetadataV2)
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
if (environment.isServer) {
// return a promise in SSR to avoid the hydration mismatch
return Promise.resolve([]);
} else {
const metadata: RootWorkspaceMetadata[] = [];
// fixme(himself65): we might not need step 1
// step 1: try load metadata from localStorage
{
// don't change this key,
// otherwise it will cause the data loss in the production
const primitiveMetadata = localStorage.getItem(METADATA_STORAGE_KEY);
if (primitiveMetadata) {
try {
const items = JSON.parse(primitiveMetadata) as z.infer<
typeof rootWorkspaceMetadataArraySchema
>;
rootWorkspaceMetadataArraySchema.parse(items);
metadata.push(...items);
} catch (e) {
console.error('cannot parse worksapce', e);
}
}
// migration step, only data in `METADATA_STORAGE_KEY` will be migrated
if (metadata.some(meta => !('version' in meta))) {
await new Promise<void>((resolve, reject) => {
signal.addEventListener('abort', () => reject(), { once: true });
window.addEventListener('migration-done', () => resolve(), {
once: true,
});
});
}
}
// step 2: fetch from adapters
{
const lists = Object.values(WorkspaceAdapters)
.sort((a, b) => a.loadPriority - b.loadPriority)
.map(({ CRUD }) => CRUD.list);
for (const list of lists) {
try {
const item = await list();
if (metadata.length) {
item.sort((a, b) => {
return (
metadata.findIndex(x => x.id === a.id) -
metadata.findIndex(x => x.id === b.id)
);
});
}
metadata.push(
...item.map(x => ({
id: x.id,
flavour: x.flavour,
version: WorkspaceVersion.SubDoc,
}))
);
} catch (e) {
console.error('list data error:', e);
}
}
}
// step 3: create initial workspaces
{
if (
metadata.length === 0 &&
localStorage.getItem('is-first-open') === null
) {
metadata.push(...createFirst());
console.info('create first workspace', metadata);
localStorage.setItem('is-first-open', 'false');
}
}
const metadataMap = new Map(metadata.map(x => [x.id, x]));
return Array.from(metadataMap.values());
}
});
type SetStateAction<Value> = Value | ((prev: Value) => Value);
export const rootWorkspacesMetadataAtom = atom<
Promise<RootWorkspaceMetadata[]>,
[SetStateAction<RootWorkspaceMetadata[]>],
Promise<RootWorkspaceMetadata[]>
>( >(
// don't change this key, async get => {
// otherwise it will cause the data loss in the production if (environment.isServer) {
'jotai-workspaces', return Promise.resolve([]);
[] }
const maybeMetadata = get(rootWorkspacesMetadataPrimitiveAtom);
if (maybeMetadata !== null) {
return maybeMetadata;
}
return get(rootWorkspacesMetadataPromiseAtom);
},
async (get, set, action) => {
// get metadata
let metadata: RootWorkspaceMetadata[];
const maybeMetadata = get(rootWorkspacesMetadataPrimitiveAtom);
if (maybeMetadata !== null) {
metadata = maybeMetadata;
} else {
metadata = await get(rootWorkspacesMetadataPromiseAtom);
}
// update metadata
if (typeof action === 'function') {
metadata = action(metadata);
} else {
metadata = action;
}
const metadataMap = new Map(metadata.map(x => [x.id, x]));
metadata = Array.from(metadataMap.values());
// write back to localStorage
rootWorkspaceMetadataArraySchema.parse(metadata);
localStorage.setItem(METADATA_STORAGE_KEY, JSON.stringify(metadata));
set(rootWorkspacesMetadataPrimitiveAtom, metadata);
return metadata;
}
); );
// two more atoms to store the current workspace and page // two more atoms to store the current workspace and page
export const rootCurrentWorkspaceIdAtom = atom<string | null>(null); export const rootCurrentWorkspaceIdAtom = atom<string | null>(null);
rootCurrentWorkspaceIdAtom.onMount = set => { rootCurrentWorkspaceIdAtom.onMount = set => {
if (isBrowser) { if (environment.isBrowser) {
const callback = (url: string) => { const callback = (url: string) => {
const value = url.split('/')[2]; const value = url.split('/')[2];
if (value) { if (value) {
@@ -67,7 +243,7 @@ rootCurrentWorkspaceIdAtom.onMount = set => {
export const rootCurrentPageIdAtom = atom<string | null>(null); export const rootCurrentPageIdAtom = atom<string | null>(null);
rootCurrentPageIdAtom.onMount = set => { rootCurrentPageIdAtom.onMount = set => {
if (isBrowser) { if (environment.isBrowser) {
const callback = (url: string) => { const callback = (url: string) => {
const value = url.split('/')[3]; const value = url.split('/')[3];
if (value) { if (value) {

View File

@@ -26,7 +26,7 @@ export function upgradeV1ToV2(oldWorkspace: LocalWorkspace): LocalWorkspace {
} }
}); });
}); });
console.log(newBlockSuiteWorkspace.doc.toJSON()); console.log('migration result', newBlockSuiteWorkspace.doc.toJSON());
return { return {
blockSuiteWorkspace: newBlockSuiteWorkspace, blockSuiteWorkspace: newBlockSuiteWorkspace,

View File

@@ -20,9 +20,11 @@ import { createAffineBlobStorage } from './blob';
import { createSQLiteStorage } from './blob/sqlite-blob-storage'; import { createSQLiteStorage } from './blob/sqlite-blob-storage';
export function cleanupWorkspace(flavour: WorkspaceFlavour) { export function cleanupWorkspace(flavour: WorkspaceFlavour) {
rootStore.set(rootWorkspacesMetadataAtom, metas => rootStore
metas.filter(meta => meta.flavour !== flavour) .set(rootWorkspacesMetadataAtom, metas =>
); metas.filter(meta => meta.flavour !== flavour)
)
.catch(console.error);
} }
function setEditorFlags(workspace: Workspace) { function setEditorFlags(workspace: Workspace) {