From 593be47606d4d24116d6c95f4973197148e9c74b Mon Sep 17 00:00:00 2001 From: Jimmfly Date: Wed, 4 Dec 2024 08:49:13 +0800 Subject: [PATCH] feat(core): support creating cloud workspaces to different servers --- .../modules/workspace/services/transform.ts | 5 +- .../core/src/commands/affine-creation.tsx | 2 +- .../hooks/affine/use-enable-cloud.tsx | 34 +++- .../src/components/server-selector/index.tsx | 40 +++++ .../components/server-selector/style.css.ts | 5 + .../core/src/components/sign-in/index.tsx | 8 +- .../enable-cloud/enable-cloud.css.ts | 38 ++++ .../enable-cloud/enable-cloud.tsx | 38 ++++ .../workspace-selector/enable-cloud/index.ts | 1 + .../add-server/index.css.ts | 23 +++ .../add-server/index.tsx | 37 ++++ .../user-with-workspace-list/index.tsx | 27 +-- .../workspace-list/index.css.ts | 48 +++++- .../workspace-list/index.tsx | 56 +++++- .../dialogs/create-workspace/index.tsx | 27 ++- .../dialogs/enable-cloud/dialog.css.ts | 86 ++++++++++ .../desktop/dialogs/enable-cloud/index.tsx | 162 ++++++++++++++++++ .../core/src/desktop/dialogs/index.tsx | 2 + .../src/desktop/dialogs/sign-in/index.tsx | 9 +- .../core/src/modules/dialogs/constant.ts | 22 ++- .../i18n/src/i18n-completenesses.json | 2 +- packages/frontend/i18n/src/resources/en.json | 4 + tests/affine-cloud/e2e/login.spec.ts | 4 +- tests/kit/utils/cloud.ts | 8 +- 24 files changed, 621 insertions(+), 67 deletions(-) create mode 100644 packages/frontend/core/src/components/server-selector/index.tsx create mode 100644 packages/frontend/core/src/components/server-selector/style.css.ts create mode 100644 packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.css.ts create mode 100644 packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.tsx create mode 100644 packages/frontend/core/src/components/workspace-selector/enable-cloud/index.ts create mode 100644 packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts create mode 100644 packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx create mode 100644 packages/frontend/core/src/desktop/dialogs/enable-cloud/dialog.css.ts create mode 100644 packages/frontend/core/src/desktop/dialogs/enable-cloud/index.tsx diff --git a/packages/common/infra/src/modules/workspace/services/transform.ts b/packages/common/infra/src/modules/workspace/services/transform.ts index b40671f51a063..90cef321e48ee 100644 --- a/packages/common/infra/src/modules/workspace/services/transform.ts +++ b/packages/common/infra/src/modules/workspace/services/transform.ts @@ -23,14 +23,15 @@ export class WorkspaceTransformService extends Service { */ transformLocalToCloud = async ( local: Workspace, - accountId: string + accountId: string, + flavour: string ): Promise => { assertEquals(local.flavour, 'local'); const localDocStorage = local.engine.doc.storage.behavior; const newMetadata = await this.factory.create( - 'affine-cloud', + flavour ?? 'affine-cloud', async (docCollection, blobStorage, docStorage) => { const rootDocBinary = await localDocStorage.doc.get( local.docCollection.doc.guid diff --git a/packages/frontend/core/src/commands/affine-creation.tsx b/packages/frontend/core/src/commands/affine-creation.tsx index 5cf4d66000a07..be82ab557b211 100644 --- a/packages/frontend/core/src/commands/affine-creation.tsx +++ b/packages/frontend/core/src/commands/affine-creation.tsx @@ -62,7 +62,7 @@ export function registerAffineCreationCommands({ run() { track.$.cmdk.workspace.createWorkspace(); - globalDialogService.open('create-workspace', undefined); + globalDialogService.open('create-workspace', {}); }, }) ); diff --git a/packages/frontend/core/src/components/hooks/affine/use-enable-cloud.tsx b/packages/frontend/core/src/components/hooks/affine/use-enable-cloud.tsx index 327ee13b213d6..c1a407054c6dc 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-enable-cloud.tsx +++ b/packages/frontend/core/src/components/hooks/affine/use-enable-cloud.tsx @@ -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) => { @@ -83,6 +92,11 @@ export const useEnableCloud = () => { onSuccess?.(); }; + if (serverList.length > 1) { + globalDialogService.open('enable-cloud', { workspace: ws, options }); + return; + } + openConfirmModal( { title: t['Enable AFFiNE Cloud'](), @@ -110,7 +124,15 @@ export const useEnableCloud = () => { } ); }, - [closeConfirmModal, loginStatus, openConfirmModal, signInOrEnableCloud, t] + [ + closeConfirmModal, + globalDialogService, + loginStatus, + openConfirmModal, + serverList.length, + signInOrEnableCloud, + t, + ] ); return confirmEnableCloud; diff --git a/packages/frontend/core/src/components/server-selector/index.tsx b/packages/frontend/core/src/components/server-selector/index.tsx new file mode 100644 index 0000000000000..fe10ff52d2fe3 --- /dev/null +++ b/packages/frontend/core/src/components/server-selector/index.tsx @@ -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 => ( + onSelect(server)}> + {server.config$.value.serverName} ({server.baseUrl}) + + )); + }, [servers, onSelect]); + + return ( + + {selectedSeverName} + + ); +}; diff --git a/packages/frontend/core/src/components/server-selector/style.css.ts b/packages/frontend/core/src/components/server-selector/style.css.ts new file mode 100644 index 0000000000000..10fb8a9dbd789 --- /dev/null +++ b/packages/frontend/core/src/components/server-selector/style.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css'; + +export const triggerStyle = style({ + width: '100%', +}); diff --git a/packages/frontend/core/src/components/sign-in/index.tsx b/packages/frontend/core/src/components/sign-in/index.tsx index 8b164d6cd84c1..e5ac4a9a7a66d 100644 --- a/packages/frontend/core/src/components/sign-in/index.tsx +++ b/packages/frontend/core/src/components/sign-in/index.tsx @@ -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({ - step: initialServerBaseUrl ? 'addSelfhosted' : 'signIn', + step: initStep + ? initStep + : initialServerBaseUrl + ? 'addSelfhosted' + : 'signIn', initialServerBaseUrl: initialServerBaseUrl, }); diff --git a/packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.css.ts b/packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.css.ts new file mode 100644 index 0000000000000..af7b329127a47 --- /dev/null +++ b/packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.css.ts @@ -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%', +}); diff --git a/packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.tsx b/packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.tsx new file mode 100644 index 0000000000000..23d33772c74f7 --- /dev/null +++ b/packages/frontend/core/src/components/workspace-selector/enable-cloud/enable-cloud.tsx @@ -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 ( +
+ +
+ {title ?
{title}
: null} + {description ? ( +
{description}
+ ) : null} +
+
+ +
+
+ ); +}; diff --git a/packages/frontend/core/src/components/workspace-selector/enable-cloud/index.ts b/packages/frontend/core/src/components/workspace-selector/enable-cloud/index.ts new file mode 100644 index 0000000000000..864d954a21d47 --- /dev/null +++ b/packages/frontend/core/src/components/workspace-selector/enable-cloud/index.ts @@ -0,0 +1 @@ +export * from './enable-cloud'; diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts new file mode 100644 index 0000000000000..dd1183dd2a111 --- /dev/null +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx new file mode 100644 index 0000000000000..e10bbf907cf32 --- /dev/null +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/add-server/index.tsx @@ -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 ( +
+ } + onClick={onAddServer} + data-testid="new-server" + className={styles.ItemContainer} + > +
+ {t['com.affine.workspaceList.addServer']()} +
+
+
+ ); +}; diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx index e9ead87b7e207..b5b7e1780f6d9 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/index.tsx @@ -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 (
- {isAuthenticated ? ( - - ) : ( - - )} - +
); }; diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts index 455943f6bfdf5..20a3bf56e432d 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx index 0f38278ec68b3..1257c7e7fc122 100644 --- a/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx +++ b/packages/frontend/core/src/components/workspace-selector/user-with-workspace-list/workspace-list/index.tsx @@ -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 (
-
- - {serverName} -  - {account ? account.email : 'Not signed in'} +
+
+ {serverType === ServerDeploymentType.Affine ? ( + + ) : ( + + )} +
{serverName}
+
+
+ {account ? account.email : 'Not signed in'} +
+ - } /> +
+ } /> +
+ } + onClick={onNewWorkspace} + className={styles.ItemContainer} + > +
+ {t['com.affine.workspaceList.addWorkspace.create']()} +
+
); }; diff --git a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx index 1ae9c5d025237..b1f5b451f9e3a 100644 --- a/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/create-workspace/index.tsx @@ -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 = ({
@@ -131,7 +132,7 @@ const NameWorkspaceContent = ({
- {!enableLocalWorkspace ? ( + {forcedCloud ? ( ) => { 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 = ({ diff --git a/packages/frontend/core/src/desktop/dialogs/enable-cloud/dialog.css.ts b/packages/frontend/core/src/desktop/dialogs/enable-cloud/dialog.css.ts new file mode 100644 index 0000000000000..bcd11697709d3 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/enable-cloud/dialog.css.ts @@ -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', +}); diff --git a/packages/frontend/core/src/desktop/dialogs/enable-cloud/index.tsx b/packages/frontend/core/src/desktop/dialogs/enable-cloud/index.tsx new file mode 100644 index 0000000000000..c3da220a56292 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/enable-cloud/index.tsx @@ -0,0 +1,162 @@ +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 type { ConfirmEnableCloudOptions } from '@affine/core/modules/dialogs/constant'; +import { useI18n } from '@affine/i18n'; +import { CloudWorkspaceIcon } from '@blocksuite/icons/rc'; +import type { Workspace } from '@toeverything/infra'; +import { + FrameworkScope, + useLiveData, + useService, + WorkspacesService, +} from '@toeverything/infra'; +import { useCallback, useState } from 'react'; + +import * as styles from './dialog.css'; + +const Dialog = ({ + workspace, + options, + close, + selectedServer, + setSelectedServer, + serverList, +}: { + serverList: Server[]; + selectedServer: Server; + setSelectedServer: (server: Server) => void; + workspace?: Workspace; + options?: ConfirmEnableCloudOptions; + 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 { 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, options?.openPageId || 'all'); + options?.onSuccess?.(); + close?.(); + } catch (e) { + console.error(e); + notify.error({ + title: t['com.affine.workspace.enable-cloud.failed'](), + }); + } + }, [ + account, + close, + jumpToPage, + options, + selectedServer.id, + t, + workspace, + workspacesService, + ]); + + 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 ( +
+ +
+
+ {t['com.affine.enableAffineCloudModal.custom-server.title']({ + workspaceName: workspace?.name$.value || 'untitled', + })} +
+
+ {t['com.affine.enableAffineCloudModal.custom-server.description']()} +
+
+
+ +
+ + +
+ ); +}; + +export const EnableCloudDialog = ({ + workspace, + options, + close, +}: DialogComponentProps) => { + const serversService = useService(ServersService); + const serverList = useLiveData(serversService.servers$); + const [selectedServer, setSelectedServer] = useState(serverList[0]); + + return ( + close()} + > + + + + + ); +}; diff --git a/packages/frontend/core/src/desktop/dialogs/index.tsx b/packages/frontend/core/src/desktop/dialogs/index.tsx index 641a4ae448380..0ec8b3565e1b4 100644 --- a/packages/frontend/core/src/desktop/dialogs/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/index.tsx @@ -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 diff --git a/packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx b/packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx index 93ff5c7b1a514..14cedb123d888 100644 --- a/packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/sign-in/index.tsx @@ -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) => { return ( - + ); }; diff --git a/packages/frontend/core/src/modules/dialogs/constant.ts b/packages/frontend/core/src/modules/dialogs/constant.ts index 86e8ea8e7a12b..358f5f6310476 100644 --- a/packages/frontend/core/src/modules/dialogs/constant.ts +++ b/packages/frontend/core/src/modules/dialogs/constant.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-types */ import type { DocMode } from '@blocksuite/affine/blocks'; -import type { WorkspaceMetadata } from '@toeverything/infra'; +import type { Workspace, WorkspaceMetadata } from '@toeverything/infra'; export type SettingTab = | 'shortcuts' @@ -13,8 +13,20 @@ export type SettingTab = | 'account' | `workspace:${'preference' | 'properties'}`; +export interface ConfirmEnableCloudOptions { + /** + * Fired when the workspace is successfully enabled + */ + onSuccess?: () => void; + /** + * Fired when workspace is successfully enabled or user cancels the operation + */ + onFinished?: () => void; + openPageId?: string; + serverId?: string; +} export type GLOBAL_DIALOG_SCHEMA = { - 'create-workspace': () => { + 'create-workspace': (props: { serverId?: string; forcedCloud?: boolean }) => { metadata: WorkspaceMetadata; defaultDocId?: string; }; @@ -31,9 +43,13 @@ export type GLOBAL_DIALOG_SCHEMA = { workspaceMetadata?: WorkspaceMetadata | null; scrollAnchor?: string; }) => void; - 'sign-in': (props: { server?: string; step?: 'sign-in' }) => void; + 'sign-in': (props: { server?: string; step?: string }) => void; 'change-password': (props: { server?: string }) => void; 'verify-email': (props: { server?: string; changeEmail?: boolean }) => void; + 'enable-cloud': (props: { + workspace?: Workspace; + options?: ConfirmEnableCloudOptions; + }) => void; }; export type WORKSPACE_DIALOG_SCHEMA = { diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 350b446a0ed6c..a840973c70e2d 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -20,5 +20,5 @@ "sv-SE": 4, "ur": 3, "zh-Hans": 99, - "zh-Hant": 99 + "zh-Hant": 98 } \ No newline at end of file diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 3e09070b153b4..5907838f9fbca 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -473,6 +473,9 @@ "com.affine.empty.tags.title": "Tag management", "com.affine.emptyDesc": "There's no doc here yet", "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.no-page-root.title": "Doc content is missing", "com.affine.error.refetch": "Refetch", @@ -1432,6 +1435,7 @@ "com.affine.workspaceList.addWorkspace.create-cloud": "Create cloud workspace", "com.affine.workspaceList.workspaceListType.cloud": "Cloud sync", "com.affine.workspaceList.workspaceListType.local": "Local storage", + "com.affine.workspaceList.addServer": "Add Server", "com.affine.workspaceSubPath.all": "All docs", "com.affine.workspaceSubPath.trash": "Trash", "com.affine.workspaceSubPath.trash.empty-description": "Deleted docs will appear here.", diff --git a/tests/affine-cloud/e2e/login.spec.ts b/tests/affine-cloud/e2e/login.spec.ts index 782d3b35fcdc7..e9154e15bf8a9 100644 --- a/tests/affine-cloud/e2e/login.spec.ts +++ b/tests/affine-cloud/e2e/login.spec.ts @@ -9,7 +9,6 @@ import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic'; import { clickUserInfoCard } from '@affine-test/kit/utils/setting'; import { clickSideBarAllPageButton, - clickSideBarCurrentWorkspaceBanner, clickSideBarSettingButton, clickSideBarUseAvatar, } 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 }) => { await openHomePage(page); await waitForEditorLoad(page); - await clickSideBarCurrentWorkspaceBanner(page); - await page.getByTestId('cloud-signin-button').click({ + await page.getByTestId('sidebar-user-avatar').click({ delay: 200, }); await expect(page.getByTestId('auth-modal')).toBeVisible(); diff --git a/tests/kit/utils/cloud.ts b/tests/kit/utils/cloud.ts index 44d056f35d86d..277e7abf17ee4 100644 --- a/tests/kit/utils/cloud.ts +++ b/tests/kit/utils/cloud.ts @@ -4,10 +4,7 @@ import { waitForAllPagesLoad, waitForEditorLoad, } from '@affine-test/kit/utils/page-logic'; -import { - clickSideBarCurrentWorkspaceBanner, - clickSideBarSettingButton, -} from '@affine-test/kit/utils/sidebar'; +import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar'; import { faker } from '@faker-js/faker'; import { hash } from '@node-rs/argon2'; import type { BrowserContext, Cookie, Page } from '@playwright/test'; @@ -239,8 +236,7 @@ export async function loginUser( await waitForEditorLoad(page); } - await clickSideBarCurrentWorkspaceBanner(page); - await page.getByTestId('cloud-signin-button').click({ + await page.getByTestId('sidebar-user-avatar').click({ delay: 200, }); await loginUserDirectly(page, user, config);