Merge pull request #7722 from overleaf/ii-add-email-ui-without-affiliations

Add emails without affiliations

GitOrigin-RevId: 13d53b604f8d7cf0f36b2c5caea85ecc15cfc6d5
This commit is contained in:
Timothée Alby 2022-04-25 13:05:07 +02:00 committed by Copybot
parent bb59627db3
commit d3dc83b776
8 changed files with 346 additions and 30 deletions

View file

@ -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": "",

View file

@ -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() {
<a href="/learn/how-to/Keeping_your_account_secure" />
</Trans>
</p>
{isInitializing && (
<div className="text-center">
<Icon type="refresh" fw spin /> {t('loading')}...
</div>
)}
{isInitializingSuccess && (
<>
<EmailsHeader />
{userEmails?.map((userEmail, i) => (
<Fragment key={userEmail.email}>
<EmailsRow userEmailData={userEmail} />
{i + 1 !== userEmails.length && (
<div className="table">
<EmailsHeader />
{isInitializing ? (
<div className="text-center">
<Icon type="refresh" fw spin /> {t('loading')}...
</div>
) : (
<>
{userEmails?.map(userEmail => (
<Fragment key={userEmail.email}>
<EmailsRow userEmailData={userEmail} />
<div className="horizontal-divider" />
)}
</Fragment>
))}
</>
)}
{isInitializingError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</Alert>
)}
</Fragment>
))}
</>
)}
<AddEmail />
{isInitializingError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</Alert>
)}
</div>
</>
)
}

View file

@ -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<HTMLInputElement>) => {
setNewEmail(event.target.value)
}
const handleAddNewEmail = () => {
runAsync(
postJSON('/user/emails', {
body: {
email: newEmail,
},
})
)
.then(() => {
getEmails()
setIsFormVisible(false)
setNewEmail('')
})
.catch(error => {
console.error(error)
})
}
return (
<div className="affiliations-table-row--highlighted">
<Row>
{!isFormVisible ? (
<Col md={4}>
<Cell>
<Button
className="btn-inline-link"
onClick={handleShowAddEmailForm}
>
{t('add_another_email')}
</Button>
</Cell>
</Col>
) : (
<form>
<Col md={4}>
<Cell>
<label htmlFor="affiliations-email" className="sr-only">
{t('email')}
</label>
<input
id="affiliations-email"
className="form-control"
type="email"
onChange={handleEmailChange}
placeholder="e.g. johndoe@mit.edu"
/>
</Cell>
</Col>
<Col md={4}>
<Cell>
{isInstitutionFieldsVisible ? (
<>
<div className="form-group mb-2">
<input className="form-control" />
</div>
<div className="form-group mb-0">
<input className="form-control" />
</div>
</>
) : (
<div className="mt-1">
{t('is_email_affiliated')}
<br />
<Button
className="btn-inline-link"
onClick={handleShowInstitutionFields}
>
{t('let_us_know')}
</Button>
</div>
)}
</Cell>
</Col>
<Col md={4}>
<Cell className="text-md-right">
<Button
bsSize="small"
bsStyle="success"
disabled={
!isValidEmail(newEmail) || isLoading || state.isLoading
}
onClick={handleAddNewEmail}
>
{t('add_new_email')}
</Button>
{isError && (
<div className="text-danger small">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</div>
)}
</Cell>
</Col>
</form>
)}
</Row>
</div>
)
}
export default AddEmail

View file

@ -99,7 +99,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
setValue={setDepartment}
/>
<Button
bsSize="sm"
bsSize="small"
bsStyle="success"
type="submit"
disabled={!role || !department || isLoading || state.isLoading}

View file

@ -17,7 +17,7 @@ function EmailsRow({ userEmailData }: EmailsRowProps) {
<Email userEmailData={userEmailData} />
</EmailCell>
</Col>
<Col sm={4}>
<Col md={4}>
{userEmailData.affiliation?.institution && (
<EmailCell>
<InstitutionAndRole userEmailData={userEmailData} />

View file

@ -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<UserEmailData[]>(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]

View file

@ -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%;
}

View file

@ -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('<EmailsSection />', 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(<EmailsSection />)
screen.getByRole('button', { name: /add another email/i })
})
it('renders input', async function () {
fetchMock.get('/user/emails?ensureAffiliation=true', [])
render(<EmailsSection />)
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(<EmailsSection />)
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(<EmailsSection />)
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(<EmailsSection />)
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
})
})