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
@@ -1,5 +1,5 @@
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 { useI18n } from '@affine/i18n';
import type { Workspace } from '@toeverything/infra';
@@ -22,6 +22,7 @@ interface ConfirmEnableCloudOptions {
*/
onFinished?: () => void;
openPageId?: string;
serverId?: string;
}
type ConfirmEnableArgs = [Workspace, ConfirmEnableCloudOptions | undefined];
@@ -33,6 +34,9 @@ export const useEnableCloud = () => {
const globalDialogService = useService(GlobalDialogService);
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
const workspacesService = useService(WorkspacesService);
const serversService = useService(ServersService);
const serverList = useLiveData(serversService.servers$);
const { jumpToPage } = useNavigateHelper();
const enableCloud = useCallback(
@@ -42,7 +46,8 @@ export const useEnableCloud = () => {
if (!account) return;
const { id: newId } = await workspacesService.transformLocalToCloud(
ws,
account.id
account.id,
'affine-cloud'
);
jumpToPage(newId, options?.openPageId || 'all');
options?.onSuccess?.();
@@ -56,9 +61,13 @@ export const useEnableCloud = () => {
[account, jumpToPage, t, workspacesService]
);
const openSignIn = useCallback(() => {
globalDialogService.open('sign-in', {});
}, [globalDialogService]);
const openSignIn = useCallback(
() =>
globalDialogService.open('sign-in', {
step: 'signIn',
}),
[globalDialogService]
);
const signInOrEnableCloud = useCallback(
async (...args: ConfirmEnableArgs) => {
@@ -76,13 +85,22 @@ export const useEnableCloud = () => {
const confirmEnableCloud = useCallback(
(ws: Workspace, options?: ConfirmEnableCloudOptions) => {
const { onSuccess, onFinished } = options ?? {};
const { onSuccess, onFinished, serverId, openPageId } = options ?? {};
const closeOnSuccess = () => {
closeConfirmModal();
onSuccess?.();
};
if (serverList.length > 1) {
globalDialogService.open('enable-cloud', {
workspaceId: ws.id,
serverId,
openPageId,
});
return;
}
openConfirmModal(
{
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;
@@ -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>
);
};
@@ -0,0 +1,5 @@
import { style } from '@vanilla-extract/css';
export const triggerStyle = style({
width: '100%',
});
@@ -24,12 +24,18 @@ export interface SignInState {
export const SignInPanel = ({
onClose,
server: initialServerBaseUrl,
initStep,
}: {
onClose: () => void;
server?: string;
initStep?: SignInStep | undefined;
}) => {
const [state, setState] = useState<SignInState>({
step: initialServerBaseUrl ? 'addSelfhosted' : 'signIn',
step: initStep
? initStep
: initialServerBaseUrl
? 'addSelfhosted'
: 'signIn',
initialServerBaseUrl: initialServerBaseUrl,
});
@@ -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%',
});
@@ -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>
);
};
@@ -0,0 +1 @@
export * from './enable-cloud';
@@ -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',
});
@@ -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>
);
};
@@ -14,10 +14,9 @@ import {
} from '@toeverything/infra';
import { useCallback } from 'react';
import { useCatchEventCallback } from '../../hooks/use-catch-event-hook';
import { AddServer } from './add-server';
import { AddWorkspace } from './add-workspace';
import * as styles from './index.css';
import { UserAccountItem } from './user-account';
import { AFFiNEWorkspaceList } from './workspace-list';
export const SignInItem = () => {
@@ -90,7 +89,7 @@ const UserWithWorkspaceListInner = ({
return openSignInModal();
}
track.$.navigationPanel.workspaceList.createWorkspace();
globalDialogService.open('create-workspace', undefined, payload => {
globalDialogService.open('create-workspace', {}, payload => {
if (payload) {
onCreatedWorkspace?.(payload);
}
@@ -117,28 +116,15 @@ const UserWithWorkspaceListInner = ({
onEventEnd?.();
}, [globalDialogService, onCreatedWorkspace, onEventEnd]);
const onAddServer = useCallback(() => {
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
}, [globalDialogService]);
const workspaceManager = useService(WorkspacesService);
const workspaces = useLiveData(workspaceManager.list.workspaces$);
const onOpenPricingPlan = useCatchEventCallback(() => {
globalDialogService.open('setting', {
activeTab: 'plans',
scrollAnchor: 'cloudPricingPlan',
});
}, [globalDialogService]);
return (
<div className={styles.workspaceListWrapper}>
{isAuthenticated ? (
<UserAccountItem
email={session.session.account.email ?? 'Unknown User'}
onEventEnd={onEventEnd}
onClick={onOpenPricingPlan}
/>
) : (
<SignInItem />
)}
<Divider size="thinner" />
<AFFiNEWorkspaceList
onEventEnd={onEventEnd}
onClickWorkspace={onClickWorkspace}
@@ -150,6 +136,7 @@ const UserWithWorkspaceListInner = ({
onAddWorkspace={onAddWorkspace}
onNewWorkspace={onNewWorkspace}
/>
<AddServer onAddServer={onAddServer} />
</div>
);
};
@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const workspaceListsWrapper = style({
display: 'flex',
@@ -15,11 +16,19 @@ export const workspaceListWrapper = style({
export const workspaceServer = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
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({
display: 'flex',
alignItems: 'center',
@@ -27,10 +36,19 @@ export const workspaceServerName = style({
fontWeight: 500,
fontSize: cssVar('fontXs'),
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({
color: cssVar('iconSecondary'),
color: cssVarV2('icon/primary'),
fontSize: '16px',
});
export const scrollbar = style({
width: '4px',
@@ -39,3 +57,25 @@ export const workspaceCard = style({
height: '44px',
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',
});
@@ -11,11 +11,14 @@ import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-he
import type { Server } from '@affine/core/modules/cloud';
import { AuthService, ServersService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { ServerDeploymentType } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import {
CloudWorkspaceIcon,
LocalWorkspaceIcon,
MoreHorizontalIcon,
PlusIcon,
TeamWorkspaceIcon,
} from '@blocksuite/icons/rc';
import type { WorkspaceMetadata } from '@toeverything/infra';
import {
@@ -54,6 +57,7 @@ const CloudWorkSpaceList = ({
onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void;
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
}) => {
const t = useI18n();
const globalContextService = useService(GlobalContextService);
const globalDialogService = useService(GlobalDialogService);
const serverName = useLiveData(server.config$.selector(c => c.serverName));
@@ -67,6 +71,8 @@ const CloudWorkSpaceList = ({
globalContextService.globalContext.workspaceFlavour.$
);
const serverType = server.config$.value.type;
const handleDeleteServer = useCallback(() => {
serversService.removeServer(server.id);
@@ -94,18 +100,38 @@ const CloudWorkSpaceList = ({
});
}, [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 (
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceServer}>
<div className={styles.workspaceServerName}>
<CloudWorkspaceIcon
width={14}
height={14}
className={styles.workspaceTypeIcon}
/>
{serverName}&nbsp;-&nbsp;
{account ? account.email : 'Not signed in'}
<div className={styles.workspaceServerContent}>
<div className={styles.workspaceServerName}>
{serverType === ServerDeploymentType.Affine ? (
<CloudWorkspaceIcon className={styles.workspaceTypeIcon} />
) : (
<TeamWorkspaceIcon className={styles.workspaceTypeIcon} />
)}
<div className={styles.account}>{serverName}</div>
</div>
<div className={styles.account}>
{account ? account.email : 'Not signed in'}
</div>
</div>
<Menu
items={[
server.id !== 'affine-cloud' && (
@@ -125,7 +151,9 @@ const CloudWorkSpaceList = ({
),
]}
>
<IconButton icon={<MoreHorizontalIcon />} />
<div>
<IconButton icon={<MoreHorizontalIcon />} />
</div>
</Menu>
</div>
<WorkspaceList
@@ -134,6 +162,16 @@ const CloudWorkSpaceList = ({
onSettingClick={onClickWorkspaceSetting}
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>
);
};