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

@@ -0,0 +1,158 @@
import { Button, Modal, notify, Wrapper } from '@affine/component';
import {
AuthContent,
AuthInput,
ModalHeader,
} from '@affine/component/auth-components';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import {
AuthService,
DefaultServerService,
ServersService,
} from '@affine/core/modules/cloud';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs';
import { Unreachable } from '@affine/env/constant';
import {
sendChangePasswordEmailMutation,
sendSetPasswordEmailMutation,
} from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useState } from 'react';
export const ChangePasswordDialog = ({
close,
server: serverBaseUrl,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['change-password']>) => {
const t = useI18n();
const defaultServerService = useService(DefaultServerService);
const serversService = useService(ServersService);
let server;
if (serverBaseUrl) {
server = serversService.getServerByBaseUrl(serverBaseUrl);
if (!server) {
throw new Unreachable('Server not found');
}
} else {
server = defaultServerService.server;
}
const authService = server.scope.get(AuthService);
const account = useLiveData(authService.session.account$);
const email = account?.email;
const hasPassword = account?.info?.hasPassword;
const [hasSentEmail, setHasSentEmail] = useState(false);
const [loading, setLoading] = useState(false);
const passwordLimits = useLiveData(
server.credentialsRequirement$.map(r => r?.password)
);
const serverName = useLiveData(server.config$.selector(c => c.serverName));
if (!email) {
// should not happen
throw new Unreachable();
}
const onSendEmail = useAsyncCallback(async () => {
setLoading(true);
try {
if (hasPassword) {
await server.gql({
query: sendChangePasswordEmailMutation,
variables: {
callbackUrl: `/auth/changePassword`,
},
});
} else {
await server.gql({
query: sendSetPasswordEmailMutation,
variables: {
callbackUrl: `/auth/setPassword`,
},
});
}
notify.success({
title: hasPassword
? t['com.affine.auth.sent.change.password.hint']()
: t['com.affine.auth.sent.set.password.hint'](),
});
setHasSentEmail(true);
} catch (err) {
console.error(err);
notify.error({
title: t['com.affine.auth.sent.change.email.fail'](),
});
} finally {
setLoading(false);
}
}, [hasPassword, server, t]);
if (!passwordLimits) {
// TODO(@eyhn): loading & error UI
return null;
}
return (
<Modal
open
onOpenChange={() => close()}
width={400}
minHeight={500}
contentOptions={{
['data-testid' as string]: 'change-password-modal',
style: { padding: '44px 40px 20px' },
}}
>
<ModalHeader
title={serverName}
subTitle={
hasPassword
? t['com.affine.auth.reset.password']()
: t['com.affine.auth.set.password']()
}
/>
<AuthContent>
{hasPassword
? t['com.affine.auth.reset.password.message']()
: t['com.affine.auth.set.password.message']({
min: String(passwordLimits.minLength),
max: String(passwordLimits.maxLength),
})}
</AuthContent>
<Wrapper
marginTop={30}
marginBottom={50}
style={{
position: 'relative',
}}
>
<AuthInput
label={t['com.affine.settings.email']()}
disabled={true}
value={email}
/>
</Wrapper>
<Button
variant="primary"
size="extraLarge"
style={{ width: '100%' }}
disabled={hasSentEmail}
loading={loading}
onClick={onSendEmail}
>
{hasSentEmail
? t['com.affine.auth.sent']()
: hasPassword
? t['com.affine.auth.send.reset.password.link']()
: t['com.affine.auth.send.set.password.link']()}
</Button>
</Modal>
);
};

View File

@@ -1,14 +1,13 @@
import { Avatar, ConfirmModal, Input, Switch } from '@affine/component';
import type { ConfirmModalProps } from '@affine/component/ui/modal';
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
import { authAtom } from '@affine/core/components/atoms';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AuthService } from '@affine/core/modules/cloud';
import {
type DialogComponentProps,
type GLOBAL_DIALOG_SCHEMA,
GlobalDialogService,
} from '@affine/core/modules/dialogs';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
@@ -17,7 +16,6 @@ import {
useService,
WorkspacesService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react';
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
@@ -27,7 +25,7 @@ interface NameWorkspaceContentProps extends ConfirmModalProps {
loading: boolean;
onConfirmName: (
name: string,
workspaceFlavour: WorkspaceFlavour,
workspaceFlavour: string,
avatar?: File
) => void;
}
@@ -47,14 +45,11 @@ const NameWorkspaceContent = ({
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
const setOpenSignIn = useSetAtom(authAtom);
const globalDialogService = useService(GlobalDialogService);
const openSignInModal = useCallback(() => {
setOpenSignIn(state => ({
...state,
openModal: true,
}));
}, [setOpenSignIn]);
globalDialogService.open('sign-in', {});
}, [globalDialogService]);
const onSwitchChange = useCallback(
(checked: boolean) => {
@@ -67,10 +62,7 @@ const NameWorkspaceContent = ({
);
const handleCreateWorkspace = useCallback(() => {
onConfirmName(
workspaceName,
enable ? WorkspaceFlavour.AFFINE_CLOUD : WorkspaceFlavour.LOCAL
);
onConfirmName(workspaceName, enable ? 'affine-cloud' : 'local');
}, [enable, onConfirmName, workspaceName]);
const onEnter = useCallback(() => {
@@ -161,7 +153,7 @@ export const CreateWorkspaceDialog = ({
const [loading, setLoading] = useState(false);
const onConfirmName = useAsyncCallback(
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
async (name: string, workspaceFlavour: string) => {
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
if (loading) return;
setLoading(true);

View File

@@ -12,7 +12,6 @@ import {
ImportTemplateService,
TemplateDownloaderService,
} from '@affine/core/modules/import-template';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import type { DocMode } from '@blocksuite/affine/blocks';
import { AllDocsIcon } from '@blocksuite/icons/rc';
@@ -56,7 +55,7 @@ const Dialog = ({
useState<WorkspaceMetadata | null>(null);
const selectedWorkspace =
rawSelectedWorkspace ??
workspaces.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
workspaces.find(w => w.flavour !== 'local') ??
workspaces.at(0);
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
const { openPage, jumpToSignIn } = useNavigateHelper();
@@ -146,7 +145,8 @@ const Dialog = ({
try {
const { workspaceId, docId } =
await importTemplateService.importToNewWorkspace(
WorkspaceFlavour.AFFINE_CLOUD,
// TODO: support selfhosted
'affine-cloud',
'Workspace',
templateDownloader.data$.value
);

View File

@@ -6,7 +6,6 @@ import {
import { _addLocalWorkspace } from '@affine/core/modules/workspace-engine';
import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import { useService, WorkspacesService } from '@toeverything/infra';
import { useLayoutEffect } from 'react';
@@ -37,7 +36,7 @@ export const ImportWorkspaceDialog = ({
workspacesService.list.revalidate();
close({
workspace: {
flavour: WorkspaceFlavour.LOCAL,
flavour: 'local',
id: result.workspaceId,
},
});

View File

@@ -1,4 +1,3 @@
import { AuthModal } from '@affine/core/components/affine/auth';
import {
type DialogComponentProps,
type GLOBAL_DIALOG_SCHEMA,
@@ -8,6 +7,7 @@ import {
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
import { useLiveData, useService } from '@toeverything/infra';
import { ChangePasswordDialog } from './change-password';
import { CollectionEditorDialog } from './collection-editor';
import { CreateWorkspaceDialog } from './create-workspace';
import { DocInfoDialog } from './doc-info';
@@ -19,12 +19,17 @@ import { DateSelectorDialog } from './selectors/date';
import { DocSelectorDialog } from './selectors/doc';
import { TagSelectorDialog } from './selectors/tag';
import { SettingDialog } from './setting';
import { SignInDialog } from './sign-in';
import { VerifyEmailDialog } from './verify-email';
const GLOBAL_DIALOGS = {
'create-workspace': CreateWorkspaceDialog,
'import-workspace': ImportWorkspaceDialog,
'import-template': ImportTemplateDialog,
setting: SettingDialog,
'sign-in': SignInDialog,
'change-password': ChangePasswordDialog,
'verify-email': VerifyEmailDialog,
} satisfies {
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
@@ -66,8 +71,6 @@ export const GlobalDialogs = () => {
/>
);
})}
<AuthModal />
</>
);
};

View File

@@ -5,11 +5,11 @@ import {
} from '@affine/component/setting-components';
import { Avatar } from '@affine/component/ui/avatar';
import { Button } from '@affine/component/ui/button';
import { authAtom } from '@affine/core/components/atoms';
import { useSignOut } from '@affine/core/components/hooks/affine/use-sign-out';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
import { Upload } from '@affine/core/components/pure/file-upload';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
@@ -20,7 +20,6 @@ import {
useService,
useServices,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import { AuthService, ServerService } from '../../../../modules/cloud';
@@ -178,9 +177,10 @@ export const AccountSetting = ({
}: {
onChangeSettingState?: (settingState: SettingState) => void;
}) => {
const { authService, serverService } = useServices({
const { authService, serverService, globalDialogService } = useServices({
AuthService,
ServerService,
GlobalDialogService,
});
const serverFeatures = useLiveData(serverService.server.features$);
const t = useI18n();
@@ -189,28 +189,20 @@ export const AccountSetting = ({
session.revalidate();
}, [session]);
const account = useEnsureLiveData(session.account$);
const setAuthModal = useSetAtom(authAtom);
const openSignOutModal = useSignOut();
const onChangeEmail = useCallback(() => {
setAuthModal({
openModal: true,
state: 'sendEmail',
// @ts-expect-error accont email is always defined
email: account.email,
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
globalDialogService.open('verify-email', {
server: serverService.server.baseUrl,
changeEmail: !!account.info?.emailVerified,
});
}, [account.email, account.info?.emailVerified, setAuthModal]);
}, [account, globalDialogService, serverService.server.baseUrl]);
const onPasswordButtonClick = useCallback(() => {
setAuthModal({
openModal: true,
state: 'sendEmail',
// @ts-expect-error accont email is always defined
email: account.email,
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
globalDialogService.open('change-password', {
server: serverService.server.baseUrl,
});
}, [account.email, account.info?.hasPassword, setAuthModal]);
}, [globalDialogService, serverService.server.baseUrl]);
return (
<>

View File

@@ -1,19 +1,16 @@
import { Button, type ButtonProps } from '@affine/component';
import { authAtom } from '@affine/core/components/atoms';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { useI18n } from '@affine/i18n';
import { useSetAtom } from 'jotai';
import { useService } from '@toeverything/infra';
import { useCallback } from 'react';
export const AILogin = (btnProps: ButtonProps) => {
const t = useI18n();
const setOpen = useSetAtom(authAtom);
const globalDialogService = useService(GlobalDialogService);
const onClickSignIn = useCallback(() => {
setOpen(state => ({
...state,
openModal: true,
}));
}, [setOpen]);
globalDialogService.open('sign-in', {});
}, [globalDialogService]);
return (
<Button onClick={onClickSignIn} variant="primary" {...btnProps}>

View File

@@ -1,9 +1,9 @@
import { Button, type ButtonProps } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip';
import { authAtom } from '@affine/core/components/atoms';
import { generateSubscriptionCallbackLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import {
type CreateCheckoutSessionInput,
SubscriptionRecurring,
@@ -18,7 +18,6 @@ import { track } from '@affine/track';
import { DoneIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import type { PropsWithChildren } from 'react';
import { useCallback, useMemo, useState } from 'react';
@@ -387,14 +386,11 @@ export const SignUpAction = ({
children,
className,
}: PropsWithChildren<{ className?: string }>) => {
const setOpen = useSetAtom(authAtom);
const globalDialogService = useService(GlobalDialogService);
const onClickSignIn = useCallback(() => {
setOpen(state => ({
...state,
openModal: true,
}));
}, [setOpen]);
globalDialogService.open('sign-in', {});
}, [globalDialogService]);
return (
<Button

View File

@@ -6,11 +6,11 @@ import { Avatar } from '@affine/component/ui/avatar';
import { Tooltip } from '@affine/component/ui/tooltip';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
import { authAtom } from '@affine/core/components/atoms';
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
import { AuthService } from '@affine/core/modules/cloud';
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useI18n } from '@affine/i18n';
@@ -25,7 +25,6 @@ import {
WorkspacesService,
} from '@toeverything/infra';
import clsx from 'clsx';
import { useSetAtom } from 'jotai/react';
import {
type MouseEvent,
Suspense,
@@ -95,14 +94,14 @@ export const UserInfo = ({
export const SignInButton = () => {
const t = useI18n();
const setAuthModal = useSetAtom(authAtom);
const globalDialogService = useService(GlobalDialogService);
return (
<div
className={style.accountButton}
onClick={useCallback(() => {
setAuthModal({ openModal: true, state: 'signIn' });
}, [setAuthModal])}
globalDialogService.open('sign-in', {});
}, [globalDialogService])}
>
<div className="avatar not-sign">
<Logo1Icon />

View File

@@ -3,7 +3,6 @@ import type { ConfirmModalProps } from '@affine/component/ui/modal';
import { ConfirmModal } from '@affine/component/ui/modal';
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans, useI18n } from '@affine/i18n';
import type { WorkspaceMetadata } from '@toeverything/infra';
import { useCallback, useState } from 'react';
@@ -44,7 +43,7 @@ export const WorkspaceDeleteModal = ({
}}
{...props}
>
{workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
{workspaceMetadata.flavour === 'local' ? (
<Trans i18nKey="com.affine.workspaceDelete.description">
Deleting (
<span className={styles.workspaceName}>

View File

@@ -2,7 +2,6 @@ import { SettingRow } from '@affine/component/setting-components';
import { Button } from '@affine/component/ui/button';
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import {
useLiveData,
@@ -37,7 +36,7 @@ export const EnableCloudPanel = ({
});
}, [confirmEnableCloud, onCloseSetting, workspace]);
if (flavour !== WorkspaceFlavour.LOCAL) {
if (flavour !== 'local') {
return null;
}

View File

@@ -20,7 +20,6 @@ import {
WorkspacePermissionService,
} from '@affine/core/modules/permissions';
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Permission, UserFriendlyError } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
@@ -410,7 +409,7 @@ export const MembersPanel = ({
onChangeSettingState: (settingState: SettingState) => void;
}): ReactElement | null => {
const workspace = useService(WorkspaceService).workspace;
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
if (workspace.flavour === 'local') {
return <MembersPanelLocal />;
}
return (

View File

@@ -6,13 +6,12 @@ import {
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
export const SharingPanel = () => {
const workspace = useService(WorkspaceService).workspace;
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
if (workspace.flavour === 'local') {
return null;
}
return <Sharing />;

View File

@@ -0,0 +1,25 @@
import { Modal } from '@affine/component';
import { SignInPanel } from '@affine/core/components/sign-in';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs';
export const SignInDialog = ({
close,
server: initialServerBaseUrl,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
return (
<Modal
open
onOpenChange={() => close()}
width={400}
minHeight={500}
contentOptions={{
['data-testid' as string]: 'auth-modal',
style: { padding: '44px 40px 20px' },
}}
>
<SignInPanel onClose={close} server={initialServerBaseUrl} />
</Modal>
);
};

View File

@@ -0,0 +1,145 @@
import { Button, Modal, notify, Wrapper } from '@affine/component';
import {
AuthContent,
AuthInput,
ModalHeader,
} from '@affine/component/auth-components';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import {
AuthService,
DefaultServerService,
ServersService,
} from '@affine/core/modules/cloud';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs';
import { Unreachable } from '@affine/env/constant';
import {
sendChangeEmailMutation,
sendVerifyEmailMutation,
} from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useState } from 'react';
export const VerifyEmailDialog = ({
close,
server: serverBaseUrl,
changeEmail,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['verify-email']>) => {
const t = useI18n();
const defaultServerService = useService(DefaultServerService);
const serversService = useService(ServersService);
let server;
if (serverBaseUrl) {
server = serversService.getServerByBaseUrl(serverBaseUrl);
if (!server) {
throw new Unreachable('Server not found');
}
} else {
server = defaultServerService.server;
}
const authService = server.scope.get(AuthService);
const account = useLiveData(authService.session.account$);
const email = account?.email;
const [hasSentEmail, setHasSentEmail] = useState(false);
const [loading, setLoading] = useState(false);
const passwordLimits = useLiveData(
server.credentialsRequirement$.map(r => r?.password)
);
const serverName = useLiveData(server.config$.selector(c => c.serverName));
if (!email) {
// should not happen
throw new Unreachable();
}
const onSendEmail = useAsyncCallback(async () => {
setLoading(true);
try {
if (changeEmail) {
await server.gql({
query: sendChangeEmailMutation,
variables: {
callbackUrl: `/auth/changeEmail`,
},
});
} else {
await server.gql({
query: sendVerifyEmailMutation,
variables: {
callbackUrl: `/auth/verify-email`,
},
});
}
notify.success({
title: t['com.affine.auth.send.verify.email.hint'](),
});
setHasSentEmail(true);
} catch (err) {
console.error(err);
notify.error({
title: t['com.affine.auth.sent.change.email.fail'](),
});
} finally {
setLoading(false);
}
}, [changeEmail, server, t]);
if (!passwordLimits) {
// should never reach here
return null;
}
return (
<Modal
open
onOpenChange={() => close()}
width={400}
minHeight={500}
contentOptions={{
['data-testid' as string]: 'verify-email-modal',
style: { padding: '44px 40px 20px' },
}}
>
<ModalHeader
title={serverName}
subTitle={t['com.affine.settings.email.action.change']()}
/>
<AuthContent>
{t['com.affine.auth.verify.email.message']({ email })}
</AuthContent>
<Wrapper
marginTop={30}
marginBottom={50}
style={{
position: 'relative',
}}
>
<AuthInput
label={t['com.affine.settings.email']()}
disabled={true}
value={email}
/>
</Wrapper>
<Button
variant="primary"
size="extraLarge"
style={{ width: '100%' }}
disabled={hasSentEmail}
loading={loading}
onClick={onSendEmail}
>
{hasSentEmail
? t['com.affine.auth.sent']()
: t['com.affine.auth.send.verify.email.hint']()}
</Button>
</Modal>
);
};

View File

@@ -1,12 +1,14 @@
import { notify } from '@affine/component';
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
import { SignInPageContainer } from '@affine/component/auth-components';
import { SignInPanel } from '@affine/core/components/sign-in';
import { AuthService } from '@affine/core/modules/cloud';
import { useLiveData, useService } from '@toeverything/infra';
import { useI18n } from '@affine/i18n';
import { 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 { AuthPanel } from '../../../components/affine/auth';
import {
RouteLogic,
useNavigateHelper,
@@ -17,33 +19,39 @@ export const SignIn = ({
}: {
redirectUrl?: string;
}) => {
const t = useI18n();
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;
const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri');
const error = searchParams.get('error');
useEffect(() => {
if (isLoggedIn) {
if (redirectUrl) {
navigate(redirectUrl, {
replace: true,
});
} else {
jumpToIndex(RouteLogic.REPLACE, {
search: searchParams.toString(),
});
}
if (error) {
notify.error({
title: t['com.affine.auth.toast.title.failed'](),
message: error,
});
}
}, [jumpToIndex, navigate, isLoggedIn, redirectUrl, searchParams]);
}, [error, t]);
const handleClose = () => {
if (session.status$.value === 'authenticated' && redirectUrl) {
navigate(redirectUrl, {
replace: true,
});
} else {
jumpToIndex(RouteLogic.REPLACE, {
search: searchParams.toString(),
});
}
};
return (
<SignInPageContainer>
<div style={{ maxWidth: '400px', width: '100%' }}>
<AuthPanel onSkip={jumpToIndex} redirectUrl={redirectUrl} />
<SignInPanel onClose={handleClose} />
</div>
</SignInPageContainer>
);

View File

@@ -3,7 +3,6 @@ import {
buildShowcaseWorkspace,
createFirstAppData,
} from '@affine/core/utils/first-app-data';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
useLiveData,
useService,
@@ -59,11 +58,8 @@ export const Component = ({
const createCloudWorkspace = useCallback(() => {
if (createOnceRef.current) return;
createOnceRef.current = true;
buildShowcaseWorkspace(
workspacesService,
WorkspaceFlavour.AFFINE_CLOUD,
'AFFiNE Cloud'
)
// TODO: support selfhosted
buildShowcaseWorkspace(workspacesService, 'affine-cloud', 'AFFiNE Cloud')
.then(({ meta, defaultDocId }) => {
if (defaultDocId) {
jumpToPage(meta.id, defaultDocId);
@@ -86,15 +82,14 @@ export const Component = ({
// check is user logged in && has cloud workspace
if (searchParams.get('initCloud') === 'true') {
if (loggedIn) {
if (list.every(w => w.flavour !== WorkspaceFlavour.AFFINE_CLOUD)) {
if (list.every(w => w.flavour !== 'affine-cloud')) {
createCloudWorkspace();
return;
}
// open first cloud workspace
const openWorkspace =
list.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
list[0];
list.find(w => w.flavour === 'affine-cloud') ?? list[0];
openPage(openWorkspace.id, defaultIndexRoute);
} else {
return;

View File

@@ -1,6 +1,10 @@
import { NotificationCenter } from '@affine/component';
import { DefaultServerService } from '@affine/core/modules/cloud';
import { FrameworkScope, useService } from '@toeverything/infra';
import {
FrameworkScope,
GlobalContextService,
useService,
} from '@toeverything/infra';
import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
@@ -10,6 +14,7 @@ import { FindInPageModal } from './find-in-page/find-in-page-modal';
export const RootWrapper = () => {
const defaultServerService = useService(DefaultServerService);
const globalContextService = useService(GlobalContextService);
const [isServerReady, setIsServerReady] = useState(false);
useEffect(() => {
@@ -30,6 +35,15 @@ export const RootWrapper = () => {
};
}, [defaultServerService, isServerReady]);
useEffect(() => {
globalContextService.globalContext.serverId.set(
defaultServerService.server.id
);
return () => {
globalContextService.globalContext.serverId.set(null);
};
}, [defaultServerService, globalContextService]);
return (
<FrameworkScope scope={defaultServerService.server.scope}>
<GlobalDialogs />

View File

@@ -1,5 +1,9 @@
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
import { workbenchRoutes } from '@affine/core/desktop/workbench-router';
import {
DefaultServerService,
WorkspaceServerService,
} from '@affine/core/modules/cloud';
import { ZipTransformer } from '@blocksuite/affine/blocks';
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
import {
@@ -125,12 +129,15 @@ export const Component = (): ReactElement => {
};
const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
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 });
@@ -189,13 +196,30 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
};
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,
]);
if (!workspace) {
return null; // skip this, workspace will be set in layout effect
@@ -203,19 +227,23 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
if (!isRootDocReady) {
return (
<FrameworkScope scope={workspace.scope}>
<AppContainer fallback />
<FrameworkScope scope={workspaceServer?.scope}>
<FrameworkScope scope={workspace.scope}>
<AppContainer fallback />
</FrameworkScope>
</FrameworkScope>
);
}
return (
<FrameworkScope scope={workspace.scope}>
<AffineErrorBoundary height="100vh">
<WorkspaceLayout>
<WorkbenchRoot />
</WorkspaceLayout>
</AffineErrorBoundary>
<FrameworkScope scope={workspaceServer?.scope}>
<FrameworkScope scope={workspace.scope}>
<AffineErrorBoundary height="100vh">
<WorkspaceLayout>
<WorkbenchRoot />
</WorkspaceLayout>
</AffineErrorBoundary>
</FrameworkScope>
</FrameworkScope>
);
};

View File

@@ -11,7 +11,6 @@ import { AppContainer } from '@affine/core/desktop/components/app-container';
import { WorkspaceDialogs } from '@affine/core/desktop/dialogs';
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
LiveData,
useLiveData,
@@ -29,10 +28,9 @@ export const WorkspaceLayout = function WorkspaceLayout({
<WorkspaceDialogs />
{/* ---- some side-effect components ---- */}
{currentWorkspace?.flavour === WorkspaceFlavour.LOCAL && (
{currentWorkspace?.flavour === 'local' ? (
<LocalQuotaModal />
)}
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD && (
) : (
<CloudQuotaModal />
)}
<AiLoginRequiredModal />

View File

@@ -1,4 +1,3 @@
import { AuthModal } from '@affine/core/components/affine/auth';
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
@@ -30,7 +29,6 @@ export function ShareHeader({
snapshotUrl={snapshotUrl}
templateName={templateName}
/>
<AuthModal />
</div>
);
}

View File

@@ -21,7 +21,6 @@ import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
import { ShareReaderService } from '@affine/core/modules/share-doc';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import {
type DocMode,
@@ -170,7 +169,7 @@ const SharePageInner = ({
{
metadata: {
id: workspaceId,
flavour: WorkspaceFlavour.AFFINE_CLOUD,
flavour: 'affine-cloud',
},
isSharedMode: true,
},