From d3dc83b77611568230488f21afc8eaa4c3cc7f04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timoth=C3=A9e=20Alby?=
Date: Mon, 25 Apr 2022 13:05:07 +0200
Subject: [PATCH] Merge pull request #7722 from
overleaf/ii-add-email-ui-without-affiliations
Add emails without affiliations
GitOrigin-RevId: 13d53b604f8d7cf0f36b2c5caea85ecc15cfc6d5
---
.../web/frontend/extracted-translations.json | 4 +
.../settings/components/emails-section.tsx | 48 +++---
.../settings/components/emails/add-email.tsx | 145 ++++++++++++++++
.../emails/institution-and-role.tsx | 2 +-
.../settings/components/emails/row.tsx | 2 +-
.../settings/context/user-email-context.tsx | 13 +-
.../stylesheets/app/account-settings.less | 3 +
.../emails-section-add-new-email.test.tsx | 159 ++++++++++++++++++
8 files changed, 346 insertions(+), 30 deletions(-)
create mode 100644 services/web/frontend/js/features/settings/components/emails/add-email.tsx
create mode 100644 services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 4ccacf1639..17c1f35251 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -2,7 +2,9 @@
"access_your_projects_with_git": "",
"account_not_linked_to_dropbox": "",
"account_settings": "",
+ "add_another_email": "",
"add_files": "",
+ "add_new_email": "",
"add_role_and_department": "",
"also": "",
"anyone_with_link_can_edit": "",
@@ -203,6 +205,7 @@
"invalid_filename": "",
"invalid_request": "",
"invite_not_accepted": "",
+ "is_email_affiliated": "",
"last_name": "",
"layout": "",
"layout_processing": "",
@@ -210,6 +213,7 @@
"learn_more": "",
"learn_more_about_link_sharing": "",
"learn_more_about_the_symbol_palette": "",
+ "let_us_know": "",
"link": "",
"link_sharing_is_off": "",
"link_sharing_is_on": "",
diff --git a/services/web/frontend/js/features/settings/components/emails-section.tsx b/services/web/frontend/js/features/settings/components/emails-section.tsx
index e42b69f56a..cc010d38e5 100644
--- a/services/web/frontend/js/features/settings/components/emails-section.tsx
+++ b/services/web/frontend/js/features/settings/components/emails-section.tsx
@@ -7,6 +7,7 @@ import {
} from '../context/user-email-context'
import EmailsHeader from './emails/header'
import EmailsRow from './emails/row'
+import AddEmail from './emails/add-email'
import Icon from '../../../shared/components/icon'
import { Alert } from 'react-bootstrap'
import { ExposedSettings } from '../../../../../types/exposed-settings'
@@ -16,7 +17,6 @@ function EmailsSectionContent() {
const {
state: { data: userEmailsData },
isInitializing,
- isInitializingSuccess,
isInitializingError,
} = useUserEmailsContext()
const userEmails = Object.values(userEmailsData.byId)
@@ -32,30 +32,30 @@ function EmailsSectionContent() {
- {isInitializing && (
-
- {t('loading')}...
-
- )}
- {isInitializingSuccess && (
- <>
-
- {userEmails?.map((userEmail, i) => (
-
-
- {i + 1 !== userEmails.length && (
+
+
+ {isInitializing ? (
+
+ {t('loading')}...
+
+ ) : (
+ <>
+ {userEmails?.map(userEmail => (
+
+
- )}
-
- ))}
- >
- )}
- {isInitializingError && (
-
- {' '}
- {t('error_performing_request')}
-
- )}
+
+ ))}
+ >
+ )}
+
+ {isInitializingError && (
+
+ {' '}
+ {t('error_performing_request')}
+
+ )}
+
>
)
}
diff --git a/services/web/frontend/js/features/settings/components/emails/add-email.tsx b/services/web/frontend/js/features/settings/components/emails/add-email.tsx
new file mode 100644
index 0000000000..b337e65f4f
--- /dev/null
+++ b/services/web/frontend/js/features/settings/components/emails/add-email.tsx
@@ -0,0 +1,145 @@
+import { useState, useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Row, Col } from 'react-bootstrap'
+import Cell from './cell'
+import useAsync from '../../../../shared/hooks/use-async'
+import { useUserEmailsContext } from '../../context/user-email-context'
+import { postJSON } from '../../../../infrastructure/fetch-json'
+import Icon from '../../../../shared/components/icon'
+
+const isValidEmail = (email: string) => {
+ return Boolean(email)
+}
+
+function AddEmail() {
+ const { t } = useTranslation()
+ const [isFormVisible, setIsFormVisible] = useState(
+ () => window.location.hash === '#add-email'
+ )
+ const [newEmail, setNewEmail] = useState('')
+ const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] =
+ useState(false)
+ const { isLoading, isError, runAsync } = useAsync()
+ const {
+ state,
+ setLoading: setUserEmailsContextLoading,
+ getEmails,
+ } = useUserEmailsContext()
+
+ useEffect(() => {
+ setUserEmailsContextLoading(isLoading)
+ }, [setUserEmailsContextLoading, isLoading])
+
+ const handleShowAddEmailForm = () => {
+ setIsFormVisible(true)
+ }
+
+ const handleShowInstitutionFields = () => {
+ setIsInstitutionFieldsVisible(true)
+ }
+
+ const handleEmailChange = (event: React.ChangeEvent) => {
+ setNewEmail(event.target.value)
+ }
+
+ const handleAddNewEmail = () => {
+ runAsync(
+ postJSON('/user/emails', {
+ body: {
+ email: newEmail,
+ },
+ })
+ )
+ .then(() => {
+ getEmails()
+ setIsFormVisible(false)
+ setNewEmail('')
+ })
+ .catch(error => {
+ console.error(error)
+ })
+ }
+
+ return (
+
+
+ {!isFormVisible ? (
+
+ |
+
+ |
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+export default AddEmail
diff --git a/services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx b/services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx
index 2d87ad7d50..602f7fde4b 100644
--- a/services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx
+++ b/services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx
@@ -99,7 +99,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
setValue={setDepartment}
/>
-
+
{userEmailData.affiliation?.institution && (
diff --git a/services/web/frontend/js/features/settings/context/user-email-context.tsx b/services/web/frontend/js/features/settings/context/user-email-context.tsx
index ffb918a490..961ec2dc91 100644
--- a/services/web/frontend/js/features/settings/context/user-email-context.tsx
+++ b/services/web/frontend/js/features/settings/context/user-email-context.tsx
@@ -196,9 +196,9 @@ const reducer = (state: State, action: Action) => {
function useUserEmails() {
const [state, unsafeDispatch] = useReducer(reducer, initialState)
const dispatch = useSafeDispatch(unsafeDispatch)
- const { isLoading, isSuccess, isError, runAsync } = useAsync()
+ const { data, isLoading, isError, runAsync } = useAsync()
- useEffect(() => {
+ const getEmails = useCallback(() => {
runAsync(getJSON('/user/emails?ensureAffiliation=true'))
.then(data => {
dispatch(ActionCreators.setData(data))
@@ -206,11 +206,16 @@ function useUserEmails() {
.catch(() => {})
}, [runAsync, dispatch])
+ // Get emails on page load
+ useEffect(() => {
+ getEmails()
+ }, [getEmails])
+
return {
state,
- isInitializing: isLoading,
- isInitializingSuccess: isSuccess,
+ isInitializing: isLoading && !data,
isInitializingError: isError,
+ getEmails,
setLoading: useCallback(
(flag: boolean) => dispatch(ActionCreators.setLoading(flag)),
[dispatch]
diff --git a/services/web/frontend/stylesheets/app/account-settings.less b/services/web/frontend/stylesheets/app/account-settings.less
index edb5f9f80a..85ead9bb7e 100644
--- a/services/web/frontend/stylesheets/app/account-settings.less
+++ b/services/web/frontend/stylesheets/app/account-settings.less
@@ -26,6 +26,9 @@
.affiliations-table-cell {
padding: 0.5rem;
}
+.affiliations-table-row--highlighted {
+ background-color: tint(@content-alt-bg-color, 6%);
+}
.affiliations-table-email {
width: 40%;
}
diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx
new file mode 100644
index 0000000000..8e6ffcba1a
--- /dev/null
+++ b/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx
@@ -0,0 +1,159 @@
+import {
+ render,
+ screen,
+ fireEvent,
+ waitForElementToBeRemoved,
+} from '@testing-library/react'
+import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
+import { expect } from 'chai'
+import fetchMock from 'fetch-mock'
+import { UserEmailData } from '../../../../../../types/user-email'
+
+const userEmailData: UserEmailData = {
+ affiliation: {
+ cachedConfirmedAt: null,
+ cachedPastReconfirmDate: false,
+ cachedReconfirmedAt: null,
+ department: 'Art History',
+ institution: {
+ commonsAccount: false,
+ confirmed: true,
+ id: 1,
+ isUniversity: false,
+ name: 'Overleaf',
+ ssoEnabled: false,
+ ssoBeta: false,
+ },
+ inReconfirmNotificationPeriod: false,
+ inferred: false,
+ licence: 'pro_plus',
+ pastReconfirmDate: false,
+ portal: { slug: '', templates_count: 1 },
+ role: 'Reader',
+ },
+ email: 'baz@overleaf.com',
+ default: false,
+}
+
+describe('', function () {
+ beforeEach(function () {
+ window.metaAttributesCache.set('ol-ExposedSettings', {
+ hasAffiliationsFeature: true,
+ })
+ fetchMock.reset()
+ })
+
+ afterEach(function () {
+ window.metaAttributesCache = new Map()
+ fetchMock.reset()
+ })
+
+ it('renders "add another email" button', function () {
+ fetchMock.get('/user/emails?ensureAffiliation=true', [])
+ render()
+
+ screen.getByRole('button', { name: /add another email/i })
+ })
+
+ it('renders input', async function () {
+ fetchMock.get('/user/emails?ensureAffiliation=true', [])
+ render()
+
+ const addAnotherEmailBtn = (await screen.findByRole('button', {
+ name: /add another email/i,
+ })) as HTMLButtonElement
+ fireEvent.click(addAnotherEmailBtn)
+
+ screen.getByLabelText(/email/i)
+ })
+
+ it('renders "add new email" button', async function () {
+ fetchMock.get('/user/emails?ensureAffiliation=true', [])
+ render()
+
+ const addAnotherEmailBtn = (await screen.findByRole('button', {
+ name: /add another email/i,
+ })) as HTMLButtonElement
+ fireEvent.click(addAnotherEmailBtn)
+
+ screen.getByRole('button', { name: /add new email/i })
+ })
+
+ it('adds new email address', async function () {
+ fetchMock.get('/user/emails?ensureAffiliation=true', [])
+ render()
+
+ await fetchMock.flush(true)
+ fetchMock.reset()
+ fetchMock
+ .get('/user/emails?ensureAffiliation=true', [userEmailData])
+ .post('/user/emails', 200)
+
+ const addAnotherEmailBtn = await screen.findByRole('button', {
+ name: /add another email/i,
+ })
+
+ fireEvent.click(addAnotherEmailBtn)
+ const input = screen.getByLabelText(/email/i)
+
+ fireEvent.change(input, {
+ target: { value: userEmailData.email },
+ })
+
+ const submitBtn = screen.getByRole('button', {
+ name: /add new email/i,
+ }) as HTMLButtonElement
+
+ expect(submitBtn.disabled).to.be.false
+
+ fireEvent.click(submitBtn)
+
+ expect(submitBtn.disabled).to.be.true
+
+ await waitForElementToBeRemoved(() =>
+ screen.getByRole('button', {
+ name: /add new email/i,
+ })
+ )
+
+ screen.getByText(userEmailData.email)
+ })
+
+ it('fails to add add new email address', async function () {
+ fetchMock.get('/user/emails?ensureAffiliation=true', [])
+ render()
+
+ await fetchMock.flush(true)
+ fetchMock.reset()
+ fetchMock
+ .get('/user/emails?ensureAffiliation=true', [])
+ .post('/user/emails', 500)
+
+ const addAnotherEmailBtn = await screen.findByRole('button', {
+ name: /add another email/i,
+ })
+
+ fireEvent.click(addAnotherEmailBtn)
+ const input = screen.getByLabelText(/email/i)
+
+ fireEvent.change(input, {
+ target: { value: userEmailData.email },
+ })
+
+ const submitBtn = screen.getByRole('button', {
+ name: /add new email/i,
+ }) as HTMLButtonElement
+
+ expect(submitBtn.disabled).to.be.false
+
+ fireEvent.click(submitBtn)
+
+ expect(submitBtn.disabled).to.be.true
+
+ await screen.findByText(
+ /an error has occurred while performing your request/i
+ )
+ expect(submitBtn).to.not.be.null
+ expect(submitBtn.disabled).to.be.false
+ })
+})