mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #7722 from overleaf/ii-add-email-ui-without-affiliations
Add emails without affiliations GitOrigin-RevId: 13d53b604f8d7cf0f36b2c5caea85ecc15cfc6d5
This commit is contained in:
parent
bb59627db3
commit
d3dc83b776
8 changed files with 346 additions and 30 deletions
|
@ -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": "",
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue