mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-23 09:17:06 +08:00
feat(core): desktop multiple server support (#8979)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
25
packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Normal file
25
packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user