revert: loadWorkspace unexpected behavior (#1172)

This commit is contained in:
Himself65 2023-02-21 20:44:18 -06:00 committed by GitHub
parent 86346b284e
commit 0b072da346
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1225 additions and 2198 deletions

View File

@ -0,0 +1,80 @@
import { Modal, ModalWrapper } from '@affine/component';
import { IconButton } from '@affine/component';
import { toast } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons';
import router from 'next/router';
import { useCallback, useState } from 'react';
import { useGlobalState } from '@/store/app';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
interface EnableWorkspaceModalProps {
open: boolean;
onClose: () => void;
}
export const EnableWorkspaceModal = ({
open,
onClose,
}: EnableWorkspaceModalProps) => {
const { t } = useTranslation();
const login = useGlobalState(store => store.login);
const user = useGlobalState(store => store.user);
const dataCenter = useGlobalState(store => store.dataCenter);
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const [loading, setLoading] = useState(false);
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<IconButton
onClick={() => {
onClose();
}}
>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
{/* <StyleTips>{t('Retain local cached data')}</StyleTips> */}
<div>
<StyleButton
shape="round"
type="primary"
loading={loading}
onClick={async () => {
setLoading(true);
if (!user) {
await login();
}
if (currentWorkspace) {
const workspace = await dataCenter.enableWorkspaceCloud(
currentWorkspace
);
workspace &&
router.push(`/workspace/${workspace.id}/setting`);
toast(t('Enabled success'));
}
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t('Not now')}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};

View File

@ -12,7 +12,6 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/datacenter": "workspace:*", "@affine/datacenter": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@affine/store": "workspace:*",
"@affine/debug": "workspace:*", "@affine/debug": "workspace:*",
"@blocksuite/blocks": "0.4.1", "@blocksuite/blocks": "0.4.1",
"@blocksuite/editor": "0.4.1", "@blocksuite/editor": "0.4.1",

View File

@ -4,7 +4,7 @@ import { CloseIcon } from '@blocksuite/icons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useDataCenter, useGlobalState } from '@/store/app'; import { useGlobalState } from '@/store/app';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
@ -20,7 +20,7 @@ export const EnableWorkspaceModal = ({
const { t } = useTranslation(); const { t } = useTranslation();
const login = useGlobalState(store => store.login); const login = useGlobalState(store => store.login);
const user = useGlobalState(store => store.user); const user = useGlobalState(store => store.user);
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
const currentWorkspace = useGlobalState( const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, []) useCallback(store => store.currentDataCenterWorkspace, [])
); );

View File

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

View File

@ -2,13 +2,13 @@ import { toast } from '@affine/component';
import { MessageCenter } from '@affine/datacenter'; import { MessageCenter } from '@affine/datacenter';
import { AffineProvider } from '@affine/datacenter'; import { AffineProvider } from '@affine/datacenter';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { ReactNode, useEffect } from 'react'; import { ReactNode, useCallback, useEffect } from 'react';
import { useDataCenter } from '@/store/app'; import { useGlobalState } from '@/store/app';
export function MessageCenterHandler({ children }: { children?: ReactNode }) { export function MessageCenterHandler({ children }: { children?: ReactNode }) {
const router = useRouter(); const router = useRouter();
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(useCallback(store => store.dataCenter, []));
useEffect(() => { useEffect(() => {
const instance = MessageCenter.getInstance(); const instance = MessageCenter.getInstance();
if (instance) { if (instance) {

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { PageMeta, useDataCenter } from '@affine/store';
import { EdgelessIcon, PaperIcon } from '@blocksuite/icons'; import { EdgelessIcon, PaperIcon } from '@blocksuite/icons';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
@ -7,6 +6,8 @@ import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import usePageHelper from '@/hooks/use-page-helper'; import usePageHelper from '@/hooks/use-page-helper';
import { PageMeta } from '@/providers/app-state-provider';
import { useGlobalState } from '@/store/app';
import { NoResultSVG } from './NoResultSVG'; import { NoResultSVG } from './NoResultSVG';
import { StyledListItem, StyledNotFound } from './style'; import { StyledListItem, StyledNotFound } from './style';
@ -23,7 +24,7 @@ export const PublishedResults = (props: {
props; props;
const { search } = usePageHelper(); const { search } = usePageHelper();
const [results, setResults] = useState(new Map<string, string | undefined>()); const [results, setResults] = useState(new Map<string, string | undefined>());
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
const router = useRouter(); const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]); const [pageList, setPageList] = useState<PageMeta[]>([]);
useEffect(() => { useEffect(() => {

View File

@ -1,14 +1,18 @@
import { useGlobalState } from '@affine/store';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect } from 'react'; import { PropsWithChildren } from 'react';
import HelpIsland from '@/components/help-island'; import HelpIsland from '@/components/help-island';
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar'; import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
import { useRouterTargetWorkspace } from '@/hooks/use-router-target-workspace'; import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
import { PageLoading } from '../loading'; import { PageLoading } from '../loading';
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles'; import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
export const WorkspaceDefender = ({ children }: PropsWithChildren) => {
const { workspaceLoaded } = useEnsureWorkspace();
return <>{workspaceLoaded ? children : <PageLoading />}</>;
};
export const WorkspaceLayout = ({ children }: PropsWithChildren) => { export const WorkspaceLayout = ({ children }: PropsWithChildren) => {
const router = useRouter(); const router = useRouter();
@ -31,22 +35,10 @@ export const WorkspaceLayout = ({ children }: PropsWithChildren) => {
}; };
export const Layout = ({ children }: PropsWithChildren) => { export const Layout = ({ children }: PropsWithChildren) => {
const { targetWorkspace, exist } = useRouterTargetWorkspace(); return (
const router = useRouter(); <WorkspaceDefender>
const loadWorkspace = useGlobalState(store => store.loadWorkspace); <WorkspaceLayout>{children}</WorkspaceLayout>
useEffect(() => { </WorkspaceDefender>
if (!exist) { );
router.replace('/404');
}
}, [exist, router]);
useEffect(() => {
if (exist && targetWorkspace) {
loadWorkspace(targetWorkspace.id);
}
}, [exist, loadWorkspace, targetWorkspace]);
if (!targetWorkspace) {
return <PageLoading />;
}
return <WorkspaceLayout>{children}</WorkspaceLayout>;
}; };
export default Layout; export default Layout;

View File

@ -5,7 +5,7 @@ import { HelpIcon, PlusIcon } from '@blocksuite/icons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { useDataCenter, useGlobalState } from '@/store/app'; import { useGlobalState } from '@/store/app';
import { CreateWorkspaceModal } from '../create-workspace'; import { CreateWorkspaceModal } from '../create-workspace';
import { LoginModal } from '../login-modal'; import { LoginModal } from '../login-modal';
@ -34,7 +34,7 @@ interface WorkspaceModalProps {
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => { export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false); const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const logout = useGlobalState(store => store.logout); const logout = useGlobalState(store => store.logout);
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false); const [loginOpen, setLoginOpen] = useState(false);

View File

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

View File

@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { WorkspaceModal } from '@/components/workspace-modal'; import { WorkspaceModal } from '@/components/workspace-modal';
import { useDataCenter, useGlobalState } from '@/store/app'; import { useGlobalState } from '@/store/app';
import { SelectorWrapper, WorkspaceName } from './styles'; import { SelectorWrapper, WorkspaceName } from './styles';
@ -11,7 +11,7 @@ export const WorkspaceSelector = () => {
const currentWorkspace = useGlobalState( const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, []) useCallback(store => store.currentDataCenterWorkspace, [])
); );
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(useCallback(store => store.dataCenter, []));
if (dataCenter.workspaces.length === 0) { if (dataCenter.workspaces.length === 0) {
setWorkspaceListShow(true); setWorkspaceListShow(true);

View File

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

View File

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

View File

@ -0,0 +1,73 @@
import { assertEquals } from '@blocksuite/global/utils';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import { useGlobalState } from '@/store/app';
// todo: refactor with suspense mode
// It is a fully effective hook
// Cause it not just ensure workspace loaded, but also have router change.
export const useEnsureWorkspace = () => {
const dataCenter = useGlobalState(useCallback(store => store.dataCenter, []));
const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const loadWorkspace = useGlobalState(
useCallback(store => store.loadWorkspace, [])
);
const router = useRouter();
const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string | null>(
typeof router.query.workspaceId === 'string'
? router.query.workspaceId
: null
);
// const defaultOutLineWorkspaceId = '99ce7eb7';
// console.log(defaultOutLineWorkspaceId);
useEffect(() => {
const abortController = new AbortController();
const workspaceId =
(router.query.workspaceId as string) || dataCenter.workspaces[0]?.id;
// If router.query.workspaceId is not in workspace list, jump to 404 page
// If workspaceList is empty, we need to create a default workspace but not jump to 404
if (
workspaceId &&
dataCenter.workspaces.length &&
dataCenter.workspaces.findIndex(
meta => meta.id.toString() === workspaceId
) === -1
) {
router.push('/404');
return;
}
// If user is not login and input a custom workspaceId, jump to 404 page
// if (
// !user &&
// router.query.workspaceId &&
// router.query.workspaceId !== defaultOutLineWorkspaceId
// ) {
// router.push('/404');
// return;
// }
loadWorkspace(workspaceId, abortController.signal).then(unit => {
if (!abortController.signal.aborted && unit) {
setCurrentWorkspaceId(unit.id);
assertEquals(unit.id, workspaceId);
}
});
return () => {
abortController.abort();
};
}, [dataCenter, loadWorkspace, router]);
return {
workspaceLoaded: currentWorkspace?.id === currentWorkspaceId,
activeWorkspaceId: currentWorkspace?.id ?? router.query.workspaceId,
};
};
export default useEnsureWorkspace;

View File

@ -0,0 +1,35 @@
import { getDataCenter, WorkspaceUnit } from '@affine/datacenter';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
export function useLoadPublicWorkspace(workspaceId: string) {
const router = useRouter();
const [workspace, setWorkspace] = useState<WorkspaceUnit | null>();
const [status, setStatus] = useState<'loading' | 'error' | 'success'>(
'loading'
);
useEffect(() => {
setStatus('loading');
const init = async () => {
const dataCenter = await getDataCenter();
dataCenter
.loadPublicWorkspace(workspaceId)
.then(data => {
setWorkspace(data);
setStatus('success');
})
.catch(() => {
// if (!cancel) {
// router.push('/404');
// }
setStatus('error');
});
};
init();
}, [router, workspaceId]);
return { status, workspace };
}

View File

@ -1,9 +1,9 @@
import { Member } from '@affine/datacenter'; import { Member } from '@affine/datacenter';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useDataCenter, useGlobalState } from '@/store/app'; import { useGlobalState } from '@/store/app';
export const useMembers = () => { export const useMembers = () => {
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
const currentWorkspace = useGlobalState( const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, []) useCallback(store => store.currentDataCenterWorkspace, [])
); );

View File

@ -1,13 +1,13 @@
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { PageMeta } from '@affine/store';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
import { uuidv4, Workspace } from '@blocksuite/store'; import { uuidv4, Workspace } from '@blocksuite/store';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import type { QueryContent } from '@blocksuite/store/dist/workspace/search'; import type { QueryContent } from '@blocksuite/store/dist/workspace/search';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useCallback, useMemo } from 'react'; import { useCallback } from 'react';
import { useChangePageMeta } from '@/hooks/use-change-page-meta'; import { useChangePageMeta } from '@/hooks/use-change-page-meta';
import { PageMeta } from '@/providers/app-state-provider';
import { useGlobalState } from '@/store/app'; import { useGlobalState } from '@/store/app';
export type EditorHandlers = { export type EditorHandlers = {
@ -48,8 +48,7 @@ export const usePageHelper = (): EditorHandlers => {
useCallback(store => store.currentDataCenterWorkspace, []) useCallback(store => store.currentDataCenterWorkspace, [])
); );
return useMemo( return {
() => ({
createPage: ({ createPage: ({
pageId = uuidv4().replaceAll('-', ''), pageId = uuidv4().replaceAll('-', ''),
title = '', title = '',
@ -146,9 +145,7 @@ export const usePageHelper = (): EditorHandlers => {
) as PageMeta) || null ) as PageMeta) || null
); );
}, },
}), };
[changePageMeta, currentWorkspace, editor, router]
);
}; };
export default usePageHelper; export default usePageHelper;

View File

@ -1,26 +0,0 @@
import { useDataCenter, useDataCenterWorkspace } from '@affine/store';
import { useRouter } from 'next/router';
import { useMemo } from 'react';
export function useRouterTargetWorkspace() {
const router = useRouter();
const dataCenter = useDataCenter();
const workspaceId =
typeof router.query.workspaceId === 'string'
? router.query.workspaceId
: dataCenter.workspaces.at(0)?.id ?? null;
const targetWorkspace = useDataCenterWorkspace(workspaceId);
const notExist = useMemo(
() =>
workspaceId &&
dataCenter.workspaces.length &&
dataCenter.workspaces.findIndex(
meta => meta.id.toString() === workspaceId
) === -1,
[dataCenter.workspaces, workspaceId]
);
return {
targetWorkspace,
exist: !notExist,
};
}

View File

@ -1,10 +1,10 @@
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDataCenter, useGlobalState } from '@/store/app'; import { useGlobalState } from '@/store/app';
export const useWorkspaceHelper = () => { export const useWorkspaceHelper = () => {
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
const currentWorkspace = useGlobalState( const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, []) useCallback(store => store.currentDataCenterWorkspace, [])
); );

View File

@ -6,22 +6,24 @@ import '../utils/print-build-info';
import '@affine/i18n'; import '@affine/i18n';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { DataCenterPreloader } from '@affine/store';
import { Logger } from '@toeverything/pathfinder-logger'; import { Logger } from '@toeverything/pathfinder-logger';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
// import AppStateProvider2 from '@/providers/app-state-provider2/provider'; // import AppStateProvider2 from '@/providers/app-state-provider2/provider';
import type { ReactElement, ReactNode } from 'react'; import { useRouter } from 'next/router';
import { Suspense } from 'react'; import type { PropsWithChildren, ReactElement, ReactNode } from 'react';
import { Suspense, useEffect } from 'react';
import React from 'react'; import React from 'react';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import { MessageCenterHandler } from '@/components/message-center-handler'; import { MessageCenterHandler } from '@/components/message-center-handler';
import ProviderComposer from '@/components/provider-composer'; import ProviderComposer from '@/components/provider-composer';
import { AppStateProvider } from '@/providers/app-state-provider';
import ConfirmProvider from '@/providers/ConfirmProvider'; import ConfirmProvider from '@/providers/ConfirmProvider';
import { ThemeProvider } from '@/providers/ThemeProvider'; import { ThemeProvider } from '@/providers/ThemeProvider';
import { GlobalAppProvider } from '@/store/app'; import { GlobalAppProvider } from '@/store/app';
import { DataCenterPreloader } from '@/store/app/datacenter';
import { ModalProvider } from '@/store/globalModal'; import { ModalProvider } from '@/store/globalModal';
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage< export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
@ -35,9 +37,16 @@ type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout; Component: NextPageWithLayout;
}; };
// Page list which do not rely on app state
const NoNeedAppStatePageList = [
'/404',
'/public-workspace/[workspaceId]',
'/public-workspace/[workspaceId]/[pageId]',
];
const App = ({ Component, pageProps }: AppPropsWithLayout) => { const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const getLayout = Component.getLayout || (page => page); const getLayout = Component.getLayout || (page => page);
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const router = useRouter();
React.useEffect(() => { React.useEffect(() => {
document.documentElement.lang = i18n.language; document.documentElement.lang = i18n.language;
@ -56,24 +65,43 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
<title>AFFiNE</title> <title>AFFiNE</title>
</Head> </Head>
<Logger /> <Logger />
<GlobalAppProvider key="BlockSuiteProvider">
<ProviderComposer <ProviderComposer
contexts={[ contexts={[
<GlobalAppProvider key="GlobalAppProvider" />,
<ThemeProvider key="ThemeProvider" />, <ThemeProvider key="ThemeProvider" />,
<AppStateProvider key="appStateProvider" />,
<ModalProvider key="ModalProvider" />, <ModalProvider key="ModalProvider" />,
<ConfirmProvider key="ConfirmProvider" />, <ConfirmProvider key="ConfirmProvider" />,
]} ]}
> >
{NoNeedAppStatePageList.includes(router.route) ? (
getLayout(<Component {...pageProps} />)
) : (
<Suspense fallback={<PageLoading />}> <Suspense fallback={<PageLoading />}>
<DataCenterPreloader> <DataCenterPreloader>
<MessageCenterHandler> <MessageCenterHandler>
<AppDefender>
{getLayout(<Component {...pageProps} />)} {getLayout(<Component {...pageProps} />)}
</AppDefender>
</MessageCenterHandler> </MessageCenterHandler>
</DataCenterPreloader> </DataCenterPreloader>
</Suspense> </Suspense>
)}
</ProviderComposer> </ProviderComposer>
</GlobalAppProvider>
</> </>
); );
}; };
const AppDefender = ({ children }: PropsWithChildren) => {
const router = useRouter();
useEffect(() => {
if (['/index.html', '/'].includes(router.asPath)) {
router.replace('/workspace');
}
}, [router]);
return <>{children}</>;
};
export default App; export default App;

View File

@ -1,15 +1,7 @@
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { PageLoading } from '@/components/loading';
const Home: NextPage = () => { const Home: NextPage = () => {
const router = useRouter(); return <div title="Home Page"></div>;
useEffect(() => {
router.replace('/workspace');
}, [router]);
return <PageLoading />;
}; };
export default Home; export default Home;

View File

@ -11,7 +11,7 @@ import { useEffect, useState } from 'react';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useDataCenter } from '@/store/app'; import { useGlobalState } from '@/store/app';
import inviteError from '../../../public/imgs/invite-error.svg'; import inviteError from '../../../public/imgs/invite-error.svg';
import inviteSuccess from '../../../public/imgs/invite-success.svg'; import inviteSuccess from '../../../public/imgs/invite-success.svg';
@ -21,7 +21,7 @@ export default function DevPage() {
const router = useRouter(); const router = useRouter();
const [inviteData, setInviteData] = useState<Permission | null>(null); const [inviteData, setInviteData] = useState<Permission | null>(null);
const { acceptInvite } = useWorkspaceHelper(); const { acceptInvite } = useWorkspaceHelper();
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {

View File

@ -2,7 +2,6 @@ import { displayFlex, styled } from '@affine/component';
import { Breadcrumbs } from '@affine/component'; import { Breadcrumbs } from '@affine/component';
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { useDataCenterPublicWorkspace } from '@affine/store';
import { PaperIcon, SearchIcon } from '@blocksuite/icons'; import { PaperIcon, SearchIcon } from '@blocksuite/icons';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import NextLink from 'next/link'; import NextLink from 'next/link';
@ -11,6 +10,7 @@ import { ReactElement, useEffect, useMemo } from 'react';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useLoadPublicWorkspace } from '@/hooks/use-load-public-workspace';
import { useModal } from '@/store/globalModal'; import { useModal } from '@/store/globalModal';
import type { NextPageWithLayout } from '../..//_app'; import type { NextPageWithLayout } from '../..//_app';
@ -21,17 +21,13 @@ const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
const Page: NextPageWithLayout = () => { const Page: NextPageWithLayout = () => {
const router = useRouter(); const router = useRouter();
const { workspaceId, pageId } = router.query; const { workspaceId, pageId } = router.query as Record<string, string>;
const { error, workspace: workspaceUnit } = useDataCenterPublicWorkspace( const { status, workspace: workspaceUnit } =
typeof workspaceId === 'string' ? workspaceId : null useLoadPublicWorkspace(workspaceId);
);
const { triggerQuickSearchModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const { t } = useTranslation(); const { t } = useTranslation();
const page = useMemo(() => { const page = useMemo(() => {
if (typeof pageId !== 'string') {
return null;
}
if (workspaceUnit?.blocksuiteWorkspace) { if (workspaceUnit?.blocksuiteWorkspace) {
return workspaceUnit.blocksuiteWorkspace.getPage(pageId); return workspaceUnit.blocksuiteWorkspace.getPage(pageId);
} }
@ -50,15 +46,18 @@ const Page: NextPageWithLayout = () => {
}, [workspace, router, pageId]); }, [workspace, router, pageId]);
useEffect(() => { useEffect(() => {
if (error) { if (status === 'error') {
router.push('/404'); router.push('/404');
} }
}, [router, error]); }, [router, status]);
if (!workspace) { if (status === 'loading') {
return <PageLoading />; return <PageLoading />;
} }
if (status === 'error') {
return null;
}
return ( return (
<PageContainer> <PageContainer>
<NavContainer> <NavContainer>
@ -87,7 +86,7 @@ const Page: NextPageWithLayout = () => {
</SearchButton> </SearchButton>
</NavContainer> </NavContainer>
{page && ( {workspace && page && (
<DynamicBlocksuite <DynamicBlocksuite
page={page} page={page}
workspace={workspace} workspace={workspace}

View File

@ -1,5 +1,4 @@
import { Breadcrumbs } from '@affine/component'; import { Breadcrumbs } from '@affine/component';
import { PageMeta, useDataCenterPublicWorkspace } from '@affine/store';
import { SearchIcon } from '@blocksuite/icons'; import { SearchIcon } from '@blocksuite/icons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { ReactElement, useEffect, useMemo } from 'react'; import { ReactElement, useEffect, useMemo } from 'react';
@ -7,6 +6,8 @@ import { ReactElement, useEffect, useMemo } from 'react';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import { PageList } from '@/components/page-list'; import { PageList } from '@/components/page-list';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; 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 { useModal } from '@/store/globalModal';
import { import {
@ -19,10 +20,8 @@ import {
const All = () => { const All = () => {
const router = useRouter(); const router = useRouter();
const { triggerQuickSearchModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const { workspace, error } = useDataCenterPublicWorkspace( const { status, workspace } = useLoadPublicWorkspace(
typeof router.query.workspaceId === 'string' router.query.workspaceId as string
? router.query.workspaceId
: null
); );
const pageList = useMemo(() => { const pageList = useMemo(() => {
@ -32,15 +31,19 @@ const All = () => {
const workspaceName = workspace?.blocksuiteWorkspace?.meta.name; const workspaceName = workspace?.blocksuiteWorkspace?.meta.name;
useEffect(() => { useEffect(() => {
if (error) { if (status === 'error') {
router.push('/404'); router.push('/404');
} }
}, [router, error]); }, [router, status]);
if (!workspace) { if (status === 'loading') {
return <PageLoading />; return <PageLoading />;
} }
if (status === 'error') {
return null;
}
return ( return (
<PageContainer> <PageContainer>
<NavContainer> <NavContainer>

View File

@ -15,7 +15,7 @@ import { EditorHeader } from '@/components/header';
import MobileModal from '@/components/mobile-modal'; import MobileModal from '@/components/mobile-modal';
import WorkspaceLayout from '@/components/workspace-layout'; import WorkspaceLayout from '@/components/workspace-layout';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useDataCenter, useGlobalState, useGlobalStateApi } from '@/store/app'; import { useGlobalState, useGlobalStateApi } from '@/store/app';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhills.md'; import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhills.md';
import type { NextPageWithLayout } from '../..//_app'; import type { NextPageWithLayout } from '../..//_app';
@ -109,7 +109,7 @@ const PageDefender = ({ children }: PropsWithChildren) => {
const currentWorkspace = useGlobalState( const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, []) useCallback(store => store.currentDataCenterWorkspace, [])
); );
const dataCenter = useDataCenter(); const dataCenter = useGlobalState(store => store.dataCenter);
const { createPage } = usePageHelper(); const { createPage } = usePageHelper();
useEffect(() => { useEffect(() => {

View File

@ -1,48 +1,44 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
import usePageHelper from '@/hooks/use-page-helper'; import usePageHelper from '@/hooks/use-page-helper';
import { useRouterTargetWorkspace } from '@/hooks/use-router-target-workspace'; import { useGlobalState } from '@/store/app';
const WorkspaceIndex = () => { const WorkspaceIndex = () => {
const router = useRouter(); const router = useRouter();
const { targetWorkspace, exist } = useRouterTargetWorkspace(); const currentWorkspace = useGlobalState(
useCallback(store => store.currentDataCenterWorkspace, [])
);
const { createPage } = usePageHelper(); const { createPage } = usePageHelper();
const { workspaceLoaded, activeWorkspaceId } = useEnsureWorkspace();
useEffect(() => { useEffect(() => {
if (!exist) {
router.push('/404');
return;
}
const abortController = new AbortController();
const initPage = async () => { const initPage = async () => {
if (abortController.signal.aborted) { if (!workspaceLoaded) {
return;
}
if (!targetWorkspace) {
return; return;
} }
const savedPageId = const savedPageId =
targetWorkspace.blocksuiteWorkspace?.meta.pageMetas.find( currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas.find(
meta => !meta.trash meta => !meta.trash
)?.id; )?.id;
if (savedPageId) { if (savedPageId) {
router.replace(`/workspace/${targetWorkspace.id}/${savedPageId}`); router.replace(`/workspace/${activeWorkspaceId}/${savedPageId}`);
return; return;
} else { }
const pageId = await createPage(); const pageId = await createPage();
if (abortController.signal.aborted) { router.replace(`/workspace/${activeWorkspaceId}/${pageId}`);
return;
}
router.replace(`/workspace/${targetWorkspace.id}/${pageId}`);
}
}; };
initPage(); initPage();
return () => { }, [
abortController.abort(); currentWorkspace,
}; createPage,
}, [targetWorkspace, createPage, router, exist]); router,
workspaceLoaded,
activeWorkspaceId,
]);
return <PageLoading />; return <PageLoading />;
}; };

View File

@ -1,31 +1,23 @@
import { useGlobalStateApi } from '@affine/store';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useRef } from 'react'; import { useCallback, useEffect } from 'react';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import { useRouterTargetWorkspace } from '@/hooks/use-router-target-workspace'; import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
import { useGlobalState } from '@/store/app';
export const WorkspaceIndex = () => { export const WorkspaceIndex = () => {
const router = useRouter(); const router = useRouter();
const api = useGlobalStateApi(); const currentWorkspace = useGlobalState(
const { targetWorkspace, exist } = useRouterTargetWorkspace(); useCallback(store => store.currentDataCenterWorkspace, [])
const onceRef = useRef(true); );
const { workspaceLoaded } = useEnsureWorkspace();
useEffect(() => { useEffect(() => {
if (!onceRef.current) { if (workspaceLoaded) {
return; router.push(`/workspace/${currentWorkspace?.id}`);
} }
onceRef.current = true; }, [currentWorkspace, router, workspaceLoaded]);
if (!exist) {
router.push('/404');
} else if (targetWorkspace) {
api
.getState()
.loadWorkspace(targetWorkspace.id)
.then(() => {
router.push(`/workspace/${targetWorkspace.id}`);
});
}
}, [targetWorkspace, exist, router, api]);
return <PageLoading />; return <PageLoading />;
}; };

View File

@ -1,4 +1,5 @@
import { import {
Theme,
ThemeMode, ThemeMode,
ThemeProviderProps, ThemeProviderProps,
ThemeProviderValue, ThemeProviderValue,
@ -16,15 +17,7 @@ import {
ThemeProvider as MuiThemeProvider, ThemeProvider as MuiThemeProvider,
} from '@mui/material/styles'; } from '@mui/material/styles';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { import { createContext, useContext, useEffect, useState } from 'react';
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
useSyncExternalStore,
} from 'react';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
@ -42,31 +35,16 @@ export const ThemeProvider = ({
defaultTheme = 'light', defaultTheme = 'light',
children, children,
}: PropsWithChildren<ThemeProviderProps>) => { }: PropsWithChildren<ThemeProviderProps>) => {
const localStorageThemeMode = useSyncExternalStore<ThemeMode>( const [theme, setTheme] = useState<Theme>(defaultTheme);
useCallback(cb => { const [mode, setMode] = useState<ThemeMode>('auto');
localStorageThemeHelper.callback.add(cb);
return () => {
localStorageThemeHelper.callback.delete(cb);
};
}, []),
useCallback(() => localStorageThemeHelper.get() ?? 'light', []),
useCallback(() => defaultTheme, [defaultTheme])
);
const [mode, setMode] = useState<ThemeMode>(defaultTheme);
if (localStorageThemeMode !== mode) {
setMode(localStorageThemeMode);
}
const { mode: editorMode = 'page' } = useCurrentPageMeta() || {}; const { mode: editorMode = 'page' } = useCurrentPageMeta() || {};
const themeStyle = const themeStyle =
mode === 'light' ? getLightTheme(editorMode) : getDarkTheme(editorMode); theme === 'light' ? getLightTheme(editorMode) : getDarkTheme(editorMode);
const changeMode = useCallback( const changeMode = (themeMode: ThemeMode) => {
(themeMode: ThemeMode) => {
themeMode !== mode && setMode(themeMode); themeMode !== mode && setMode(themeMode);
// Remember the theme mode which user selected for next time // Remember the theme mode which user selected for next time
localStorageThemeHelper.set(themeMode); localStorageThemeHelper.set(themeMode);
}, };
[mode]
);
// ===================== A temporary solution, just use system theme and not remember the user selected ==================== // ===================== A temporary solution, just use system theme and not remember the user selected ====================
useEffect(() => { useEffect(() => {
@ -79,9 +57,9 @@ export const ThemeProvider = ({
}); });
}, []); }, []);
// useEffect(() => { useEffect(() => {
// setTheme(mode === 'auto' ? theme : mode); setTheme(mode === 'auto' ? theme : mode);
// }, [mode, setTheme, theme]); }, [mode, setTheme, theme]);
// ===================== ==================== // ===================== ====================
// useEffect(() => { // useEffect(() => {
@ -115,12 +93,7 @@ export const ThemeProvider = ({
return ( return (
// Use MuiThemeProvider is just because some Transitions in Mui components need it // Use MuiThemeProvider is just because some Transitions in Mui components need it
<MuiThemeProvider theme={muiTheme}> <MuiThemeProvider theme={muiTheme}>
<ThemeContext.Provider <ThemeContext.Provider value={{ mode, changeMode, theme: themeStyle }}>
value={useMemo(
() => ({ mode, changeMode, theme: themeStyle }),
[changeMode, mode, themeStyle]
)}
>
<Global <Global
styles={css` styles={css`
:root { :root {

View File

@ -0,0 +1,51 @@
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

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

View File

@ -0,0 +1,28 @@
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

@ -0,0 +1,9 @@
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

@ -2,7 +2,7 @@ import { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
import { Page, Workspace } from '@blocksuite/store'; import { Page, Workspace } from '@blocksuite/store';
import { GlobalActionsCreator } from '..'; import { GlobalActionsCreator } from '@/store/app';
export interface BlockSuiteState { export interface BlockSuiteState {
currentWorkspace: Workspace | null; currentWorkspace: Workspace | null;

View File

@ -0,0 +1,142 @@
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 => ({
dataCenter: null!,
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 = 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 +1,89 @@
export * from '@affine/store'; 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);
}) as any;
export const GlobalAppProvider: React.FC<React.PropsWithChildren> =
function ModelProvider({ children }) {
return (
<GlobalStateContext.Provider value={useMemo(() => create(), [])}>
{children}
</GlobalStateContext.Provider>
);
};

View File

@ -1,8 +1,7 @@
import { User } from '@affine/datacenter'; import { User } from '@affine/datacenter';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { GlobalActionsCreator } from '..'; import { GlobalActionsCreator } from '@/store/app';
import { dataCenterPromise } from '../datacenter';
export interface UserState { export interface UserState {
user: User | null; user: User | null;
@ -28,8 +27,7 @@ export const createUserActions: GlobalActionsCreator<UserActions> = (
) => { ) => {
return { return {
login: async () => { login: async () => {
const { currentDataCenterWorkspace: workspace } = get(); const { dataCenter, currentDataCenterWorkspace: workspace } = get();
const dataCenter = await dataCenterPromise;
try { try {
await dataCenter.login(); await dataCenter.login();
const user = (await dataCenter.getUserInfo()) as User; const user = (await dataCenter.getUserInfo()) as User;
@ -58,7 +56,7 @@ export const createUserActions: GlobalActionsCreator<UserActions> = (
} }
}, },
logout: async () => { logout: async () => {
const dataCenter = await dataCenterPromise; const { dataCenter } = get();
await dataCenter.logout(); await dataCenter.logout();
logger.debug('logout success'); logger.debug('logout success');
set({ user: null }); set({ user: null });

View File

@ -2,13 +2,11 @@ import { ThemeMode } from '../types';
export class LocalStorageThemeHelper { export class LocalStorageThemeHelper {
name = 'Affine-theme-mode'; name = 'Affine-theme-mode';
callback = new Set<() => void>();
get = (): ThemeMode | null => { get = (): ThemeMode | null => {
return localStorage.getItem(this.name) as ThemeMode | null; return localStorage.getItem(this.name) as ThemeMode | null;
}; };
set = (mode: ThemeMode) => { set = (mode: ThemeMode) => {
localStorage.setItem(this.name, mode); localStorage.setItem(this.name, mode);
this.callback.forEach(cb => cb());
}; };
} }

View File

@ -6,6 +6,17 @@ const _initializeDataCenter = () => {
return () => { return () => {
if (!_dataCenterInstance) { if (!_dataCenterInstance) {
_dataCenterInstance = DataCenter.init(); _dataCenterInstance = DataCenter.init();
_dataCenterInstance.then(dc => {
try {
if (window) {
(window as any).dc = dc;
}
} catch (_) {
// ignore
}
return dc;
});
} }
return _dataCenterInstance; return _dataCenterInstance;
@ -14,7 +25,7 @@ const _initializeDataCenter = () => {
export const getDataCenter = _initializeDataCenter(); export const getDataCenter = _initializeDataCenter();
export { DataCenter }; export type { DataCenter };
export * from './message'; export * from './message';
export { AffineProvider } from './provider/affine'; export { AffineProvider } from './provider/affine';
export * from './provider/affine/apis'; export * from './provider/affine/apis';

View File

@ -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"
}
}

View File

@ -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}</>;
}

View File

@ -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>
);
};

View File

@ -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';

View File

@ -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');
});
});

File diff suppressed because it is too large Load Diff

View File

@ -8,3 +8,4 @@ if [ "$1" == "--latest" ]; then
else else
pnpm up "@blocksuite/*@${1}" "!@blocksuite/icons" -r pnpm up "@blocksuite/*@${1}" "!@blocksuite/icons" -r
fi fi

View File

@ -6,7 +6,7 @@ import { test } from './libs/playwright';
loadPage(); loadPage();
test.describe('Local first export page', () => { test.describe('Local first export page', () => {
test('New a page ,then open it and export html', async ({ page }) => { test.skip('New a page ,then open it and export html', async ({ page }) => {
await newPage(page); await newPage(page);
await page.getByPlaceholder('Title').click(); await page.getByPlaceholder('Title').click();
await page await page
@ -34,7 +34,9 @@ test.describe('Local first export page', () => {
); );
}); });
test('New a page ,then open it and export markdown', async ({ page }) => { test.skip('New a page ,then open it and export markdown', async ({
page,
}) => {
await newPage(page); await newPage(page);
await page.getByPlaceholder('Title').click(); await page.getByPlaceholder('Title').click();
await page await page

View File

@ -1,13 +1,8 @@
import path from 'node:path';
import react from '@vitejs/plugin-react';
import { fileURLToPath } from 'url';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
export default defineConfig({ export default defineConfig({
plugins: [react()],
test: { test: {
include: ['packages/**/*.spec.ts', 'packages/**/*.spec.tsx'], include: ['packages/**/*.spec.ts'],
testTimeout: 5000, testTimeout: 5000,
coverage: { coverage: {
provider: 'istanbul', // or 'c8' provider: 'istanbul', // or 'c8'
@ -15,11 +10,4 @@ export default defineConfig({
reportsDirectory: '.coverage/store', reportsDirectory: '.coverage/store',
}, },
}, },
resolve: {
alias: {
'@affine/store': path.resolve(
fileURLToPath(new URL('packages/store', import.meta.url))
),
},
},
}); });