mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 10:36:22 +08:00
feat(core): support creating cloud workspaces to different servers (#9006)
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+38
@@ -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%',
|
||||
});
|
||||
+38
@@ -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';
|
||||
+23
@@ -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',
|
||||
});
|
||||
+37
@@ -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>
|
||||
);
|
||||
};
|
||||
+7
-20
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+44
-4
@@ -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',
|
||||
});
|
||||
|
||||
+47
-9
@@ -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} -
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user