mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
refactor: rootWorkspacesMetadataAtom loading logic (#2882)
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}.`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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]
|
||||||
)}
|
)}
|
||||||
|
|||||||
11
packages/env/src/workspace.ts
vendored
11
packages/env/src/workspace.ts
vendored
@@ -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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user