feat(core): add sign out confirm modal (#4592)

This commit is contained in:
JimmFly 2023-10-16 16:44:09 +08:00 committed by GitHub
parent c1d386d932
commit 07b5d18441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 199 additions and 50 deletions

View File

@ -11,6 +11,7 @@ export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false); export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
export const openQuickSearchModalAtom = atom(false); export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false); export const openOnboardingModalAtom = atom(false);
export const openSignOutModalAtom = atom(false);
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & { export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
open: boolean; open: boolean;

View File

@ -23,10 +23,8 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { authAtom } from '../../../../atoms'; import { authAtom, openSignOutModalAtom } from '../../../../atoms';
import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; 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 { Upload } from '../../../pure/file-upload';
import * as style from './style.css'; import * as style from './style.css';
@ -168,8 +166,8 @@ const StoragePanel = () => {
export const AccountSetting: FC = () => { export const AccountSetting: FC = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const user = useCurrentUser(); const user = useCurrentUser();
const { jumpToIndex } = useNavigateHelper();
const setAuthModal = useSetAtom(authAtom); const setAuthModal = useSetAtom(authAtom);
const setSignOutModal = useSetAtom(openSignOutModalAtom);
const onChangeEmail = useCallback(() => { const onChangeEmail = useCallback(() => {
setAuthModal({ setAuthModal({
@ -189,6 +187,10 @@ export const AccountSetting: FC = () => {
}); });
}, [setAuthModal, user.email, user.hasPassword]); }, [setAuthModal, user.email, user.hasPassword]);
const onOpenSignOutModal = useCallback(() => {
setSignOutModal(true);
}, [setSignOutModal]);
return ( return (
<> <>
<SettingHeader <SettingHeader
@ -220,13 +222,7 @@ export const AccountSetting: FC = () => {
desc={t['com.affine.setting.sign.out.message']()} desc={t['com.affine.setting.sign.out.message']()}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
data-testid="sign-out-button" data-testid="sign-out-button"
onClick={useCallback(() => { onClick={onOpenSignOutModal}
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
}, [jumpToIndex])}
> >
<ArrowRightSmallIcon /> <ArrowRightSmallIcon />
</SettingRow> </SettingRow>

View File

@ -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 (
<ConfirmModal
title={title ?? defaultTexts.title}
description={description ?? defaultTexts.description}
cancelText={cancelText ?? defaultTexts.cancelText}
confirmButtonOptions={{
type: 'error',
['data-testid' as string]: 'confirm-sign-out-button',
children: confirmButtonOptions.children ?? defaultTexts.children,
}}
contentOptions={{
['data-testid' as string]: 'confirm-sign-out-modal',
}}
{...props}
/>
);
};

View File

@ -10,15 +10,15 @@ import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { openSettingModalAtom } from '../../../../../atoms'; import {
import { signOutCloud } from '../../../../../utils/cloud-utils'; openSettingModalAtom,
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper'; openSignOutModalAtom,
} from '../../../../../atoms';
import * as styles from './index.css'; import * as styles from './index.css';
const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => { const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => {
const setSettingModalAtom = useSetAtom(openSettingModalAtom); const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const setOpenSignOutModalAtom = useSetAtom(openSignOutModalAtom);
const { jumpToIndex } = useNavigateHelper();
const onOpenAccountSetting = useCallback(() => { const onOpenAccountSetting = useCallback(() => {
setSettingModalAtom(prev => ({ setSettingModalAtom(prev => ({
@ -28,14 +28,10 @@ const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => {
})); }));
}, [setSettingModalAtom]); }, [setSettingModalAtom]);
const onSignOut = useCallback(async () => { const onOpenSignOutModal = useCallback(() => {
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
onEventEnd?.(); onEventEnd?.();
}, [onEventEnd, jumpToIndex]); setOpenSignOutModalAtom(true);
}, [onEventEnd, setOpenSignOutModalAtom]);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -47,7 +43,7 @@ const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => {
<AccountIcon /> <AccountIcon />
</MenuIcon> </MenuIcon>
} }
data-testid="editor-option-menu-import" data-testid="workspace-modal-account-settings-option"
onClick={onOpenAccountSetting} onClick={onOpenAccountSetting}
> >
{t['com.affine.workspace.cloud.account.settings']()} {t['com.affine.workspace.cloud.account.settings']()}
@ -59,8 +55,8 @@ const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => {
<SignOutIcon /> <SignOutIcon />
</MenuIcon> </MenuIcon>
} }
data-testid="editor-option-menu-import" data-testid="workspace-modal-sign-out-option"
onClick={onSignOut} onClick={onOpenSignOutModal}
> >
{t['com.affine.workspace.cloud.account.logout']()} {t['com.affine.workspace.cloud.account.logout']()}
</MenuItem> </MenuItem>
@ -86,7 +82,7 @@ export const UserAccountItem = ({
}} }}
> >
<IconButton <IconButton
data-testid="more-button" data-testid="workspace-modal-account-option"
icon={<MoreHorizontalIcon />} icon={<MoreHorizontalIcon />}
type="plain" type="plain"
/> />

View File

@ -2,24 +2,34 @@ import { NotFoundPage } from '@affine/component/not-found-page';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import type { ReactElement } from '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 { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
import { signOutCloud } from '../utils/cloud-utils'; import { signOutCloud } from '../utils/cloud-utils';
export const Component = (): ReactElement => { export const Component = (): ReactElement => {
const { data: session } = useSession(); const { data: session } = useSession();
const { jumpToIndex } = useNavigateHelper(); const { jumpToIndex } = useNavigateHelper();
const [open, setOpen] = useState(false);
const handleBackButtonClick = useCallback( const handleBackButtonClick = useCallback(
() => jumpToIndex(RouteLogic.REPLACE), () => jumpToIndex(RouteLogic.REPLACE),
[jumpToIndex] [jumpToIndex]
); );
const handleSignOut = useCallback(async () => {
const handleOpenSignOutModal = useCallback(() => {
setOpen(true);
}, [setOpen]);
const onConfirmSignOut = useCallback(async () => {
setOpen(false);
await signOutCloud({ await signOutCloud({
callbackUrl: '/signIn', callbackUrl: '/signIn',
}); });
}, []); }, [setOpen]);
return ( return (
<>
<NotFoundPage <NotFoundPage
user={ user={
session?.user session?.user
@ -31,7 +41,13 @@ export const Component = (): ReactElement => {
: null : null
} }
onBack={handleBackButtonClick} onBack={handleBackButtonClick}
onSignOut={handleSignOut} onSignOut={handleOpenSignOutModal}
/> />
<SignOutModal
open={open}
onOpenChange={setOpen}
onConfirm={onConfirmSignOut}
/>
</>
); );
}; };

View File

@ -10,9 +10,11 @@ import {
openCreateWorkspaceModalAtom, openCreateWorkspaceModalAtom,
openDisableCloudAlertModalAtom, openDisableCloudAlertModalAtom,
openSettingModalAtom, openSettingModalAtom,
openSignOutModalAtom,
} from '../atoms'; } from '../atoms';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { signOutCloud } from '../utils/cloud-utils';
const SettingModal = lazy(() => const SettingModal = lazy(() =>
import('../components/affine/setting-modal').then(module => ({ 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 = () => { export const Setting = () => {
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const [{ open, workspaceId, activeTab }, setOpenSettingModalAtom] = 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 (
<SignOutModal open={open} onOpenChange={setOpen} onConfirm={onConfirm} />
);
};
export const AllWorkspaceModals = (): ReactElement => { export const AllWorkspaceModals = (): ReactElement => {
const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom( const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
openCreateWorkspaceModalAtom openCreateWorkspaceModalAtom
@ -172,6 +198,9 @@ export const AllWorkspaceModals = (): ReactElement => {
<Suspense> <Suspense>
<AuthModal /> <AuthModal />
</Suspense> </Suspense>
<Suspense>
<SignOutConfirmModal />
</Suspense>
</> </>
); );
}; };

View File

@ -58,6 +58,7 @@ const LocalShareMenu = (props: ShareMenuProps) => {
items={<ShareMenuContent {...props} />} items={<ShareMenuContent {...props} />}
contentOptions={{ contentOptions={{
className: styles.menuStyle, className: styles.menuStyle,
['data-testid' as string]: 'local-share-menu',
}} }}
rootOptions={{ rootOptions={{
modal: false, modal: false,
@ -81,6 +82,7 @@ const CloudShareMenu = (props: ShareMenuProps) => {
items={<ShareMenuContent {...props} />} items={<ShareMenuContent {...props} />}
contentOptions={{ contentOptions={{
className: styles.menuStyle, className: styles.menuStyle,
['data-testid' as string]: 'cloud-share-menu',
}} }}
rootOptions={{ rootOptions={{
modal: false, modal: false,

View File

@ -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.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": "Share Privately",
"com.affine.share-menu.share-privately.description": "Only members of this Workspace can open this link.", "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"
} }

View File

@ -18,6 +18,7 @@ import {
} from '@affine-test/kit/utils/setting'; } from '@affine-test/kit/utils/setting';
import { import {
clickSideBarAllPageButton, clickSideBarAllPageButton,
clickSideBarCurrentWorkspaceBanner,
clickSideBarSettingButton, clickSideBarSettingButton,
} from '@affine-test/kit/utils/sidebar'; } from '@affine-test/kit/utils/sidebar';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
@ -189,6 +190,7 @@ test.describe('collaboration', () => {
await clickSideBarSettingButton(page); await clickSideBarSettingButton(page);
await clickUserInfoCard(page); await clickUserInfoCard(page);
await page.getByTestId('sign-out-button').click(); await page.getByTestId('sign-out-button').click();
await page.getByTestId('confirm-sign-out-button').click();
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
expect(page.url()).toBe(url); expect(page.url()).toBe(url);
}); });
@ -260,3 +262,26 @@ test.describe('collaboration members', () => {
expect(await page.locator('[data-testid="member-item"]').count()).toBe(3); 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);
});
});

View File

@ -2,7 +2,10 @@ import { platform } from 'node:os';
import { test } from '@affine-test/kit/electron'; import { test } from '@affine-test/kit/electron';
import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic'; 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'; import { expect } from '@playwright/test';
test('new page', async ({ page, workspace }) => { test('new page', async ({ page, workspace }) => {
@ -156,7 +159,7 @@ test('windows only check', async ({ page }) => {
}); });
test('delete workspace', 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('new-workspace').click();
await page await page
.getByTestId('create-workspace-input') .getByTestId('create-workspace-input')

View File

@ -1,6 +1,7 @@
import path from 'node:path'; import path from 'node:path';
import { test } from '@affine-test/kit/electron'; import { test } from '@affine-test/kit/electron';
import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
import fs from 'fs-extra'; import fs from 'fs-extra';
@ -85,7 +86,7 @@ test.fixme('export then add', async ({ page, appInfo, workspace }) => {
// add workspace // add workspace
// we are reusing the same db file so that we don't need to maintain one // we are reusing the same db file so that we don't need to maintain one
// in the codebase // in the codebase
await page.getByTestId('current-workspace').click(); await clickSideBarCurrentWorkspaceBanner(page);
await page.getByTestId('add-or-new-workspace').click(); await page.getByTestId('add-or-new-workspace').click();
await page.evaluate(tmpPath => { await page.evaluate(tmpPath => {

View File

@ -7,7 +7,7 @@ test('goto not found page', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
const currentUrl = page.url(); const currentUrl = page.url();
const invalidUrl = currentUrl.replace('hello-world', 'invalid'); const invalidUrl = currentUrl.concat('invalid');
await page.goto(invalidUrl); await page.goto(invalidUrl);
await expect(page.getByTestId('not-found')).toBeVisible({ await expect(page.getByTestId('not-found')).toBeVisible({
timeout: 10000, timeout: 10000,

View File

@ -9,7 +9,12 @@ import {
} from '@affine-test/kit/utils/sidebar'; } 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,
type Cookie,
expect,
type Page,
} from '@playwright/test';
import { z } from 'zod'; import { z } from 'zod';
export async function getCurrentMailMessageCount() { export async function getCurrentMailMessageCount() {
@ -176,8 +181,33 @@ export async function enableCloudWorkspace(page: Page) {
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickNewPageButton(page); await clickNewPageButton(page);
} }
export async function enableCloudWorkspaceFromShareButton(page: 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, Ive 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('share-menu-enable-affine-cloud-button').click();
await page.getByTestId('confirm-enable-affine-cloud-button').click(); await page.getByTestId('confirm-enable-affine-cloud-button').click();
// wait for upload and delete local workspace // wait for upload and delete local workspace