feat(core): support creating cloud workspaces to different servers (#9006)

This commit is contained in:
JimmFly
2024-12-04 06:39:13 +00:00
parent dddefcf768
commit 1fa1a95c10
24 changed files with 621 additions and 67 deletions

View File

@@ -23,6 +23,8 @@ import * as styles from './dialog.css';
interface NameWorkspaceContentProps extends ConfirmModalProps {
loading: boolean;
forcedCloud?: boolean;
serverId?: string;
onConfirmName: (
name: string,
workspaceFlavour: string,
@@ -33,15 +35,14 @@ interface NameWorkspaceContentProps extends ConfirmModalProps {
const NameWorkspaceContent = ({
loading,
onConfirmName,
forcedCloud,
serverId,
...props
}: NameWorkspaceContentProps) => {
const t = useI18n();
const [workspaceName, setWorkspaceName] = useState('');
const featureFlagService = useService(FeatureFlagService);
const enableLocalWorkspace = useLiveData(
featureFlagService.flags.enable_local_workspace.$
);
const [enable, setEnable] = useState(!enableLocalWorkspace);
const [enable, setEnable] = useState(!!forcedCloud);
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
@@ -62,8 +63,8 @@ const NameWorkspaceContent = ({
);
const handleCreateWorkspace = useCallback(() => {
onConfirmName(workspaceName, enable ? 'affine-cloud' : 'local');
}, [enable, onConfirmName, workspaceName]);
onConfirmName(workspaceName, enable ? serverId || 'affine-cloud' : 'local');
}, [enable, onConfirmName, serverId, workspaceName]);
const onEnter = useCallback(() => {
if (workspaceName) {
@@ -120,7 +121,7 @@ const NameWorkspaceContent = ({
<Switch
checked={enable}
onChange={onSwitchChange}
disabled={!enableLocalWorkspace}
disabled={forcedCloud}
/>
</div>
<div className={styles.cardDescription}>
@@ -131,7 +132,7 @@ const NameWorkspaceContent = ({
<CloudSvg />
</div>
</div>
{!enableLocalWorkspace ? (
{forcedCloud ? (
<a
className={styles.cloudTips}
href={BUILD_CONFIG.downloadUrl}
@@ -147,9 +148,15 @@ const NameWorkspaceContent = ({
};
export const CreateWorkspaceDialog = ({
forcedCloud,
serverId,
close,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
const workspacesService = useService(WorkspacesService);
const featureFlagService = useService(FeatureFlagService);
const enableLocalWorkspace = useLiveData(
featureFlagService.flags.enable_local_workspace.$
);
const [loading, setLoading] = useState(false);
const onConfirmName = useAsyncCallback(
@@ -185,6 +192,8 @@ export const CreateWorkspaceDialog = ({
<NameWorkspaceContent
loading={loading}
open
serverId={serverId}
forcedCloud={forcedCloud || !enableLocalWorkspace}
onOpenChange={onOpenChange}
onConfirmName={onConfirmName}
/>

View File

@@ -0,0 +1,86 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const dialogContainer = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
color: cssVarV2('text/primary'),
padding: '16px',
});
export const mainIcon = style({
width: 36,
height: 36,
color: cssVarV2('icon/primary'),
});
export const mainTitle = style({
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
marginTop: '16px',
fontWeight: 600,
});
export const desc = style({
textAlign: 'center',
color: cssVarV2('text/secondary'),
marginBottom: '20px',
});
export const mainButton = style({
width: '100%',
fontSize: '14px',
height: '42px',
});
export const modal = style({
maxWidth: '352px',
});
export const workspaceSelector = style({
margin: '0 -16px',
width: 'calc(100% + 32px)',
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
padding: '0 16px',
});
export const root = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
gap: '20px',
});
export const textContainer = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
});
export const title = style({
fontSize: cssVar('fontH6'),
fontWeight: 600,
lineHeight: '26px',
});
export const description = style({
fontSize: cssVar('fontBase'),
fontWeight: 400,
lineHeight: '24px',
color: cssVar('textSecondaryColor'),
});
export const serverSelector = style({
width: '100%',
});
export const button = style({
width: '100%',
marginTop: '20px',
});

View File

@@ -0,0 +1,169 @@
import { Button, Modal, notify } from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { ServerSelector } from '@affine/core/components/server-selector';
import {
AuthService,
type Server,
ServersService,
} from '@affine/core/modules/cloud';
import {
type DialogComponentProps,
type GLOBAL_DIALOG_SCHEMA,
GlobalDialogService,
} from '@affine/core/modules/dialogs';
import { useI18n } from '@affine/i18n';
import { CloudWorkspaceIcon } from '@blocksuite/icons/rc';
import {
FrameworkScope,
useLiveData,
useService,
WorkspacesService,
} from '@toeverything/infra';
import { useCallback, useState } from 'react';
import * as styles from './dialog.css';
const Dialog = ({
workspaceId,
close,
selectedServer,
setSelectedServer,
serverList,
openPageId,
}: {
workspaceId: string;
serverList: Server[];
selectedServer: Server;
setSelectedServer: (server: Server) => void;
openPageId?: string;
serverId?: string;
close?: () => void;
}) => {
const t = useI18n();
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
const loginStatus = useLiveData(useService(AuthService).session.status$);
const globalDialogService = useService(GlobalDialogService);
const workspacesService = useService(WorkspacesService);
const workspaceMeta = useLiveData(
workspacesService.list.workspace$(workspaceId)
);
const { workspace } = workspaceMeta
? workspacesService.open({ metadata: workspaceMeta })
: { workspace: undefined };
const { jumpToPage } = useNavigateHelper();
const enableCloud = useCallback(async () => {
try {
if (!workspace) return;
if (!account) return;
const { id: newId } = await workspacesService.transformLocalToCloud(
workspace,
account.id,
selectedServer.id
);
jumpToPage(newId, openPageId || 'all');
close?.();
} catch (e) {
console.error(e);
notify.error({
title: t['com.affine.workspace.enable-cloud.failed'](),
});
}
}, [
workspace,
account,
workspacesService,
selectedServer.id,
jumpToPage,
openPageId,
close,
t,
]);
const openSignIn = useCallback(() => {
globalDialogService.open('sign-in', {
server: selectedServer.baseUrl,
});
}, [globalDialogService, selectedServer.baseUrl]);
const signInOrEnableCloud = useAsyncCallback(async () => {
// not logged in, open login modal
if (loginStatus === 'unauthenticated') {
openSignIn();
}
if (loginStatus === 'authenticated') {
await enableCloud();
}
}, [enableCloud, loginStatus, openSignIn]);
return (
<div className={styles.root}>
<CloudWorkspaceIcon width={'36px'} height={'36px'} />
<div className={styles.textContainer}>
<div className={styles.title}>
{t['com.affine.enableAffineCloudModal.custom-server.title']({
workspaceName: workspace?.name$.value || 'untitled',
})}
</div>
<div className={styles.description}>
{t['com.affine.enableAffineCloudModal.custom-server.description']()}
</div>
</div>
<div className={styles.serverSelector}>
<ServerSelector
servers={serverList}
selectedSeverName={`${selectedServer.config$.value.serverName} (${selectedServer.baseUrl})`}
onSelect={setSelectedServer}
/>
</div>
<Button
className={styles.button}
onClick={signInOrEnableCloud}
size="extraLarge"
variant="primary"
>
{t['com.affine.enableAffineCloudModal.custom-server.enable']()}
</Button>
</div>
);
};
export const EnableCloudDialog = ({
workspaceId,
openPageId,
serverId,
close,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['enable-cloud']>) => {
const serversService = useService(ServersService);
const serverList = useLiveData(serversService.servers$);
const [selectedServer, setSelectedServer] = useState<Server>(serverList[0]);
return (
<Modal
open
modal={true}
persistent
contentOptions={{
className: styles.modal,
}}
onOpenChange={() => close()}
>
<FrameworkScope key={selectedServer.id} scope={selectedServer.scope}>
<Dialog
workspaceId={workspaceId}
openPageId={openPageId}
serverId={serverId}
close={close}
serverList={serverList}
selectedServer={selectedServer}
setSelectedServer={setSelectedServer}
/>
</FrameworkScope>
</Modal>
);
};

View File

@@ -11,6 +11,7 @@ import { ChangePasswordDialog } from './change-password';
import { CollectionEditorDialog } from './collection-editor';
import { CreateWorkspaceDialog } from './create-workspace';
import { DocInfoDialog } from './doc-info';
import { EnableCloudDialog } from './enable-cloud';
import { ImportDialog } from './import';
import { ImportTemplateDialog } from './import-template';
import { ImportWorkspaceDialog } from './import-workspace';
@@ -30,6 +31,7 @@ const GLOBAL_DIALOGS = {
'sign-in': SignInDialog,
'change-password': ChangePasswordDialog,
'verify-email': VerifyEmailDialog,
'enable-cloud': EnableCloudDialog,
} satisfies {
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>

View File

@@ -1,5 +1,5 @@
import { Modal } from '@affine/component';
import { SignInPanel } from '@affine/core/components/sign-in';
import { SignInPanel, type SignInStep } from '@affine/core/components/sign-in';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
@@ -7,6 +7,7 @@ import type {
export const SignInDialog = ({
close,
server: initialServerBaseUrl,
step,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['sign-in']>) => {
return (
<Modal
@@ -19,7 +20,11 @@ export const SignInDialog = ({
style: { padding: '44px 40px 20px' },
}}
>
<SignInPanel onClose={close} server={initialServerBaseUrl} />
<SignInPanel
onClose={close}
server={initialServerBaseUrl}
initStep={step as SignInStep}
/>
</Modal>
);
};