feat(core): desktop multiple server support (#8979)

This commit is contained in:
EYHN
2024-12-03 05:51:09 +00:00
parent af81c95b85
commit 8963826463
137 changed files with 2052 additions and 1694 deletions

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,17 @@
import { SignInPanel } from '@affine/core/components/sign-in';
import { MobileSignInLayout } from './layout';
export const MobileSignInPanel = ({
onClose,
server,
}: {
onClose: () => void;
server?: string;
}) => {
return (
<MobileSignInLayout>
<SignInPanel onClose={onClose} server={server} />
</MobileSignInLayout>
);
};

View File

@@ -2,7 +2,6 @@ import { IconButton } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { CloseIcon, CollaborationIcon } from '@blocksuite/icons/rc';
import {
useLiveData,
@@ -16,10 +15,8 @@ import { type HTMLAttributes, useCallback, useMemo } from 'react';
import * as styles from './menu.css';
const filterByFlavour = (
workspaces: WorkspaceMetadata[],
flavour: WorkspaceFlavour
) => workspaces.filter(ws => flavour === ws.flavour);
const filterByFlavour = (workspaces: WorkspaceMetadata[], flavour: string) =>
workspaces.filter(ws => flavour === ws.flavour);
const WorkspaceItem = ({
workspace,
@@ -93,13 +90,14 @@ export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => {
const workspacesService = useService(WorkspacesService);
const workspaces = useLiveData(workspacesService.list.workspaces$);
// TODO: support selfhosted
const cloudWorkspaces = useMemo(
() => filterByFlavour(workspaces, WorkspaceFlavour.AFFINE_CLOUD),
() => filterByFlavour(workspaces, 'affine-cloud'),
[workspaces]
);
const localWorkspaces = useMemo(
() => filterByFlavour(workspaces, WorkspaceFlavour.LOCAL),
() => filterByFlavour(workspaces, 'local'),
[workspaces]
);

View File

@@ -1,4 +1,3 @@
import { AuthModal } from '@affine/core/components/affine/auth';
import {
type DialogComponentProps,
type GLOBAL_DIALOG_SCHEMA,
@@ -12,6 +11,7 @@ import { CollectionSelectorDialog } from './selectors/collection-selector';
import { DocSelectorDialog } from './selectors/doc-selector';
import { TagSelectorDialog } from './selectors/tag-selector';
import { SettingDialog } from './setting';
import { SignInDialog } from './sign-in';
const GLOBAL_DIALOGS = {
// 'create-workspace': CreateWorkspaceDialog,
@@ -19,6 +19,7 @@ const GLOBAL_DIALOGS = {
// 'import-template': ImportTemplateDialog,
setting: SettingDialog,
// import: ImportDialog,
'sign-in': SignInDialog,
} satisfies {
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
@@ -58,8 +59,6 @@ export const GlobalDialogs = () => {
/>
);
})}
<AuthModal />
</>
);
};

View File

@@ -1,14 +1,13 @@
import { Avatar } from '@affine/component';
import { authAtom } from '@affine/core/components/atoms';
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
import { AuthService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import {
useEnsureLiveData,
useLiveData,
useService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { type ReactNode } from 'react';
import { UserPlanTag } from '../../../components';
@@ -79,11 +78,11 @@ const AuthorizedUserProfile = () => {
};
const UnauthorizedUserProfile = () => {
const setAuthModal = useSetAtom(authAtom);
const globalDialogService = useService(GlobalDialogService);
return (
<BaseLayout
onClick={() => setAuthModal({ openModal: true, state: 'signIn' })}
onClick={() => globalDialogService.open('sign-in', {})}
avatar={<Avatar size={48} rounded={4} />}
title="Sign up / Sign in"
caption="Sync with AFFiNE Cloud"

View File

@@ -1,31 +1,23 @@
import { IconButton, Modal, SafeArea } from '@affine/component';
import { authAtom } from '@affine/core/components/atoms';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs';
import { CloseIcon } from '@blocksuite/icons/rc';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import { MobileSignIn } from './mobile-sign-in';
export const MobileSignInModal = () => {
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
const setOpen = useCallback(
(open: boolean) => {
setAuthAtom(prev => ({ ...prev, openModal: open }));
},
[setAuthAtom]
);
const closeModal = useCallback(() => {
setOpen(false);
}, [setOpen]);
import { MobileSignInPanel } from '../../components/sign-in';
export const SignInDialog = ({
close,
server: initialServerBaseUrl,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
return (
<Modal
fullScreen
animation="slideBottom"
open={authAtomValue.openModal}
onOpenChange={setOpen}
open
onOpenChange={() => close()}
contentOptions={{
style: {
padding: 0,
@@ -35,7 +27,7 @@ export const MobileSignInModal = () => {
}}
withoutCloseButton
>
<MobileSignIn onSkip={closeModal} />
<MobileSignInPanel onClose={close} server={initialServerBaseUrl} />
<SafeArea
top
style={{ position: 'absolute', top: 0, right: 0, paddingRight: 16 }}
@@ -46,7 +38,7 @@ export const MobileSignInModal = () => {
variant="solid"
icon={<CloseIcon />}
style={{ borderRadius: 8, padding: 4 }}
onClick={closeModal}
onClick={() => close()}
/>
</SafeArea>
</Modal>

View File

@@ -5,7 +5,6 @@ import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import { GlobalDialogs } from '../../dialogs';
import { MobileSignInModal } from '../../views/sign-in/modal';
export const RootWrapper = () => {
const defaultServerService = useService(DefaultServerService);
@@ -33,7 +32,6 @@ export const RootWrapper = () => {
<FrameworkScope scope={defaultServerService.server.scope}>
<GlobalDialogs />
<NotificationCenter />
<MobileSignInModal />
<Outlet />
</FrameworkScope>
);

View File

@@ -1,38 +1,10 @@
import {
RouteLogic,
useNavigateHelper,
} from '@affine/core/components/hooks/use-navigate-helper';
import { AuthService } from '@affine/core/modules/cloud';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { MobileSignIn } from '../views/sign-in/mobile-sign-in';
import { MobileSignInPanel } from '../components/sign-in';
export const Component = () => {
const session = useService(AuthService).session;
const status = useLiveData(session.status$);
const isRevalidating = useLiveData(session.isRevalidating$);
const navigate = useNavigate();
const { jumpToIndex } = useNavigateHelper();
const [searchParams] = useSearchParams();
const isLoggedIn = status === 'authenticated' && !isRevalidating;
useEffect(() => {
if (isLoggedIn) {
const redirectUri = searchParams.get('redirect_uri');
if (redirectUri) {
navigate(redirectUri, {
replace: true,
});
} else {
jumpToIndex(RouteLogic.REPLACE, {
search: searchParams.toString(),
});
}
}
}, [jumpToIndex, navigate, isLoggedIn, searchParams]);
return <MobileSignIn onSkip={() => navigate('/')} />;
return <MobileSignInPanel onClose={() => navigate('/')} />;
};

View File

@@ -1,7 +1,6 @@
import { IconButton, MobileMenu } from '@affine/component';
import { SharePage } from '@affine/core/components/affine/share-page-modal/share-menu/share-page';
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { ShareiOsIcon } from '@blocksuite/icons/rc';
import { DocService, useServices, WorkspaceService } from '@toeverything/infra';
@@ -16,7 +15,7 @@ export const PageHeaderShareButton = () => {
const doc = docService.doc.blockSuiteDoc;
const confirmEnableCloud = useEnableCloud();
if (workspace.meta.flavour === WorkspaceFlavour.LOCAL) {
if (workspace.meta.flavour === 'local') {
return null;
}

View File

@@ -6,8 +6,11 @@ import {
} from '@affine/core/components/affine/quota-reached-modal';
import { SWRConfigProvider } from '@affine/core/components/providers/swr-config-provider';
import { WorkspaceSideEffects } from '@affine/core/components/providers/workspace-side-effects';
import {
DefaultServerService,
WorkspaceServerService,
} from '@affine/core/modules/cloud';
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
import {
FrameworkScope,
@@ -47,12 +50,15 @@ export const WorkspaceLayout = ({
children,
}: PropsWithChildren<{ meta: WorkspaceMetadata }>) => {
// todo: reduce code duplication with packages\frontend\core\src\pages\workspace\index.tsx
const { workspacesService, globalContextService } = useServices({
WorkspacesService,
GlobalContextService,
});
const { workspacesService, globalContextService, defaultServerService } =
useServices({
WorkspacesService,
GlobalContextService,
DefaultServerService,
});
const [workspace, setWorkspace] = useState<Workspace | null>(null);
const workspaceServer = workspace?.scope.get(WorkspaceServerService)?.server;
useLayoutEffect(() => {
const ref = workspacesService.open({ metadata: meta });
@@ -75,13 +81,30 @@ export const WorkspaceLayout = ({
);
localStorage.setItem('last_workspace_id', workspace.id);
globalContextService.globalContext.workspaceId.set(workspace.id);
if (workspaceServer) {
globalContextService.globalContext.serverId.set(workspaceServer.id);
}
globalContextService.globalContext.workspaceFlavour.set(
workspace.flavour
);
return () => {
window.currentWorkspace = undefined;
globalContextService.globalContext.workspaceId.set(null);
if (workspaceServer) {
globalContextService.globalContext.serverId.set(
defaultServerService.server.id
);
}
globalContextService.globalContext.workspaceFlavour.set(null);
};
}
return;
}, [globalContextService, workspace]);
}, [
defaultServerService.server.id,
globalContextService,
workspace,
workspaceServer,
]);
const isRootDocReady =
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
@@ -95,22 +118,25 @@ export const WorkspaceLayout = ({
}
return (
<FrameworkScope scope={workspace.scope}>
<AffineErrorBoundary height="100dvh">
<SWRConfigProvider>
<WorkspaceDialogs />
<FrameworkScope scope={workspaceServer?.scope}>
<FrameworkScope scope={workspace.scope}>
<AffineErrorBoundary height="100dvh">
<SWRConfigProvider>
<WorkspaceDialogs />
{/* ---- some side-effect components ---- */}
<PeekViewManagerModal />
{workspace?.flavour === WorkspaceFlavour.LOCAL && <LocalQuotaModal />}
{workspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
<CloudQuotaModal />
)}
<AiLoginRequiredModal />
<WorkspaceSideEffects />
{children}
</SWRConfigProvider>
</AffineErrorBoundary>
{/* ---- some side-effect components ---- */}
<PeekViewManagerModal />
{workspace?.flavour === 'local' ? (
<LocalQuotaModal />
) : (
<CloudQuotaModal />
)}
<AiLoginRequiredModal />
<WorkspaceSideEffects />
{children}
</SWRConfigProvider>
</AffineErrorBoundary>
</FrameworkScope>
</FrameworkScope>
);
};

View File

@@ -1,11 +0,0 @@
import { AuthPanel } from '@affine/core/components/affine/auth';
import { MobileSignInLayout } from './layout';
export const MobileSignIn = ({ onSkip }: { onSkip: () => void }) => {
return (
<MobileSignInLayout>
<AuthPanel onSkip={onSkip} />
</MobileSignInLayout>
);
};