mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
revert: loadWorkspace unexpected behavior (#1172)
This commit is contained in:
@@ -2,13 +2,11 @@ import { ThemeMode } from '../types';
|
||||
|
||||
export class LocalStorageThemeHelper {
|
||||
name = 'Affine-theme-mode';
|
||||
callback = new Set<() => void>();
|
||||
get = (): ThemeMode | null => {
|
||||
return localStorage.getItem(this.name) as ThemeMode | null;
|
||||
};
|
||||
set = (mode: ThemeMode) => {
|
||||
localStorage.setItem(this.name, mode);
|
||||
this.callback.forEach(cb => cb());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,17 @@ const _initializeDataCenter = () => {
|
||||
return () => {
|
||||
if (!_dataCenterInstance) {
|
||||
_dataCenterInstance = DataCenter.init();
|
||||
_dataCenterInstance.then(dc => {
|
||||
try {
|
||||
if (window) {
|
||||
(window as any).dc = dc;
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return dc;
|
||||
});
|
||||
}
|
||||
|
||||
return _dataCenterInstance;
|
||||
@@ -14,7 +25,7 @@ const _initializeDataCenter = () => {
|
||||
|
||||
export const getDataCenter = _initializeDataCenter();
|
||||
|
||||
export { DataCenter };
|
||||
export type { DataCenter };
|
||||
export * from './message';
|
||||
export { AffineProvider } from './provider/affine';
|
||||
export * from './provider/affine/apis';
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "@affine/store",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@affine/datacenter": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@blocksuite/blocks": "0.4.1",
|
||||
"@blocksuite/editor": "0.4.1",
|
||||
"@blocksuite/global": "0.4.1",
|
||||
"@blocksuite/react": "0.4.1",
|
||||
"@blocksuite/store": "0.4.1",
|
||||
"lit": "^2.6.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"swr": "^2.0.3",
|
||||
"yjs": "^13.5.46",
|
||||
"zustand": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11"
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { BlockHub } from '@blocksuite/blocks';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import { Page, Workspace } from '@blocksuite/store';
|
||||
|
||||
import { GlobalActionsCreator } from '..';
|
||||
|
||||
export interface BlockSuiteState {
|
||||
currentWorkspace: Workspace | null;
|
||||
editor: EditorContainer | null;
|
||||
currentPage: Page | null;
|
||||
blockHub: BlockHub | null;
|
||||
}
|
||||
|
||||
export const createBlockSuiteState = (): BlockSuiteState => ({
|
||||
currentWorkspace: null,
|
||||
currentPage: null,
|
||||
blockHub: null,
|
||||
editor: null,
|
||||
});
|
||||
|
||||
export interface BlockSuiteActions {
|
||||
loadPage: (pageId: string) => void;
|
||||
setEditor: (editor: EditorContainer) => void;
|
||||
setWorkspace: (workspace: Workspace) => void;
|
||||
setBlockHub: (blockHub: BlockHub) => void;
|
||||
}
|
||||
|
||||
export const createBlockSuiteActions: GlobalActionsCreator<
|
||||
BlockSuiteActions
|
||||
> = (set, get) => ({
|
||||
setWorkspace: workspace => {
|
||||
set({
|
||||
currentWorkspace: workspace,
|
||||
});
|
||||
},
|
||||
setEditor: editor => {
|
||||
set({
|
||||
editor,
|
||||
});
|
||||
},
|
||||
loadPage: pageId => {
|
||||
const { currentWorkspace } = get();
|
||||
if (currentWorkspace === null) {
|
||||
console.warn('currentWorkspace is null');
|
||||
return;
|
||||
}
|
||||
const page = currentWorkspace.getPage(pageId);
|
||||
if (page === null) {
|
||||
console.warn('cannot find page ', pageId);
|
||||
return;
|
||||
}
|
||||
set({
|
||||
currentPage: page,
|
||||
});
|
||||
},
|
||||
setBlockHub: blockHub => {
|
||||
set({
|
||||
blockHub,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,225 +0,0 @@
|
||||
import { getDataCenter, WorkspaceUnit } from '@affine/datacenter';
|
||||
import { DataCenter } from '@affine/datacenter';
|
||||
import { Disposable, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { PageMeta as StorePageMeta } from '@blocksuite/store';
|
||||
import React, { useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
|
||||
|
||||
export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
|
||||
return dataCenter.createWorkspace({
|
||||
name: DEFAULT_WORKSPACE_NAME,
|
||||
});
|
||||
};
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var dataCenterPromise: Promise<DataCenter>;
|
||||
// eslint-disable-next-line no-var
|
||||
var dc: DataCenter;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
let dataCenterPromise: Promise<DataCenter> = null!;
|
||||
if (!globalThis.dataCenterPromise) {
|
||||
dataCenterPromise = getDataCenter();
|
||||
dataCenterPromise.then(dataCenter => {
|
||||
globalThis.dc = dataCenter;
|
||||
return dataCenter;
|
||||
});
|
||||
} else {
|
||||
dataCenterPromise = globalThis.dataCenterPromise;
|
||||
}
|
||||
|
||||
export { dataCenterPromise };
|
||||
|
||||
export interface PageMeta extends StorePageMeta {
|
||||
favorite: boolean;
|
||||
trash: boolean;
|
||||
trashDate: number;
|
||||
updatedDate: number;
|
||||
mode: 'edgeless' | 'page';
|
||||
}
|
||||
|
||||
import { GlobalActionsCreator, useGlobalStateApi } from '..';
|
||||
|
||||
export type DataCenterState = {
|
||||
currentDataCenterWorkspace: WorkspaceUnit | null;
|
||||
dataCenterPageList: PageMeta[];
|
||||
blobDataSynced: boolean;
|
||||
};
|
||||
|
||||
export type DataCenterActions = {
|
||||
loadWorkspace: (
|
||||
workspaceId: string,
|
||||
signal?: AbortSignal
|
||||
) => Promise<WorkspaceUnit | null>;
|
||||
};
|
||||
|
||||
export const createDataCenterState = (): DataCenterState => ({
|
||||
currentDataCenterWorkspace: null,
|
||||
dataCenterPageList: [],
|
||||
blobDataSynced: false,
|
||||
});
|
||||
|
||||
export const createDataCenterActions: GlobalActionsCreator<
|
||||
DataCenterActions
|
||||
> = (set, get) => ({
|
||||
loadWorkspace: async (workspaceId, signal) => {
|
||||
const dataCenter = await dataCenterPromise;
|
||||
const { currentDataCenterWorkspace } = get();
|
||||
if (!dataCenter.workspaces.find(v => v.id.toString() === workspaceId)) {
|
||||
return null;
|
||||
}
|
||||
if (workspaceId === currentDataCenterWorkspace?.id) {
|
||||
return currentDataCenterWorkspace;
|
||||
}
|
||||
const workspace = await dataCenter.loadWorkspace(workspaceId);
|
||||
|
||||
if (!workspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
// do not update state if aborted
|
||||
return null;
|
||||
}
|
||||
|
||||
let isOwner;
|
||||
if (workspace.provider === 'local') {
|
||||
// isOwner is useful only in the cloud
|
||||
isOwner = true;
|
||||
} else {
|
||||
const userInfo = get().user; // We must ensure workspace.owner exists, then ensure id same.
|
||||
isOwner = userInfo?.id === workspace.owner?.id;
|
||||
}
|
||||
|
||||
const pageList =
|
||||
(workspace.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
|
||||
if (workspace.blocksuiteWorkspace) {
|
||||
set({
|
||||
currentWorkspace: workspace.blocksuiteWorkspace,
|
||||
});
|
||||
}
|
||||
|
||||
set({
|
||||
isOwner,
|
||||
currentDataCenterWorkspace: workspace,
|
||||
dataCenterPageList: pageList,
|
||||
});
|
||||
|
||||
return workspace;
|
||||
},
|
||||
});
|
||||
|
||||
export function useDataCenter() {
|
||||
const { data } = useSWR<DataCenter>(['datacenter'], {
|
||||
fallbackData: DataCenter.initEmpty(),
|
||||
});
|
||||
return data as DataCenter;
|
||||
}
|
||||
|
||||
export function useDataCenterWorkspace(
|
||||
workspaceId: string | null
|
||||
): WorkspaceUnit | null {
|
||||
const { data } = useSWR<WorkspaceUnit | null>(['datacenter', workspaceId], {
|
||||
fallbackData: null,
|
||||
});
|
||||
return data ?? null;
|
||||
}
|
||||
|
||||
export function useDataCenterPublicWorkspace(workspaceId: string | null) {
|
||||
const { data, error } = useSWR<WorkspaceUnit | null>(
|
||||
['datacenter', workspaceId, 'public'],
|
||||
{
|
||||
fallbackData: null,
|
||||
}
|
||||
);
|
||||
return {
|
||||
workspace: data ?? null,
|
||||
error,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function DataCenterPreloader({ children }: React.PropsWithChildren) {
|
||||
const api = useGlobalStateApi();
|
||||
|
||||
// init user info from datacenter
|
||||
useEffect(() => {
|
||||
dataCenterPromise.then(async dataCenter => {
|
||||
const user = await dataCenter.getUserInfo();
|
||||
if (!api.getState().user) {
|
||||
api.setState({ user });
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
//# region effect for updating workspace page list
|
||||
useEffect(() => {
|
||||
return api.subscribe(
|
||||
store => store.currentDataCenterWorkspace,
|
||||
currentWorkspace => {
|
||||
const disposableGroup = new DisposableGroup();
|
||||
disposableGroup.add(
|
||||
currentWorkspace?.blocksuiteWorkspace?.meta.pagesUpdated.on(() => {
|
||||
if (
|
||||
Array.isArray(
|
||||
currentWorkspace.blocksuiteWorkspace?.meta.pageMetas
|
||||
)
|
||||
) {
|
||||
api.setState({
|
||||
dataCenterPageList: currentWorkspace.blocksuiteWorkspace?.meta
|
||||
.pageMetas as PageMeta[],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
disposableGroup.dispose();
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [api]);
|
||||
//# endregion
|
||||
//# region effect for blobDataSynced
|
||||
useEffect(
|
||||
() =>
|
||||
api.subscribe(
|
||||
store => store.currentDataCenterWorkspace,
|
||||
workspace => {
|
||||
if (!workspace?.blocksuiteWorkspace) {
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const blocksuiteWorkspace = workspace.blocksuiteWorkspace;
|
||||
let syncChangeDisposable: Disposable | undefined;
|
||||
async function subscribe() {
|
||||
const blobStorage = await blocksuiteWorkspace.blobs;
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
syncChangeDisposable =
|
||||
blobStorage?.signals.onBlobSyncStateChange.on(() => {
|
||||
if (controller.signal.aborted) {
|
||||
syncChangeDisposable?.dispose();
|
||||
return;
|
||||
} else {
|
||||
api.setState({
|
||||
blobDataSynced: blobStorage?.uploading,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
subscribe();
|
||||
return () => {
|
||||
controller.abort();
|
||||
syncChangeDisposable?.dispose();
|
||||
};
|
||||
}
|
||||
),
|
||||
[api]
|
||||
);
|
||||
//# endregion
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import type React from 'react';
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { preload, SWRConfig, SWRConfiguration } from 'swr';
|
||||
import { createStore, StateCreator, useStore } from 'zustand';
|
||||
import { combine, subscribeWithSelector } from 'zustand/middleware';
|
||||
import type { UseBoundStore } from 'zustand/react';
|
||||
|
||||
import {
|
||||
BlockSuiteActions,
|
||||
BlockSuiteState,
|
||||
createBlockSuiteActions,
|
||||
createBlockSuiteState,
|
||||
} from './blocksuite';
|
||||
import {
|
||||
createDataCenterActions,
|
||||
createDataCenterState,
|
||||
createDefaultWorkspace,
|
||||
DataCenterActions,
|
||||
dataCenterPromise,
|
||||
DataCenterState,
|
||||
} from './datacenter';
|
||||
import {
|
||||
createUserActions,
|
||||
createUserState,
|
||||
UserActions,
|
||||
UserState,
|
||||
} from './user';
|
||||
|
||||
export type GlobalActionsCreator<Actions, Store = GlobalState> = StateCreator<
|
||||
Store,
|
||||
[['zustand/subscribeWithSelector', unknown]],
|
||||
[],
|
||||
Actions
|
||||
>;
|
||||
|
||||
export interface GlobalState
|
||||
extends BlockSuiteState,
|
||||
UserState,
|
||||
DataCenterState {}
|
||||
|
||||
export interface GlobalActions
|
||||
extends BlockSuiteActions,
|
||||
UserActions,
|
||||
DataCenterActions {}
|
||||
|
||||
const create = () =>
|
||||
createStore(
|
||||
subscribeWithSelector(
|
||||
combine<GlobalState, GlobalActions>(
|
||||
{
|
||||
...createBlockSuiteState(),
|
||||
...createUserState(),
|
||||
...createDataCenterState(),
|
||||
},
|
||||
/* deepscan-disable TOO_MANY_ARGS */
|
||||
(set, get, api) => ({
|
||||
...createBlockSuiteActions(set, get, api),
|
||||
...createUserActions(set, get, api),
|
||||
...createDataCenterActions(set, get, api),
|
||||
})
|
||||
/* deepscan-enable TOO_MANY_ARGS */
|
||||
)
|
||||
)
|
||||
);
|
||||
type Store = ReturnType<typeof create>;
|
||||
|
||||
const GlobalStateContext = createContext<Store | null>(null);
|
||||
|
||||
export const useGlobalStateApi = () => {
|
||||
const api = useContext(GlobalStateContext);
|
||||
if (!api) {
|
||||
throw new Error('cannot find modal context');
|
||||
}
|
||||
return api;
|
||||
};
|
||||
|
||||
export const useGlobalState: UseBoundStore<Store> = ((
|
||||
selector: Parameters<UseBoundStore<Store>>[0],
|
||||
equals: Parameters<UseBoundStore<Store>>[1]
|
||||
) => {
|
||||
const api = useGlobalStateApi();
|
||||
return useStore(api, selector, equals);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as any;
|
||||
|
||||
export type DataKey =
|
||||
| ['datacenter', string | null, 'public' | undefined]
|
||||
| ['datacenter'];
|
||||
|
||||
const swrFetcher = async (keys: DataKey) => {
|
||||
assertEquals(keys[0], 'datacenter');
|
||||
if (keys.length === 1) {
|
||||
return await dataCenterPromise.then(async dataCenter => {
|
||||
if (dataCenter.workspaces.length === 0) {
|
||||
await createDefaultWorkspace(dataCenter);
|
||||
}
|
||||
return dataCenter;
|
||||
});
|
||||
} else {
|
||||
if (keys[1] === null) {
|
||||
return null;
|
||||
}
|
||||
const dataCenter = await dataCenterPromise;
|
||||
if (keys[2] === 'public') {
|
||||
return dataCenter.loadPublicWorkspace(keys[1]);
|
||||
}
|
||||
return dataCenter.loadWorkspace(keys[1]);
|
||||
}
|
||||
};
|
||||
|
||||
preload(['datacenter'], swrFetcher);
|
||||
|
||||
const swrConfig: SWRConfiguration = {
|
||||
fetcher: swrFetcher,
|
||||
suspense: true,
|
||||
};
|
||||
|
||||
export const GlobalAppProvider: React.FC<React.PropsWithChildren> =
|
||||
function ModelProvider({ children }) {
|
||||
return (
|
||||
<SWRConfig value={swrConfig}>
|
||||
<GlobalStateContext.Provider value={useMemo(() => create(), [])}>
|
||||
{children}
|
||||
</GlobalStateContext.Provider>
|
||||
</SWRConfig>
|
||||
);
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { User } from '@affine/datacenter';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
import { GlobalActionsCreator } from '..';
|
||||
import { dataCenterPromise } from '../datacenter';
|
||||
|
||||
export interface UserState {
|
||||
user: User | null;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export interface UserActions {
|
||||
login: () => Promise<User | null>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const createUserState = (): UserState => ({
|
||||
// initialized in DataCenterLoader (restore from localStorage)
|
||||
user: null,
|
||||
isOwner: false,
|
||||
});
|
||||
|
||||
const logger = new DebugLogger('store:user');
|
||||
|
||||
export const createUserActions: GlobalActionsCreator<UserActions> = (
|
||||
set,
|
||||
get
|
||||
) => {
|
||||
return {
|
||||
login: async () => {
|
||||
const { currentDataCenterWorkspace: workspace } = get();
|
||||
const dataCenter = await dataCenterPromise;
|
||||
try {
|
||||
await dataCenter.login();
|
||||
const user = (await dataCenter.getUserInfo()) as User;
|
||||
|
||||
if (!user) {
|
||||
// Add ErrorBoundary
|
||||
throw new Error('User info not found');
|
||||
}
|
||||
|
||||
let isOwner;
|
||||
if (workspace?.provider === 'local') {
|
||||
// isOwner is useful only in the cloud
|
||||
isOwner = true;
|
||||
} else {
|
||||
isOwner = user.id === workspace?.owner?.id;
|
||||
}
|
||||
|
||||
set({ user, isOwner });
|
||||
|
||||
logger.debug('login success', user);
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
logger.error('login failed', error);
|
||||
return null; // login failed
|
||||
}
|
||||
},
|
||||
logout: async () => {
|
||||
const dataCenter = await dataCenterPromise;
|
||||
await dataCenter.logout();
|
||||
logger.debug('logout success');
|
||||
set({ user: null });
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
export * from './app';
|
||||
export type { PageMeta } from './app/datacenter';
|
||||
export { createDefaultWorkspace, DataCenterPreloader } from './app/datacenter';
|
||||
export {
|
||||
dataCenterPromise,
|
||||
useDataCenter,
|
||||
useDataCenterPublicWorkspace,
|
||||
useDataCenterWorkspace,
|
||||
} from './app/datacenter';
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { DataCenter, getDataCenter } from '@affine/datacenter';
|
||||
import {
|
||||
createDefaultWorkspace,
|
||||
GlobalAppProvider,
|
||||
useDataCenter,
|
||||
useGlobalState,
|
||||
} from '@affine/store';
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
describe('App Store', () => {
|
||||
test('init', async () => {
|
||||
const dataCenterPromise = getDataCenter();
|
||||
const dataCenter = await dataCenterPromise;
|
||||
await createDefaultWorkspace(dataCenter);
|
||||
const Inner = () => {
|
||||
const state = useGlobalState();
|
||||
const dataCenter = useDataCenter();
|
||||
expect(state).toBeTypeOf('object');
|
||||
expect(dataCenter).toBeInstanceOf(DataCenter);
|
||||
return <div>Test2</div>;
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<GlobalAppProvider>
|
||||
<div>Test1</div>
|
||||
<Inner />
|
||||
</GlobalAppProvider>
|
||||
);
|
||||
const app = render(<App />);
|
||||
app.getByText('Test2');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user