refactor: extract store package (#1109)

This commit is contained in:
Himself65
2023-02-18 02:41:22 -06:00
committed by GitHub
parent af28418e61
commit 9d21c3efbb
34 changed files with 681 additions and 337 deletions

View File

@@ -11,10 +11,11 @@
"dependencies": {
"@affine/component": "workspace:*",
"@affine/datacenter": "workspace:*",
"@affine/store": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/blocks": "0.4.0-20230217095654-a561b36",
"@blocksuite/editor": "0.4.0-20230217095654-a561b36",
"@blocksuite/global": "0.4.0-20230216011811-2776d93",
"@blocksuite/global": "0.4.0-20230217095654-a561b36",
"@blocksuite/icons": "^2.0.14",
"@blocksuite/store": "0.4.0-20230217095654-a561b36",
"@emotion/css": "^11.10.5",

View File

@@ -2,10 +2,9 @@ import { styled } from '@affine/component';
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import { Button } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useGlobalState } from '@affine/store';
import { useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import { Check, UnCheck } from './icon';
interface LoginModalProps {
open: boolean;
@@ -14,7 +13,7 @@ interface LoginModalProps {
export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
const [localCache, setLocalCache] = useState(true);
const { blobDataSynced } = useAppState();
const blobDataSynced = useGlobalState(store => store.blobDataSynced);
const { t } = useTranslation();
return (

View File

@@ -1,10 +1,9 @@
import { TableCell } from '@affine/component';
import type { PageMeta } from '@affine/store';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import React from 'react';
import { PageMeta } from '@/providers/app-state-provider';
dayjs.extend(localizedFormat);
export const DateCell = ({

View File

@@ -7,6 +7,7 @@ import {
} from '@affine/component';
import { toast } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { PageMeta } from '@affine/store';
import {
DeleteForeverIcon,
FavouritedIcon,
@@ -18,7 +19,6 @@ import {
} from '@blocksuite/icons';
import { usePageHelper } from '@/hooks/use-page-helper';
import { PageMeta } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {

View File

@@ -10,6 +10,7 @@ import { IconButton } from '@affine/component';
import { Tooltip } from '@affine/component';
import { toast } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { PageMeta } from '@affine/store';
import {
EdgelessIcon,
FavouritedIcon,
@@ -21,7 +22,6 @@ import React, { useCallback } from 'react';
import DateCell from '@/components/page-list/DateCell';
import { usePageHelper } from '@/hooks/use-page-helper';
import { PageMeta } from '@/providers/app-state-provider';
import { useTheme } from '@/providers/ThemeProvider';
import { useGlobalState } from '@/store/app';

View File

@@ -1,4 +1,5 @@
import { useTranslation } from '@affine/i18n';
import { PageMeta } from '@affine/store';
import { EdgelessIcon, PaperIcon } from '@blocksuite/icons';
import { Workspace } from '@blocksuite/store';
import { Command } from 'cmdk';
@@ -6,7 +7,6 @@ import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import usePageHelper from '@/hooks/use-page-helper';
import { PageMeta } from '@/providers/app-state-provider';
import { useGlobalState } from '@/store/app';
import { NoResultSVG } from './NoResultSVG';

View File

@@ -11,8 +11,6 @@ import {
StyledModalWrapper,
StyledTextContent,
} from './style';
// import { getDataCenter } from '@affine/datacenter';
// import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceDeleteProps {
open: boolean;

View File

@@ -1,6 +1,6 @@
import { PageMeta } from '@affine/store';
import { useCallback } from 'react';
import { PageMeta } from '@/providers/app-state-provider';
import { useGlobalState } from '@/store/app';
export type ChangePageMeta = (

View File

@@ -1,6 +1,6 @@
import { PageMeta } from '@affine/store';
import { useCallback, useEffect, useState } from 'react';
import { PageMeta } from '@/providers/app-state-provider';
import { useGlobalState } from '@/store/app';
export const useCurrentPageMeta = (): PageMeta | null => {

View File

@@ -1,4 +1,5 @@
import { WorkspaceUnit } from '@affine/datacenter';
import { PageMeta } from '@affine/store';
import { EditorContainer } from '@blocksuite/editor';
import { uuidv4, Workspace } from '@blocksuite/store';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
@@ -7,7 +8,6 @@ import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { useChangePageMeta } from '@/hooks/use-change-page-meta';
import { PageMeta } from '@/providers/app-state-provider';
import { useGlobalState } from '@/store/app';
export type EditorHandlers = {

View File

@@ -6,6 +6,7 @@ import '../utils/print-build-info';
import '@affine/i18n';
import { useTranslation } from '@affine/i18n';
import { DataCenterPreloader } from '@affine/store';
import { Logger } from '@toeverything/pathfinder-logger';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
@@ -19,11 +20,9 @@ import React from 'react';
import { PageLoading } from '@/components/loading';
import { MessageCenterHandler } from '@/components/message-center-handler';
import ProviderComposer from '@/components/provider-composer';
import { AppStateProvider } from '@/providers/app-state-provider';
import ConfirmProvider from '@/providers/ConfirmProvider';
import { ThemeProvider } from '@/providers/ThemeProvider';
import { GlobalAppProvider } from '@/store/app';
import { DataCenterPreloader } from '@/store/app/datacenter';
import { ModalProvider } from '@/store/globalModal';
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
@@ -65,30 +64,28 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
<title>AFFiNE</title>
</Head>
<Logger />
<GlobalAppProvider key="BlockSuiteProvider">
<ProviderComposer
contexts={[
<ThemeProvider key="ThemeProvider" />,
<AppStateProvider key="appStateProvider" />,
<ModalProvider key="ModalProvider" />,
<ConfirmProvider key="ConfirmProvider" />,
]}
>
{NoNeedAppStatePageList.includes(router.route) ? (
getLayout(<Component {...pageProps} />)
) : (
<Suspense fallback={<PageLoading />}>
<DataCenterPreloader>
<MessageCenterHandler>
<AppDefender>
{getLayout(<Component {...pageProps} />)}
</AppDefender>
</MessageCenterHandler>
</DataCenterPreloader>
</Suspense>
)}
</ProviderComposer>
</GlobalAppProvider>
<ProviderComposer
contexts={[
<GlobalAppProvider key="GlobalAppProvider" />,
<ThemeProvider key="ThemeProvider" />,
<ModalProvider key="ModalProvider" />,
<ConfirmProvider key="ConfirmProvider" />,
]}
>
{NoNeedAppStatePageList.includes(router.route) ? (
getLayout(<Component {...pageProps} />)
) : (
<Suspense fallback={<PageLoading />}>
<DataCenterPreloader>
<MessageCenterHandler>
<AppDefender>
{getLayout(<Component {...pageProps} />)}
</AppDefender>
</MessageCenterHandler>
</DataCenterPreloader>
</Suspense>
)}
</ProviderComposer>
</>
);
};

View File

@@ -1,4 +1,5 @@
import { Breadcrumbs } from '@affine/component';
import { PageMeta } from '@affine/store';
import { SearchIcon } from '@blocksuite/icons';
import { useRouter } from 'next/router';
import { ReactElement, useEffect, useMemo } from 'react';
@@ -7,7 +8,6 @@ import { PageLoading } from '@/components/loading';
import { PageList } from '@/components/page-list';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useLoadPublicWorkspace } from '@/hooks/use-load-public-workspace';
import { PageMeta } from '@/providers/app-state-provider';
import { useModal } from '@/store/globalModal';
import {

View File

@@ -1,51 +0,0 @@
import type { Disposable } from '@blocksuite/global/utils';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';
import { useGlobalState } from '@/store/app';
import { AppStateContext } from './interface';
type AppStateContextProps = PropsWithChildren<Record<string, unknown>>;
export const AppState = createContext<AppStateContext>({} as AppStateContext);
export const useAppState = () => useContext(AppState);
export const AppStateProvider = ({
children,
}: PropsWithChildren<AppStateContextProps>) => {
const currentDataCenterWorkspace = useGlobalState(
store => store.currentDataCenterWorkspace
);
const [blobState, setBlobState] = useState(false);
useEffect(() => {
let syncChangeDisposable: Disposable | undefined;
const currentWorkspace = currentDataCenterWorkspace;
if (!currentWorkspace) {
return;
}
const getBlobStorage = async () => {
const blobStorage = await currentWorkspace?.blocksuiteWorkspace?.blobs;
syncChangeDisposable = blobStorage?.signals.onBlobSyncStateChange.on(
() => {
setBlobState(blobStorage?.uploading);
}
);
};
getBlobStorage();
return () => {
syncChangeDisposable?.dispose();
};
}, [currentDataCenterWorkspace]);
return (
<AppState.Provider
value={{
blobDataSynced: blobState,
}}
>
{children}
</AppState.Provider>
);
};

View File

@@ -1,2 +0,0 @@
export * from './interface';
export * from './Provider';

View File

@@ -1,28 +0,0 @@
import type { EditorContainer } from '@blocksuite/editor';
import type {
Page as StorePage,
PageMeta as StorePageMeta,
} from '@blocksuite/store';
export interface PageMeta extends StorePageMeta {
favorite: boolean;
trash: boolean;
trashDate: number;
updatedDate: number;
mode: 'edgeless' | 'page';
}
export type AppStateValue = {
blobDataSynced: boolean;
};
/**
* @deprecated
*/
export type AppStateFunction = {
// todo: remove this in the future
};
export type AppStateContext = AppStateValue & AppStateFunction;
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;

View File

@@ -1,9 +0,0 @@
import { DataCenter } from '@affine/datacenter';
const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
return dataCenter.createWorkspace({
name: DEFAULT_WORKSPACE_NAME,
});
};

View File

@@ -1,61 +0,0 @@
import { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import { Page, Workspace } from '@blocksuite/store';
import { GlobalActionsCreator } from '@/store/app';
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,
});
},
});

View File

@@ -1,143 +0,0 @@
import type { DataCenter } from '@affine/datacenter';
import { getDataCenter, WorkspaceUnit } from '@affine/datacenter';
import { DisposableGroup } from '@blocksuite/global/utils';
import React, { useCallback, useEffect } from 'react';
import { PageMeta } from '@/providers/app-state-provider';
import { createDefaultWorkspace } from '@/providers/app-state-provider/utils';
import {
GlobalActionsCreator,
useGlobalState,
useGlobalStateApi,
} from '@/store/app';
export type DataCenterState = {
readonly dataCenter: DataCenter;
readonly dataCenterPromise: Promise<DataCenter>;
currentDataCenterWorkspace: WorkspaceUnit | null;
dataCenterPageList: PageMeta[];
};
export type DataCenterActions = {
loadWorkspace: (
workspaceId: string,
signal?: AbortSignal
) => Promise<WorkspaceUnit | null>;
};
export const createDataCenterState = (): DataCenterState => ({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
dataCenter: null!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
dataCenterPromise: null!,
currentDataCenterWorkspace: null,
dataCenterPageList: [],
});
export const createDataCenterActions: GlobalActionsCreator<
DataCenterActions
> = (set, get) => ({
loadWorkspace: async (workspaceId, signal) => {
const { dataCenter, 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)) ?? 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 = isOwner = userInfo?.id === workspace?.owner?.id;
}
const pageList =
(workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
if (workspace?.blocksuiteWorkspace) {
set({
currentWorkspace: workspace.blocksuiteWorkspace,
});
}
set({
isOwner,
});
set({
currentDataCenterWorkspace: workspace,
dataCenterPageList: pageList,
});
return workspace;
},
});
export function DataCenterPreloader({ children }: React.PropsWithChildren) {
const dataCenter = useGlobalState(useCallback(store => store.dataCenter, []));
const dataCenterPromise = useGlobalState(
useCallback(store => store.dataCenterPromise, [])
);
const api = useGlobalStateApi();
//# 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
if (!dataCenter && !dataCenterPromise) {
const promise = getDataCenter();
api.setState({ dataCenterPromise: promise });
promise.then(async dataCenter => {
// Ensure datacenter has at least one workspace
if (dataCenter.workspaces.length === 0) {
await createDefaultWorkspace(dataCenter);
}
// set initial state
api.setState({
dataCenter,
currentWorkspace: null,
currentDataCenterWorkspace: null,
dataCenterPageList: [],
user:
(await dataCenter.getUserInfo(
dataCenter.providers.filter(p => p.id !== 'local')[0]?.id
)) || null,
});
});
throw promise;
}
if (!dataCenter) {
throw dataCenterPromise;
}
return <>{children}</>;
}

View File

@@ -1,90 +1 @@
import type React from 'react';
import { createContext, useContext, useMemo } from 'react';
import { createStore, StateCreator, useStore } from 'zustand';
import { combine, subscribeWithSelector } from 'zustand/middleware';
import type { UseBoundStore } from 'zustand/react';
import {
BlockSuiteActions,
BlockSuiteState,
createBlockSuiteActions,
createBlockSuiteState,
} from '@/store/app/blocksuite';
import {
createDataCenterActions,
createDataCenterState,
DataCenterActions,
DataCenterState,
} from '@/store/app/datacenter';
import {
createUserActions,
createUserState,
UserActions,
UserState,
} from '@/store/app/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 const GlobalAppProvider: React.FC<React.PropsWithChildren> =
function ModelProvider({ children }) {
return (
<GlobalStateContext.Provider value={useMemo(() => create(), [])}>
{children}
</GlobalStateContext.Provider>
);
};
export * from '@affine/store';

View File

@@ -1,57 +0,0 @@
import { User } from '@affine/datacenter';
import { GlobalActionsCreator } from '@/store/app';
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,
});
export const createUserActions: GlobalActionsCreator<UserActions> = (
set,
get
) => {
return {
login: async () => {
const { dataCenter, currentDataCenterWorkspace: workspace } = get();
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 });
return user;
} catch (error) {
return null; // login failed
}
},
logout: async () => {
const { dataCenter } = get();
await dataCenter.logout();
set({ user: null });
},
};
};