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,14 +23,15 @@ export class WorkspaceTransformService extends Service {
*/ */
transformLocalToCloud = async ( transformLocalToCloud = async (
local: Workspace, local: Workspace,
accountId: string accountId: string,
flavour: string
): Promise<WorkspaceMetadata> => { ): Promise<WorkspaceMetadata> => {
assertEquals(local.flavour, 'local'); assertEquals(local.flavour, 'local');
const localDocStorage = local.engine.doc.storage.behavior; const localDocStorage = local.engine.doc.storage.behavior;
const newMetadata = await this.factory.create( const newMetadata = await this.factory.create(
'affine-cloud', flavour,
async (docCollection, blobStorage, docStorage) => { async (docCollection, blobStorage, docStorage) => {
const rootDocBinary = await localDocStorage.doc.get( const rootDocBinary = await localDocStorage.doc.get(
local.docCollection.doc.guid local.docCollection.doc.guid

View File

@@ -62,7 +62,7 @@ export function registerAffineCreationCommands({
run() { run() {
track.$.cmdk.workspace.createWorkspace(); track.$.cmdk.workspace.createWorkspace();
globalDialogService.open('create-workspace', undefined); globalDialogService.open('create-workspace', {});
}, },
}) })
); );

View File

@@ -1,5 +1,5 @@
import { notify, useConfirmModal } from '@affine/component'; import { notify, useConfirmModal } from '@affine/component';
import { AuthService } from '@affine/core/modules/cloud'; import { AuthService, ServersService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs'; import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { Workspace } from '@toeverything/infra'; import type { Workspace } from '@toeverything/infra';
@@ -22,6 +22,7 @@ interface ConfirmEnableCloudOptions {
*/ */
onFinished?: () => void; onFinished?: () => void;
openPageId?: string; openPageId?: string;
serverId?: string;
} }
type ConfirmEnableArgs = [Workspace, ConfirmEnableCloudOptions | undefined]; type ConfirmEnableArgs = [Workspace, ConfirmEnableCloudOptions | undefined];
@@ -33,6 +34,9 @@ export const useEnableCloud = () => {
const globalDialogService = useService(GlobalDialogService); const globalDialogService = useService(GlobalDialogService);
const { openConfirmModal, closeConfirmModal } = useConfirmModal(); const { openConfirmModal, closeConfirmModal } = useConfirmModal();
const workspacesService = useService(WorkspacesService); const workspacesService = useService(WorkspacesService);
const serversService = useService(ServersService);
const serverList = useLiveData(serversService.servers$);
const { jumpToPage } = useNavigateHelper(); const { jumpToPage } = useNavigateHelper();
const enableCloud = useCallback( const enableCloud = useCallback(
@@ -42,7 +46,8 @@ export const useEnableCloud = () => {
if (!account) return; if (!account) return;
const { id: newId } = await workspacesService.transformLocalToCloud( const { id: newId } = await workspacesService.transformLocalToCloud(
ws, ws,
account.id account.id,
'affine-cloud'
); );
jumpToPage(newId, options?.openPageId || 'all'); jumpToPage(newId, options?.openPageId || 'all');
options?.onSuccess?.(); options?.onSuccess?.();
@@ -56,9 +61,13 @@ export const useEnableCloud = () => {
[account, jumpToPage, t, workspacesService] [account, jumpToPage, t, workspacesService]
); );
const openSignIn = useCallback(() => { const openSignIn = useCallback(
globalDialogService.open('sign-in', {}); () =>
}, [globalDialogService]); globalDialogService.open('sign-in', {
step: 'signIn',
}),
[globalDialogService]
);
const signInOrEnableCloud = useCallback( const signInOrEnableCloud = useCallback(
async (...args: ConfirmEnableArgs) => { async (...args: ConfirmEnableArgs) => {
@@ -76,13 +85,22 @@ export const useEnableCloud = () => {
const confirmEnableCloud = useCallback( const confirmEnableCloud = useCallback(
(ws: Workspace, options?: ConfirmEnableCloudOptions) => { (ws: Workspace, options?: ConfirmEnableCloudOptions) => {
const { onSuccess, onFinished } = options ?? {}; const { onSuccess, onFinished, serverId, openPageId } = options ?? {};
const closeOnSuccess = () => { const closeOnSuccess = () => {
closeConfirmModal(); closeConfirmModal();
onSuccess?.(); onSuccess?.();
}; };
if (serverList.length > 1) {
globalDialogService.open('enable-cloud', {
workspaceId: ws.id,
serverId,
openPageId,
});
return;
}
openConfirmModal( openConfirmModal(
{ {
title: t['Enable AFFiNE Cloud'](), title: t['Enable AFFiNE Cloud'](),
@@ -110,7 +128,15 @@ export const useEnableCloud = () => {
} }
); );
}, },
[closeConfirmModal, loginStatus, openConfirmModal, signInOrEnableCloud, t] [
closeConfirmModal,
globalDialogService,
loginStatus,
openConfirmModal,
serverList.length,
signInOrEnableCloud,
t,
]
); );
return confirmEnableCloud; return confirmEnableCloud;

View File

@@ -0,0 +1,40 @@
import { Menu, MenuItem, type MenuProps, MenuTrigger } from '@affine/component';
import type { Server } from '@affine/core/modules/cloud';
import { useMemo } from 'react';
import { triggerStyle } from './style.css';
export const ServerSelector = ({
servers,
selectedSeverName,
onSelect,
contentOptions,
}: {
servers: Server[];
selectedSeverName: string;
onSelect: (server: Server) => void;
contentOptions?: MenuProps['contentOptions'];
}) => {
const menuItems = useMemo(() => {
return servers.map(server => (
<MenuItem key={server.id} onSelect={() => onSelect(server)}>
{server.config$.value.serverName} ({server.baseUrl})
</MenuItem>
));
}, [servers, onSelect]);
return (
<Menu
items={menuItems}
contentOptions={{
...contentOptions,
style: {
...contentOptions?.style,
width: 'var(--radix-dropdown-menu-trigger-width)',
},
}}
>
<MenuTrigger className={triggerStyle}>{selectedSeverName}</MenuTrigger>
</Menu>
);
};

View File

@@ -0,0 +1,5 @@
import { style } from '@vanilla-extract/css';
export const triggerStyle = style({
width: '100%',
});

View File

@@ -24,12 +24,18 @@ export interface SignInState {
export const SignInPanel = ({ export const SignInPanel = ({
onClose, onClose,
server: initialServerBaseUrl, server: initialServerBaseUrl,
initStep,
}: { }: {
onClose: () => void; onClose: () => void;
server?: string; server?: string;
initStep?: SignInStep | undefined;
}) => { }) => {
const [state, setState] = useState<SignInState>({ const [state, setState] = useState<SignInState>({
step: initialServerBaseUrl ? 'addSelfhosted' : 'signIn', step: initStep
? initStep
: initialServerBaseUrl
? 'addSelfhosted'
: 'signIn',
initialServerBaseUrl: initialServerBaseUrl, initialServerBaseUrl: initialServerBaseUrl,
}); });

View File

@@ -0,0 +1,38 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
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%',
});

View File

@@ -0,0 +1,38 @@
import type { Server } from '@affine/core/modules/cloud';
import { CloudWorkspaceIcon } from '@blocksuite/icons/rc';
import { ServerSelector } from '../../server-selector';
import * as styles from './enable-cloud.css';
export const CustomServerEnableCloud = ({
serverList,
selectedServer,
setSelectedServer,
title,
description,
}: {
serverList: Server[];
selectedServer: Server;
title?: string;
description?: string;
setSelectedServer: (server: Server) => void;
}) => {
return (
<div className={styles.root}>
<CloudWorkspaceIcon width={'36px'} height={'36px'} />
<div className={styles.textContainer}>
{title ? <div className={styles.title}>{title}</div> : null}
{description ? (
<div className={styles.description}>{description}</div>
) : null}
</div>
<div className={styles.serverSelector}>
<ServerSelector
servers={serverList}
selectedSeverName={`${selectedServer.config$.value.serverName} (${selectedServer.baseUrl})`}
onSelect={setSelectedServer}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from './enable-cloud';

View File

@@ -0,0 +1,23 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const ItemContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '8px 14px',
gap: '14px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'background-color 0.2s',
fontSize: '24px',
color: cssVar('iconSecondary'),
});
export const ItemText = style({
fontSize: cssVar('fontSm'),
lineHeight: '22px',
color: cssVar('textSecondaryColor'),
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});

View File

@@ -0,0 +1,37 @@
import { MenuItem } from '@affine/component/ui/menu';
import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useService,
} from '@toeverything/infra';
import * as styles from './index.css';
export const AddServer = ({ onAddServer }: { onAddServer?: () => void }) => {
const t = useI18n();
const featureFlagService = useService(FeatureFlagService);
const enableMultipleServer = useLiveData(
featureFlagService.flags.enable_multiple_cloud_servers.$
);
if (!enableMultipleServer) {
return null;
}
return (
<div>
<MenuItem
block={true}
prefixIcon={<PlusIcon />}
onClick={onAddServer}
data-testid="new-server"
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspaceList.addServer']()}
</div>
</MenuItem>
</div>
);
};

View File

@@ -14,10 +14,9 @@ import {
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook'; import { AddServer } from './add-server';
import { AddWorkspace } from './add-workspace'; import { AddWorkspace } from './add-workspace';
import * as styles from './index.css'; import * as styles from './index.css';
import { UserAccountItem } from './user-account';
import { AFFiNEWorkspaceList } from './workspace-list'; import { AFFiNEWorkspaceList } from './workspace-list';
export const SignInItem = () => { export const SignInItem = () => {
@@ -90,7 +89,7 @@ const UserWithWorkspaceListInner = ({
return openSignInModal(); return openSignInModal();
} }
track.$.navigationPanel.workspaceList.createWorkspace(); track.$.navigationPanel.workspaceList.createWorkspace();
globalDialogService.open('create-workspace', undefined, payload => { globalDialogService.open('create-workspace', {}, payload => {
if (payload) { if (payload) {
onCreatedWorkspace?.(payload); onCreatedWorkspace?.(payload);
} }
@@ -117,28 +116,15 @@ const UserWithWorkspaceListInner = ({
onEventEnd?.(); onEventEnd?.();
}, [globalDialogService, onCreatedWorkspace, onEventEnd]); }, [globalDialogService, onCreatedWorkspace, onEventEnd]);
const onAddServer = useCallback(() => {
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
}, [globalDialogService]);
const workspaceManager = useService(WorkspacesService); const workspaceManager = useService(WorkspacesService);
const workspaces = useLiveData(workspaceManager.list.workspaces$); const workspaces = useLiveData(workspaceManager.list.workspaces$);
const onOpenPricingPlan = useCatchEventCallback(() => {
globalDialogService.open('setting', {
activeTab: 'plans',
scrollAnchor: 'cloudPricingPlan',
});
}, [globalDialogService]);
return ( return (
<div className={styles.workspaceListWrapper}> <div className={styles.workspaceListWrapper}>
{isAuthenticated ? (
<UserAccountItem
email={session.session.account.email ?? 'Unknown User'}
onEventEnd={onEventEnd}
onClick={onOpenPricingPlan}
/>
) : (
<SignInItem />
)}
<Divider size="thinner" />
<AFFiNEWorkspaceList <AFFiNEWorkspaceList
onEventEnd={onEventEnd} onEventEnd={onEventEnd}
onClickWorkspace={onClickWorkspace} onClickWorkspace={onClickWorkspace}
@@ -150,6 +136,7 @@ const UserWithWorkspaceListInner = ({
onAddWorkspace={onAddWorkspace} onAddWorkspace={onAddWorkspace}
onNewWorkspace={onNewWorkspace} onNewWorkspace={onNewWorkspace}
/> />
<AddServer onAddServer={onAddServer} />
</div> </div>
); );
}; };

View File

@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const workspaceListsWrapper = style({ export const workspaceListsWrapper = style({
display: 'flex', display: 'flex',
@@ -15,11 +16,19 @@ export const workspaceListWrapper = style({
export const workspaceServer = style({ export const workspaceServer = style({
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center',
gap: 4, gap: 4,
padding: '0px 12px', paddingLeft: '12px',
marginBottom: '4px',
}); });
export const workspaceServerContent = style({
display: 'flex',
flexDirection: 'column',
color: cssVarV2('text/secondary'),
gap: 4,
width: '100%',
overflow: 'hidden',
});
export const workspaceServerName = style({ export const workspaceServerName = style({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -27,10 +36,19 @@ export const workspaceServerName = style({
fontWeight: 500, fontWeight: 500,
fontSize: cssVar('fontXs'), fontSize: cssVar('fontXs'),
lineHeight: '20px', lineHeight: '20px',
color: cssVar('textSecondaryColor'), textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const account = style({
fontSize: cssVar('fontXs'),
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}); });
export const workspaceTypeIcon = style({ export const workspaceTypeIcon = style({
color: cssVar('iconSecondary'), color: cssVarV2('icon/primary'),
fontSize: '16px',
}); });
export const scrollbar = style({ export const scrollbar = style({
width: '4px', width: '4px',
@@ -39,3 +57,25 @@ export const workspaceCard = style({
height: '44px', height: '44px',
padding: '0 12px', padding: '0 12px',
}); });
export const ItemContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '8px 14px',
gap: '14px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'background-color 0.2s',
fontSize: '24px',
color: cssVarV2('icon/secondary'),
});
export const ItemText = style({
fontSize: cssVar('fontSm'),
lineHeight: '22px',
color: cssVarV2('text/secondary'),
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});

View File

@@ -11,11 +11,14 @@ import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-he
import type { Server } from '@affine/core/modules/cloud'; import type { Server } from '@affine/core/modules/cloud';
import { AuthService, ServersService } from '@affine/core/modules/cloud'; import { AuthService, ServersService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs'; import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { ServerDeploymentType } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import {
CloudWorkspaceIcon, CloudWorkspaceIcon,
LocalWorkspaceIcon, LocalWorkspaceIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
PlusIcon,
TeamWorkspaceIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { WorkspaceMetadata } from '@toeverything/infra'; import type { WorkspaceMetadata } from '@toeverything/infra';
import { import {
@@ -54,6 +57,7 @@ const CloudWorkSpaceList = ({
onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void; onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void;
onClickEnableCloud?: (meta: WorkspaceMetadata) => void; onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
}) => { }) => {
const t = useI18n();
const globalContextService = useService(GlobalContextService); const globalContextService = useService(GlobalContextService);
const globalDialogService = useService(GlobalDialogService); const globalDialogService = useService(GlobalDialogService);
const serverName = useLiveData(server.config$.selector(c => c.serverName)); const serverName = useLiveData(server.config$.selector(c => c.serverName));
@@ -67,6 +71,8 @@ const CloudWorkSpaceList = ({
globalContextService.globalContext.workspaceFlavour.$ globalContextService.globalContext.workspaceFlavour.$
); );
const serverType = server.config$.value.type;
const handleDeleteServer = useCallback(() => { const handleDeleteServer = useCallback(() => {
serversService.removeServer(server.id); serversService.removeServer(server.id);
@@ -94,18 +100,38 @@ const CloudWorkSpaceList = ({
}); });
}, [globalDialogService, server.baseUrl]); }, [globalDialogService, server.baseUrl]);
const onNewWorkspace = useCallback(() => {
globalDialogService.open(
'create-workspace',
{
serverId: server.id,
forcedCloud: true,
},
payload => {
if (payload) {
navigateHelper.openPage(payload.metadata.id, 'all');
}
}
);
}, [globalDialogService, navigateHelper, server.id]);
return ( return (
<div className={styles.workspaceListWrapper}> <div className={styles.workspaceListWrapper}>
<div className={styles.workspaceServer}> <div className={styles.workspaceServer}>
<div className={styles.workspaceServerName}> <div className={styles.workspaceServerContent}>
<CloudWorkspaceIcon <div className={styles.workspaceServerName}>
width={14} {serverType === ServerDeploymentType.Affine ? (
height={14} <CloudWorkspaceIcon className={styles.workspaceTypeIcon} />
className={styles.workspaceTypeIcon} ) : (
/> <TeamWorkspaceIcon className={styles.workspaceTypeIcon} />
{serverName}&nbsp;-&nbsp; )}
{account ? account.email : 'Not signed in'} <div className={styles.account}>{serverName}</div>
</div>
<div className={styles.account}>
{account ? account.email : 'Not signed in'}
</div>
</div> </div>
<Menu <Menu
items={[ items={[
server.id !== 'affine-cloud' && ( server.id !== 'affine-cloud' && (
@@ -125,7 +151,9 @@ const CloudWorkSpaceList = ({
), ),
]} ]}
> >
<IconButton icon={<MoreHorizontalIcon />} /> <div>
<IconButton icon={<MoreHorizontalIcon />} />
</div>
</Menu> </Menu>
</div> </div>
<WorkspaceList <WorkspaceList
@@ -134,6 +162,16 @@ const CloudWorkSpaceList = ({
onSettingClick={onClickWorkspaceSetting} onSettingClick={onClickWorkspaceSetting}
onEnableCloudClick={onClickEnableCloud} onEnableCloudClick={onClickEnableCloud}
/> />
<MenuItem
block={true}
prefixIcon={<PlusIcon />}
onClick={onNewWorkspace}
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspaceList.addWorkspace.create']()}
</div>
</MenuItem>
</div> </div>
); );
}; };

View File

@@ -23,6 +23,8 @@ import * as styles from './dialog.css';
interface NameWorkspaceContentProps extends ConfirmModalProps { interface NameWorkspaceContentProps extends ConfirmModalProps {
loading: boolean; loading: boolean;
forcedCloud?: boolean;
serverId?: string;
onConfirmName: ( onConfirmName: (
name: string, name: string,
workspaceFlavour: string, workspaceFlavour: string,
@@ -33,15 +35,14 @@ interface NameWorkspaceContentProps extends ConfirmModalProps {
const NameWorkspaceContent = ({ const NameWorkspaceContent = ({
loading, loading,
onConfirmName, onConfirmName,
forcedCloud,
serverId,
...props ...props
}: NameWorkspaceContentProps) => { }: NameWorkspaceContentProps) => {
const t = useI18n(); const t = useI18n();
const [workspaceName, setWorkspaceName] = useState(''); const [workspaceName, setWorkspaceName] = useState('');
const featureFlagService = useService(FeatureFlagService);
const enableLocalWorkspace = useLiveData( const [enable, setEnable] = useState(!!forcedCloud);
featureFlagService.flags.enable_local_workspace.$
);
const [enable, setEnable] = useState(!enableLocalWorkspace);
const session = useService(AuthService).session; const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$); const loginStatus = useLiveData(session.status$);
@@ -62,8 +63,8 @@ const NameWorkspaceContent = ({
); );
const handleCreateWorkspace = useCallback(() => { const handleCreateWorkspace = useCallback(() => {
onConfirmName(workspaceName, enable ? 'affine-cloud' : 'local'); onConfirmName(workspaceName, enable ? serverId || 'affine-cloud' : 'local');
}, [enable, onConfirmName, workspaceName]); }, [enable, onConfirmName, serverId, workspaceName]);
const onEnter = useCallback(() => { const onEnter = useCallback(() => {
if (workspaceName) { if (workspaceName) {
@@ -120,7 +121,7 @@ const NameWorkspaceContent = ({
<Switch <Switch
checked={enable} checked={enable}
onChange={onSwitchChange} onChange={onSwitchChange}
disabled={!enableLocalWorkspace} disabled={forcedCloud}
/> />
</div> </div>
<div className={styles.cardDescription}> <div className={styles.cardDescription}>
@@ -131,7 +132,7 @@ const NameWorkspaceContent = ({
<CloudSvg /> <CloudSvg />
</div> </div>
</div> </div>
{!enableLocalWorkspace ? ( {forcedCloud ? (
<a <a
className={styles.cloudTips} className={styles.cloudTips}
href={BUILD_CONFIG.downloadUrl} href={BUILD_CONFIG.downloadUrl}
@@ -147,9 +148,15 @@ const NameWorkspaceContent = ({
}; };
export const CreateWorkspaceDialog = ({ export const CreateWorkspaceDialog = ({
forcedCloud,
serverId,
close, close,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => { }: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
const workspacesService = useService(WorkspacesService); const workspacesService = useService(WorkspacesService);
const featureFlagService = useService(FeatureFlagService);
const enableLocalWorkspace = useLiveData(
featureFlagService.flags.enable_local_workspace.$
);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const onConfirmName = useAsyncCallback( const onConfirmName = useAsyncCallback(
@@ -185,6 +192,8 @@ export const CreateWorkspaceDialog = ({
<NameWorkspaceContent <NameWorkspaceContent
loading={loading} loading={loading}
open open
serverId={serverId}
forcedCloud={forcedCloud || !enableLocalWorkspace}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
onConfirmName={onConfirmName} 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 { CollectionEditorDialog } from './collection-editor';
import { CreateWorkspaceDialog } from './create-workspace'; import { CreateWorkspaceDialog } from './create-workspace';
import { DocInfoDialog } from './doc-info'; import { DocInfoDialog } from './doc-info';
import { EnableCloudDialog } from './enable-cloud';
import { ImportDialog } from './import'; import { ImportDialog } from './import';
import { ImportTemplateDialog } from './import-template'; import { ImportTemplateDialog } from './import-template';
import { ImportWorkspaceDialog } from './import-workspace'; import { ImportWorkspaceDialog } from './import-workspace';
@@ -30,6 +31,7 @@ const GLOBAL_DIALOGS = {
'sign-in': SignInDialog, 'sign-in': SignInDialog,
'change-password': ChangePasswordDialog, 'change-password': ChangePasswordDialog,
'verify-email': VerifyEmailDialog, 'verify-email': VerifyEmailDialog,
'enable-cloud': EnableCloudDialog,
} satisfies { } satisfies {
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC< [key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]> DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>

View File

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

View File

@@ -14,7 +14,7 @@ export type SettingTab =
| `workspace:${'preference' | 'properties'}`; | `workspace:${'preference' | 'properties'}`;
export type GLOBAL_DIALOG_SCHEMA = { export type GLOBAL_DIALOG_SCHEMA = {
'create-workspace': () => { 'create-workspace': (props: { serverId?: string; forcedCloud?: boolean }) => {
metadata: WorkspaceMetadata; metadata: WorkspaceMetadata;
defaultDocId?: string; defaultDocId?: string;
}; };
@@ -31,9 +31,14 @@ export type GLOBAL_DIALOG_SCHEMA = {
workspaceMetadata?: WorkspaceMetadata | null; workspaceMetadata?: WorkspaceMetadata | null;
scrollAnchor?: string; scrollAnchor?: string;
}) => void; }) => void;
'sign-in': (props: { server?: string; step?: 'sign-in' }) => void; 'sign-in': (props: { server?: string; step?: string }) => void;
'change-password': (props: { server?: string }) => void; 'change-password': (props: { server?: string }) => void;
'verify-email': (props: { server?: string; changeEmail?: boolean }) => void; 'verify-email': (props: { server?: string; changeEmail?: boolean }) => void;
'enable-cloud': (props: {
workspaceId: string;
openPageId?: string;
serverId?: string;
}) => boolean;
}; };
export type WORKSPACE_DIALOG_SCHEMA = { export type WORKSPACE_DIALOG_SCHEMA = {

View File

@@ -20,5 +20,5 @@
"sv-SE": 4, "sv-SE": 4,
"ur": 3, "ur": 3,
"zh-Hans": 99, "zh-Hans": 99,
"zh-Hant": 99 "zh-Hant": 98
} }

View File

@@ -473,6 +473,9 @@
"com.affine.empty.tags.title": "Tag management", "com.affine.empty.tags.title": "Tag management",
"com.affine.emptyDesc": "There's no doc here yet", "com.affine.emptyDesc": "There's no doc here yet",
"com.affine.enableAffineCloudModal.button.cancel": "Cancel", "com.affine.enableAffineCloudModal.button.cancel": "Cancel",
"com.affine.enableAffineCloudModal.custom-server.title": "Enable Cloud for {{workspaceName}}",
"com.affine.enableAffineCloudModal.custom-server.description": "Choose an instance.",
"com.affine.enableAffineCloudModal.custom-server.enable": "Enable Cloud",
"com.affine.error.hide-error": "Hide error", "com.affine.error.hide-error": "Hide error",
"com.affine.error.no-page-root.title": "Doc content is missing", "com.affine.error.no-page-root.title": "Doc content is missing",
"com.affine.error.refetch": "Refetch", "com.affine.error.refetch": "Refetch",
@@ -1432,6 +1435,7 @@
"com.affine.workspaceList.addWorkspace.create-cloud": "Create cloud workspace", "com.affine.workspaceList.addWorkspace.create-cloud": "Create cloud workspace",
"com.affine.workspaceList.workspaceListType.cloud": "Cloud sync", "com.affine.workspaceList.workspaceListType.cloud": "Cloud sync",
"com.affine.workspaceList.workspaceListType.local": "Local storage", "com.affine.workspaceList.workspaceListType.local": "Local storage",
"com.affine.workspaceList.addServer": "Add Server",
"com.affine.workspaceSubPath.all": "All docs", "com.affine.workspaceSubPath.all": "All docs",
"com.affine.workspaceSubPath.trash": "Trash", "com.affine.workspaceSubPath.trash": "Trash",
"com.affine.workspaceSubPath.trash.empty-description": "Deleted docs will appear here.", "com.affine.workspaceSubPath.trash.empty-description": "Deleted docs will appear here.",

View File

@@ -9,7 +9,6 @@ import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { clickUserInfoCard } from '@affine-test/kit/utils/setting'; import { clickUserInfoCard } from '@affine-test/kit/utils/setting';
import { import {
clickSideBarAllPageButton, clickSideBarAllPageButton,
clickSideBarCurrentWorkspaceBanner,
clickSideBarSettingButton, clickSideBarSettingButton,
clickSideBarUseAvatar, clickSideBarUseAvatar,
} from '@affine-test/kit/utils/sidebar'; } from '@affine-test/kit/utils/sidebar';
@@ -19,8 +18,7 @@ import { expect } from '@playwright/test';
test('can open login modal in workspace list', async ({ page }) => { test('can open login modal in workspace list', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickSideBarCurrentWorkspaceBanner(page); await page.getByTestId('sidebar-user-avatar').click({
await page.getByTestId('cloud-signin-button').click({
delay: 200, delay: 200,
}); });
await expect(page.getByTestId('auth-modal')).toBeVisible(); await expect(page.getByTestId('auth-modal')).toBeVisible();

View File

@@ -4,10 +4,7 @@ import {
waitForAllPagesLoad, waitForAllPagesLoad,
waitForEditorLoad, waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
clickSideBarCurrentWorkspaceBanner,
clickSideBarSettingButton,
} from '@affine-test/kit/utils/sidebar';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { hash } from '@node-rs/argon2'; import { hash } from '@node-rs/argon2';
import type { BrowserContext, Cookie, Page } from '@playwright/test'; import type { BrowserContext, Cookie, Page } from '@playwright/test';
@@ -239,8 +236,7 @@ export async function loginUser(
await waitForEditorLoad(page); await waitForEditorLoad(page);
} }
await clickSideBarCurrentWorkspaceBanner(page); await page.getByTestId('sidebar-user-avatar').click({
await page.getByTestId('cloud-signin-button').click({
delay: 200, delay: 200,
}); });
await loginUserDirectly(page, user, config); await loginUserDirectly(page, user, config);