From 07b5d184413350b3be26406364434f0eb6d5fd12 Mon Sep 17 00:00:00 2001 From: JimmFly Date: Mon, 16 Oct 2023 16:44:09 +0800 Subject: [PATCH] feat(core): add sign out confirm modal (#4592) --- apps/core/src/atoms/index.ts | 1 + .../setting-modal/account-setting/index.tsx | 18 +++---- .../affine/sign-out-modal/index.tsx | 46 ++++++++++++++++++ .../user-account/index.tsx | 28 +++++------ apps/core/src/pages/404.tsx | 48 ++++++++++++------- apps/core/src/providers/modal-provider.tsx | 29 +++++++++++ .../src/components/share-menu/share-menu.tsx | 2 + packages/i18n/src/resources/en.json | 6 ++- tests/affine-cloud/e2e/collaboration.spec.ts | 25 ++++++++++ tests/affine-desktop/e2e/basic.spec.ts | 7 ++- tests/affine-desktop/e2e/workspace.spec.ts | 3 +- tests/affine-local/e2e/router.spec.ts | 2 +- tests/kit/utils/cloud.ts | 34 ++++++++++++- 13 files changed, 199 insertions(+), 50 deletions(-) create mode 100644 apps/core/src/components/affine/sign-out-modal/index.tsx diff --git a/apps/core/src/atoms/index.ts b/apps/core/src/atoms/index.ts index 69ce42f732..7ebe1d6b0e 100644 --- a/apps/core/src/atoms/index.ts +++ b/apps/core/src/atoms/index.ts @@ -11,6 +11,7 @@ export const openWorkspacesModalAtom = atom(false); export const openCreateWorkspaceModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false); export const openOnboardingModalAtom = atom(false); +export const openSignOutModalAtom = atom(false); export type SettingAtom = Pick & { open: boolean; diff --git a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx index d1bb55ccb8..b58fe74b57 100644 --- a/apps/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/apps/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -23,10 +23,8 @@ import { useState, } from 'react'; -import { authAtom } from '../../../../atoms'; +import { authAtom, openSignOutModalAtom } from '../../../../atoms'; import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; -import { useNavigateHelper } from '../../../../hooks/use-navigate-helper'; -import { signOutCloud } from '../../../../utils/cloud-utils'; import { Upload } from '../../../pure/file-upload'; import * as style from './style.css'; @@ -168,8 +166,8 @@ const StoragePanel = () => { export const AccountSetting: FC = () => { const t = useAFFiNEI18N(); const user = useCurrentUser(); - const { jumpToIndex } = useNavigateHelper(); const setAuthModal = useSetAtom(authAtom); + const setSignOutModal = useSetAtom(openSignOutModalAtom); const onChangeEmail = useCallback(() => { setAuthModal({ @@ -189,6 +187,10 @@ export const AccountSetting: FC = () => { }); }, [setAuthModal, user.email, user.hasPassword]); + const onOpenSignOutModal = useCallback(() => { + setSignOutModal(true); + }, [setSignOutModal]); + return ( <> { desc={t['com.affine.setting.sign.out.message']()} style={{ cursor: 'pointer' }} data-testid="sign-out-button" - onClick={useCallback(() => { - signOutCloud() - .then(() => { - jumpToIndex(); - }) - .catch(console.error); - }, [jumpToIndex])} + onClick={onOpenSignOutModal} > diff --git a/apps/core/src/components/affine/sign-out-modal/index.tsx b/apps/core/src/components/affine/sign-out-modal/index.tsx new file mode 100644 index 0000000000..e53ef91916 --- /dev/null +++ b/apps/core/src/components/affine/sign-out-modal/index.tsx @@ -0,0 +1,46 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { + ConfirmModal, + type ConfirmModalProps, +} from '@toeverything/components/modal'; +import { useMemo } from 'react'; + +type SignOutConfirmModalI18NKeys = + | 'title' + | 'description' + | 'cancel' + | 'confirm'; + +export const SignOutModal = ({ ...props }: ConfirmModalProps) => { + const { title, description, cancelText, confirmButtonOptions = {} } = props; + const t = useAFFiNEI18N(); + + const defaultTexts = useMemo(() => { + const getDefaultText = (key: SignOutConfirmModalI18NKeys) => { + return t[`com.affine.auth.sign-out.confirm-modal.${key}`](); + }; + return { + title: getDefaultText('title'), + description: getDefaultText('description'), + cancelText: getDefaultText('cancel'), + children: getDefaultText('confirm'), + }; + }, [t]); + + return ( + + ); +}; diff --git a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx index 827ad56587..9f1a06bbf0 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx +++ b/apps/core/src/components/pure/workspace-slider-bar/user-with-workspace-list/user-account/index.tsx @@ -10,15 +10,15 @@ import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu'; import { useSetAtom } from 'jotai'; import { useCallback } from 'react'; -import { openSettingModalAtom } from '../../../../../atoms'; -import { signOutCloud } from '../../../../../utils/cloud-utils'; -import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper'; +import { + openSettingModalAtom, + openSignOutModalAtom, +} from '../../../../../atoms'; import * as styles from './index.css'; const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { const setSettingModalAtom = useSetAtom(openSettingModalAtom); - - const { jumpToIndex } = useNavigateHelper(); + const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom); const onOpenAccountSetting = useCallback(() => { setSettingModalAtom(prev => ({ @@ -28,14 +28,10 @@ const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { })); }, [setSettingModalAtom]); - const onSignOut = useCallback(async () => { - signOutCloud() - .then(() => { - jumpToIndex(); - }) - .catch(console.error); + const onOpenSignOutModal = useCallback(() => { onEventEnd?.(); - }, [onEventEnd, jumpToIndex]); + setOpenSignOutModalAtom(true); + }, [onEventEnd, setOpenSignOutModalAtom]); const t = useAFFiNEI18N(); @@ -47,7 +43,7 @@ const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { } - data-testid="editor-option-menu-import" + data-testid="workspace-modal-account-settings-option" onClick={onOpenAccountSetting} > {t['com.affine.workspace.cloud.account.settings']()} @@ -59,8 +55,8 @@ const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { } - data-testid="editor-option-menu-import" - onClick={onSignOut} + data-testid="workspace-modal-sign-out-option" + onClick={onOpenSignOutModal} > {t['com.affine.workspace.cloud.account.logout']()} @@ -86,7 +82,7 @@ export const UserAccountItem = ({ }} > } type="plain" /> diff --git a/apps/core/src/pages/404.tsx b/apps/core/src/pages/404.tsx index 43dcfda28d..4afdc78a52 100644 --- a/apps/core/src/pages/404.tsx +++ b/apps/core/src/pages/404.tsx @@ -2,36 +2,52 @@ import { NotFoundPage } from '@affine/component/not-found-page'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useSession } from 'next-auth/react'; import type { ReactElement } from 'react'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; +import { SignOutModal } from '../components/affine/sign-out-modal'; import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; import { signOutCloud } from '../utils/cloud-utils'; export const Component = (): ReactElement => { const { data: session } = useSession(); const { jumpToIndex } = useNavigateHelper(); + const [open, setOpen] = useState(false); + const handleBackButtonClick = useCallback( () => jumpToIndex(RouteLogic.REPLACE), [jumpToIndex] ); - const handleSignOut = useCallback(async () => { + + const handleOpenSignOutModal = useCallback(() => { + setOpen(true); + }, [setOpen]); + + const onConfirmSignOut = useCallback(async () => { + setOpen(false); await signOutCloud({ callbackUrl: '/signIn', }); - }, []); + }, [setOpen]); return ( - + <> + + + ); }; diff --git a/apps/core/src/providers/modal-provider.tsx b/apps/core/src/providers/modal-provider.tsx index fa95760e38..56f910733e 100644 --- a/apps/core/src/providers/modal-provider.tsx +++ b/apps/core/src/providers/modal-provider.tsx @@ -10,9 +10,11 @@ import { openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, openSettingModalAtom, + openSignOutModalAtom, } from '../atoms'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; +import { signOutCloud } from '../utils/cloud-utils'; const SettingModal = lazy(() => import('../components/affine/setting-modal').then(module => ({ @@ -45,6 +47,12 @@ const OnboardingModal = lazy(() => })) ); +const SignOutModal = lazy(() => + import('../components/affine/sign-out-modal').then(module => ({ + default: module.SignOutModal, + })) +); + export const Setting = () => { const [currentWorkspace] = useCurrentWorkspace(); const [{ open, workspaceId, activeTab }, setOpenSettingModalAtom] = @@ -141,6 +149,24 @@ export function CurrentWorkspaceModals() { ); } +export const SignOutConfirmModal = () => { + const { jumpToIndex } = useNavigateHelper(); + const [open, setOpen] = useAtom(openSignOutModalAtom); + + const onConfirm = useCallback(async () => { + setOpen(false); + signOutCloud() + .then(() => { + jumpToIndex(); + }) + .catch(console.error); + }, [jumpToIndex, setOpen]); + + return ( + + ); +}; + export const AllWorkspaceModals = (): ReactElement => { const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( openCreateWorkspaceModalAtom @@ -172,6 +198,9 @@ export const AllWorkspaceModals = (): ReactElement => { + + + ); }; diff --git a/packages/component/src/components/share-menu/share-menu.tsx b/packages/component/src/components/share-menu/share-menu.tsx index fe9e604930..81400c235d 100644 --- a/packages/component/src/components/share-menu/share-menu.tsx +++ b/packages/component/src/components/share-menu/share-menu.tsx @@ -58,6 +58,7 @@ const LocalShareMenu = (props: ShareMenuProps) => { items={} contentOptions={{ className: styles.menuStyle, + ['data-testid' as string]: 'local-share-menu', }} rootOptions={{ modal: false, @@ -81,6 +82,7 @@ const CloudShareMenu = (props: ShareMenuProps) => { items={} contentOptions={{ className: styles.menuStyle, + ['data-testid' as string]: 'cloud-share-menu', }} rootOptions={{ modal: false, diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index 76e433514e..3889d7a5b0 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -633,5 +633,9 @@ "com.affine.share-menu.publish-to-web.description": "Let anyone with a link view a read-only version of this page.", "com.affine.share-menu.share-privately": "Share Privately", "com.affine.share-menu.share-privately.description": "Only members of this Workspace can open this link.", - "com.affine.share-menu.copy-private-link": "Copy Private Link" + "com.affine.share-menu.copy-private-link": "Copy Private Link", + "com.affine.auth.sign-out.confirm-modal.title": "Sign out?", + "com.affine.auth.sign-out.confirm-modal.description": "After signing out, the Cloud Workspaces associated with this account will be removed from the current device, and signing in again will add them back.", + "com.affine.auth.sign-out.confirm-modal.cancel": "Cancel", + "com.affine.auth.sign-out.confirm-modal.confirm": "Sign Out" } diff --git a/tests/affine-cloud/e2e/collaboration.spec.ts b/tests/affine-cloud/e2e/collaboration.spec.ts index 1c0823ac82..513d92aa62 100644 --- a/tests/affine-cloud/e2e/collaboration.spec.ts +++ b/tests/affine-cloud/e2e/collaboration.spec.ts @@ -18,6 +18,7 @@ import { } from '@affine-test/kit/utils/setting'; import { clickSideBarAllPageButton, + clickSideBarCurrentWorkspaceBanner, clickSideBarSettingButton, } from '@affine-test/kit/utils/sidebar'; import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; @@ -189,6 +190,7 @@ test.describe('collaboration', () => { await clickSideBarSettingButton(page); await clickUserInfoCard(page); await page.getByTestId('sign-out-button').click(); + await page.getByTestId('confirm-sign-out-button').click(); await page.waitForTimeout(5000); expect(page.url()).toBe(url); }); @@ -260,3 +262,26 @@ test.describe('collaboration members', () => { expect(await page.locator('[data-testid="member-item"]').count()).toBe(3); }); }); + +test.describe('sign out', () => { + test('can sign out', async ({ page }) => { + await page.reload(); + await waitForEditorLoad(page); + await createLocalWorkspace( + { + name: 'test', + }, + page + ); + await clickSideBarAllPageButton(page); + const currentUrl = page.url(); + await clickSideBarCurrentWorkspaceBanner(page); + await page.getByTestId('workspace-modal-account-option').click(); + await page.getByTestId('workspace-modal-sign-out-option').click(); + await page.getByTestId('confirm-sign-out-button').click(); + await clickSideBarCurrentWorkspaceBanner(page); + const signInButton = page.getByTestId('cloud-signin-button'); + await expect(signInButton).toBeVisible(); + expect(page.url()).toBe(currentUrl); + }); +}); diff --git a/tests/affine-desktop/e2e/basic.spec.ts b/tests/affine-desktop/e2e/basic.spec.ts index 921fad362d..5b97c36771 100644 --- a/tests/affine-desktop/e2e/basic.spec.ts +++ b/tests/affine-desktop/e2e/basic.spec.ts @@ -2,7 +2,10 @@ import { platform } from 'node:os'; import { test } from '@affine-test/kit/electron'; import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic'; -import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar'; +import { + clickSideBarCurrentWorkspaceBanner, + clickSideBarSettingButton, +} from '@affine-test/kit/utils/sidebar'; import { expect } from '@playwright/test'; test('new page', async ({ page, workspace }) => { @@ -156,7 +159,7 @@ test('windows only check', async ({ page }) => { }); test('delete workspace', async ({ page }) => { - await page.getByTestId('current-workspace').click(); + await clickSideBarCurrentWorkspaceBanner(page); await page.getByTestId('new-workspace').click(); await page .getByTestId('create-workspace-input') diff --git a/tests/affine-desktop/e2e/workspace.spec.ts b/tests/affine-desktop/e2e/workspace.spec.ts index e406039dec..51d09fed89 100644 --- a/tests/affine-desktop/e2e/workspace.spec.ts +++ b/tests/affine-desktop/e2e/workspace.spec.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import { test } from '@affine-test/kit/electron'; +import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar'; import { expect } from '@playwright/test'; import fs from 'fs-extra'; @@ -85,7 +86,7 @@ test.fixme('export then add', async ({ page, appInfo, workspace }) => { // add workspace // we are reusing the same db file so that we don't need to maintain one // in the codebase - await page.getByTestId('current-workspace').click(); + await clickSideBarCurrentWorkspaceBanner(page); await page.getByTestId('add-or-new-workspace').click(); await page.evaluate(tmpPath => { diff --git a/tests/affine-local/e2e/router.spec.ts b/tests/affine-local/e2e/router.spec.ts index 2605234b79..09ab7e6264 100644 --- a/tests/affine-local/e2e/router.spec.ts +++ b/tests/affine-local/e2e/router.spec.ts @@ -7,7 +7,7 @@ test('goto not found page', async ({ page }) => { await openHomePage(page); await waitForEditorLoad(page); const currentUrl = page.url(); - const invalidUrl = currentUrl.replace('hello-world', 'invalid'); + const invalidUrl = currentUrl.concat('invalid'); await page.goto(invalidUrl); await expect(page.getByTestId('not-found')).toBeVisible({ timeout: 10000, diff --git a/tests/kit/utils/cloud.ts b/tests/kit/utils/cloud.ts index 5c8fd8877f..bc12f52f8f 100644 --- a/tests/kit/utils/cloud.ts +++ b/tests/kit/utils/cloud.ts @@ -9,7 +9,12 @@ import { } 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'; +import { + type BrowserContext, + type Cookie, + expect, + type Page, +} from '@playwright/test'; import { z } from 'zod'; export async function getCurrentMailMessageCount() { @@ -176,8 +181,33 @@ export async function enableCloudWorkspace(page: Page) { await waitForEditorLoad(page); await clickNewPageButton(page); } + export async function enableCloudWorkspaceFromShareButton(page: Page) { - await page.getByTestId('local-share-menu-button').click(); + const shareMenuButton = page.getByTestId('local-share-menu-button'); + expect(await shareMenuButton.isVisible()).toBeTruthy(); + + // FIXME: this is a workaround for the flaky test + // For unknown reasons, + // the online ci test on GitHub is unable to detect the local-share-menu, + // although it works fine in local testing. + // To ensure the tests pass consistently, I’ve made the following temporary adjustments. + // { + const maxAttempts = 5; + let attempt = 0; + let menuVisible = false; + + while (!menuVisible && attempt < maxAttempts) { + try { + await shareMenuButton.click(); + menuVisible = await page.getByTestId('local-share-menu').isVisible(); + } catch (e) { + console.error(`Attempt ${attempt + 1} failed: ${e}`); + attempt += 1; + } + } + expect(menuVisible).toBeTruthy(); + // } + await page.getByTestId('share-menu-enable-affine-cloud-button').click(); await page.getByTestId('confirm-enable-affine-cloud-button').click(); // wait for upload and delete local workspace