mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-21 08:17:10 +08:00
feat(core): desktop multiple server support (#8979)
This commit is contained in:
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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('/')} />;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user