Merge pull request #7808 from overleaf/ii-settings-fixes-1

[SettingsPage] UI Fixes 1

GitOrigin-RevId: 0e0f605191218af4db70a801ff1e50b47f6e0b01
This commit is contained in:
ilkin-overleaf 2022-04-29 14:10:10 +03:00 committed by Copybot
parent 85f731110c
commit 1def576973
14 changed files with 221 additions and 135 deletions

View file

@ -18,6 +18,7 @@ function EmailsSectionContent() {
state: { data: userEmailsData },
isInitializing,
isInitializingError,
isInitializingSuccess,
} = useUserEmailsContext()
const userEmails = Object.values(userEmailsData.byId)
@ -32,11 +33,13 @@ function EmailsSectionContent() {
<a href="/learn/how-to/Keeping_your_account_secure" />
</Trans>
</p>
<div className="table">
<>
<EmailsHeader />
{isInitializing ? (
<div className="text-center">
<Icon type="refresh" fw spin /> {t('loading')}...
<div className="affiliations-table-row--highlighted">
<div className="affiliations-table-cell text-center">
<Icon type="refresh" fw spin /> {t('loading')}...
</div>
</div>
) : (
<>
@ -48,14 +51,14 @@ function EmailsSectionContent() {
))}
</>
)}
<AddEmail />
{isInitializingSuccess && <AddEmail />}
{isInitializingError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</Alert>
)}
</div>
</>
</>
)
}
@ -73,7 +76,6 @@ function EmailsSection() {
<UserEmailsProvider>
<EmailsSectionContent />
</UserEmailsProvider>
<hr />
</>
)
}

View file

@ -2,7 +2,6 @@ import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import MakePrimary from './actions/make-primary'
import Remove from './actions/remove'
import Icon from '../../../../shared/components/icon'
import useAsync from '../../../../shared/hooks/use-async'
import { useUserEmailsContext } from '../../context/user-email-context'
import { UserEmailData } from '../../../../../../types/user-email'
@ -51,8 +50,7 @@ function Actions({ userEmailData }: ActionsProps) {
/>
{(makePrimaryAsync.isError || deleteEmailAsync.isError) && (
<div className="text-danger small">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
{t('generic_something_went_wrong')}
</div>
)}
</>

View file

@ -4,6 +4,7 @@ import {
useCallback,
useEffect,
useState,
forwardRef,
} from 'react'
import { getJSON } from '../../../../infrastructure/fetch-json'
import useAbortController from '../../../../shared/hooks/use-abort-controller'
@ -38,13 +39,14 @@ export function clearDomainCache() {
type AddEmailInputProps = {
onChange: (value: string, institution?: InstitutionInfo) => void
inputRef?: React.ForwardedRef<HTMLInputElement>
}
export function AddEmailInput({ onChange }: AddEmailInputProps) {
function AddEmailInputBase({ onChange, inputRef }: AddEmailInputProps) {
const { signal } = useAbortController()
const [suggestion, setSuggestion] = useState<string>(null)
const [inputValue, setInputValue] = useState<string>(null)
const [suggestion, setSuggestion] = useState<string | null>(null)
const [inputValue, setInputValue] = useState<string | null>(null)
const [matchedInstitution, setMatchedInstitution] =
useState<InstitutionInfo>(null)
@ -52,7 +54,10 @@ export function AddEmailInput({ onChange }: AddEmailInputProps) {
if (inputValue == null) {
return
}
if (matchedInstitution && suggestion === inputValue) {
if (
matchedInstitution &&
inputValue.endsWith(matchedInstitution.hostname)
) {
onChange(inputValue, matchedInstitution)
} else {
onChange(inputValue)
@ -103,12 +108,23 @@ export function AddEmailInput({ onChange }: AddEmailInputProps) {
const handleKeyDownEvent = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Tab' || event.key === 'Enter') {
const setInputValueAndResetSuggestion = () => {
setInputValue(suggestion)
setSuggestion(null)
}
if (event.key === 'Enter') {
event.preventDefault()
if (suggestion) {
setInputValue(suggestion)
setInputValueAndResetSuggestion()
}
}
if (event.key === 'Tab' && suggestion) {
event.preventDefault()
setInputValueAndResetSuggestion()
}
},
[suggestion]
)
@ -129,7 +145,17 @@ export function AddEmailInput({ onChange }: AddEmailInputProps) {
onKeyDown={handleKeyDownEvent}
value={inputValue || ''}
placeholder="e.g. johndoe@mit.edu"
ref={inputRef}
/>
</div>
)
}
const AddEmailInput = forwardRef<
HTMLInputElement,
Omit<AddEmailInputProps, 'inputRef'>
>((props, ref) => <AddEmailInputBase {...props} inputRef={ref} />)
AddEmailInput.displayName = 'AddEmailInput'
export { AddEmailInput }

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Row, Col } from 'react-bootstrap'
import { Button, Row, Col, Alert } from 'react-bootstrap'
import Cell from './cell'
import Icon from '../../../../shared/components/icon'
import DownshiftInput from './downshift-input'
@ -39,6 +39,8 @@ function AddEmail() {
const [isFormVisible, setIsFormVisible] = useState(
() => window.location.hash === '#add-email'
)
const emailRef = useRef<HTMLInputElement | null>(null)
const countryRef = useRef<HTMLInputElement | null>(null)
const [newEmail, setNewEmail] = useState('')
const [newEmailMatchedInstitution, setNewEmailMatchedInstitution] =
useState<InstitutionInfo | null>(null)
@ -53,7 +55,7 @@ function AddEmail() {
const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] =
useState(false)
const [isUniversityDirty, setIsUniversityDirty] = useState(false)
const { isLoading, isError, runAsync } = useAsync()
const { isLoading, isError, error, runAsync } = useAsync()
const { runAsync: institutionRunAsync } = useAsync()
const {
state,
@ -65,6 +67,18 @@ function AddEmail() {
setUserEmailsContextLoading(isLoading)
}, [setUserEmailsContextLoading, isLoading])
useEffect(() => {
if (isFormVisible && emailRef.current) {
emailRef.current?.focus()
}
}, [emailRef, isFormVisible])
useEffect(() => {
if (isInstitutionFieldsVisible && countryRef.current) {
countryRef.current?.focus()
}
}, [countryRef, isInstitutionFieldsVisible])
useEffect(() => {
if (university) {
setIsUniversityDirty(true)
@ -194,7 +208,7 @@ function AddEmail() {
<label htmlFor="affiliations-email" className="sr-only">
{t('email')}
</label>
<AddEmailInput onChange={handleEmailChange} />
<AddEmailInput onChange={handleEmailChange} ref={emailRef} />
</Cell>
</Col>
@ -211,7 +225,7 @@ function AddEmail() {
{!ssoAvailable && (
<>
<Col md={4}>
<Col md={5}>
<Cell>
{isInstitutionFieldsVisible ? (
<>
@ -219,6 +233,7 @@ function AddEmail() {
<CountryInput
id="new-email-country-input"
setValue={setCountryCode}
ref={countryRef}
/>
</div>
<div className="form-group mb-2">
@ -269,7 +284,7 @@ function AddEmail() {
</Cell>
</Col>
<Col md={4}>
<Col md={3}>
<Cell className="text-md-right">
<Button
bsSize="small"
@ -281,12 +296,6 @@ function AddEmail() {
>
{t('add_new_email')}
</Button>
{isError && (
<div className="text-danger small">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</div>
)}
</Cell>
</Col>
</>
@ -294,6 +303,11 @@ function AddEmail() {
</form>
)}
</Row>
{isError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw /> {error.getUserFacingMessage()}
</Alert>
)}
</div>
)
}

View file

@ -1,5 +1,5 @@
import { useState, forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useCombobox } from 'downshift'
import classnames from 'classnames'
import { defaults as countries } from '../../countries-list'
@ -7,11 +7,12 @@ import { CountryCode } from '../../../../../../types/country'
type CountryInputProps = {
setValue: React.Dispatch<React.SetStateAction<CountryCode | null>>
inputRef?: React.ForwardedRef<HTMLInputElement>
} & React.InputHTMLAttributes<HTMLInputElement>
const itemToString = (item: typeof countries[number] | null) => item?.name ?? ''
function CountryInput({ setValue }: CountryInputProps) {
function Downshift({ setValue, inputRef }: CountryInputProps) {
const { t } = useTranslation()
const [inputItems, setInputItems] = useState(() => countries)
const [inputValue, setInputValue] = useState('')
@ -23,6 +24,7 @@ function CountryInput({ setValue }: CountryInputProps) {
getInputProps,
getComboboxProps,
getItemProps,
highlightedIndex,
openMenu,
selectedItem,
} = useCombobox({
@ -66,6 +68,7 @@ function CountryInput({ setValue }: CountryInputProps) {
openMenu()
}
},
ref: inputRef,
})}
className="form-control"
type="text"
@ -86,6 +89,8 @@ function CountryInput({ setValue }: CountryInputProps) {
<div
className={classnames('ui-select-choices-row', {
active: selectedItem?.name === item.name,
'ui-select-choices-row--highlighted':
highlightedIndex === index,
})}
>
<span className="ui-select-choices-row-inner">
@ -99,4 +104,11 @@ function CountryInput({ setValue }: CountryInputProps) {
)
}
const CountryInput = forwardRef<
HTMLInputElement,
Omit<CountryInputProps, 'inputRef'>
>((props, ref) => <Downshift {...props} inputRef={ref} />)
CountryInput.displayName = 'CountryInput'
export default CountryInput

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, forwardRef } from 'react'
import { useCombobox } from 'downshift'
import classnames from 'classnames'
@ -7,6 +7,7 @@ type DownshiftInputProps = {
inputValue: string
label: string
setValue: React.Dispatch<React.SetStateAction<string>>
inputRef?: React.ForwardedRef<HTMLInputElement>
} & React.InputHTMLAttributes<HTMLInputElement>
const filterItemsByInputValue = (
@ -14,13 +15,14 @@ const filterItemsByInputValue = (
inputValue: DownshiftInputProps['inputValue']
) => items.filter(item => item.toLowerCase().includes(inputValue.toLowerCase()))
function DownshiftInput({
function Downshift({
items,
inputValue,
placeholder,
label,
setValue,
disabled,
inputRef,
}: DownshiftInputProps) {
const [inputItems, setInputItems] = useState(items)
@ -35,6 +37,7 @@ function DownshiftInput({
getInputProps,
getComboboxProps,
getItemProps,
highlightedIndex,
openMenu,
selectedItem,
} = useCombobox({
@ -78,6 +81,7 @@ function DownshiftInput({
openMenu()
}
},
ref: inputRef,
})}
className="form-control"
type="text"
@ -98,6 +102,8 @@ function DownshiftInput({
<div
className={classnames('ui-select-choices-row', {
active: selectedItem === item,
'ui-select-choices-row--highlighted':
highlightedIndex === index,
})}
>
<span className="ui-select-choices-row-inner">
@ -111,4 +117,11 @@ function DownshiftInput({
)
}
const DownshiftInput = forwardRef<
HTMLInputElement,
Omit<DownshiftInputProps, 'inputRef'>
>((props, ref) => <Downshift {...props} inputRef={ref} />)
DownshiftInput.displayName = 'DownshiftInput'
export default DownshiftInput

View file

@ -1,11 +1,10 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../types/user-email'
import { Button } from 'react-bootstrap'
import { isChangingAffiliation } from '../../utils/selectors'
import { useUserEmailsContext } from '../../context/user-email-context'
import DownshiftInput from './downshift-input'
import Icon from '../../../../shared/components/icon'
import useAsync from '../../../../shared/hooks/use-async'
import { getJSON, postJSON } from '../../../../infrastructure/fetch-json'
import { defaults as defaultRoles } from '../../roles'
@ -30,11 +29,22 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
const [role, setRole] = useState(affiliation?.role || '')
const [department, setDepartment] = useState(affiliation?.department || '')
const [departments, setDepartments] = useState(defaultDepartments)
const roleRef = useRef<HTMLInputElement | null>(null)
const isChangingAffiliationInProgress = isChangingAffiliation(
state,
userEmailData.email
)
useEffect(() => {
setUserEmailsContextLoading(isLoading)
}, [setUserEmailsContextLoading, isLoading])
useEffect(() => {
if (isChangingAffiliationInProgress && roleRef.current) {
roleRef.current?.focus()
}
}, [roleRef, isChangingAffiliationInProgress])
const handleChangeAffiliation = () => {
setEmailAffiliationBeingEdited(userEmailData.email)
@ -87,7 +97,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
return (
<>
<div>{affiliation.institution.name}</div>
{!isChangingAffiliation(state, userEmailData.email) ? (
{!isChangingAffiliationInProgress ? (
<div className="small">
{(affiliation.role || affiliation.department) && (
<>
@ -112,6 +122,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
placeholder={t('role')}
label={t('role')}
setValue={setRole}
ref={roleRef}
/>
<DownshiftInput
items={departments}
@ -130,7 +141,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
</Button>
{!isLoading && (
<>
<span className="mx-2">{t('save_or_cancel-or')}</span>
<span className="mx-1">{t('save_or_cancel-or')}</span>
<Button
className="btn-inline-link"
onClick={handleCancelAffiliationChange}
@ -143,10 +154,9 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
</div>
)}
{isError && (
<span className="text-danger small">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</span>
<div className="text-danger small">
{t('generic_something_went_wrong')}
</div>
)}
</>
)

View file

@ -53,10 +53,7 @@ function ResendConfirmationEmailButton({
</Button>
<br />
{isError && (
<span className="text-danger">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</span>
<div className="text-danger">{t('generic_something_went_wrong')}</div>
)}
</>
)

View file

@ -17,14 +17,14 @@ function EmailsRow({ userEmailData }: EmailsRowProps) {
<Email userEmailData={userEmailData} />
</EmailCell>
</Col>
<Col md={4}>
<Col md={5}>
{userEmailData.affiliation?.institution && (
<EmailCell>
<InstitutionAndRole userEmailData={userEmailData} />
</EmailCell>
)}
</Col>
<Col md={4}>
<Col md={3}>
<EmailCell className="text-md-right">
<Actions userEmailData={userEmailData} />
</EmailCell>

View file

@ -196,7 +196,7 @@ const reducer = (state: State, action: Action) => {
function useUserEmails() {
const [state, unsafeDispatch] = useReducer(reducer, initialState)
const dispatch = useSafeDispatch(unsafeDispatch)
const { data, isLoading, isError, runAsync } = useAsync()
const { data, isLoading, isError, isSuccess, runAsync } = useAsync()
const getEmails = useCallback(() => {
runAsync<UserEmailData[]>(getJSON('/user/emails?ensureAffiliation=true'))
@ -214,6 +214,7 @@ function useUserEmails() {
return {
state,
isInitializing: isLoading && !data,
isInitializingSuccess: isSuccess,
isInitializingError: isError,
getEmails,
setLoading: useCallback(

View file

@ -26,7 +26,8 @@
}
.ui-select-choices-row:hover {
.ui-select-choices-row:hover,
.ui-select-choices-row--highlighted {
background-color: #f5f5f5;
}
@ -34,19 +35,19 @@
/* Mark invalid Select2 */
.ng-dirty.ng-invalid > a.select2-choice {
border-color: #D44950;
border-color: #D44950;
}
.select2-result-single {
padding-left: 0;
}
.select2-locked > .select2-search-choice-close{
display:none;
.select2-locked > .select2-search-choice-close {
display: none;
}
.select-locked > .ui-select-match-close{
display:none;
.select-locked > .ui-select-match-close {
display: none;
}
body > .select2-container.open {
@ -56,46 +57,49 @@ body > .select2-container.open {
/* Handle up direction Select2 */
.ui-select-container[theme="select2"].direction-up .ui-select-match,
.ui-select-container.select2.direction-up .ui-select-match {
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-top-left-radius: 0;
border-top-right-radius: 0;
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown,
.ui-select-container.select2.direction-up .ui-select-dropdown {
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-width: 1px; /* FIXME hardcoded value :-/ */
border-top-style: solid;
border-top-width: 1px; /* FIXME hardcoded value :-/ */
border-top-style: solid;
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
margin-top: -4px; /* FIXME hardcoded value :-/ */
margin-top: -4px; /* FIXME hardcoded value :-/ */
}
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown .select2-search,
.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search {
margin-top: 4px; /* FIXME hardcoded value :-/ */
margin-top: 4px; /* FIXME hardcoded value :-/ */
}
.ui-select-container[theme="select2"].direction-up.select2-dropdown-open .ui-select-match,
.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match {
border-bottom-color: #5897fb;
border-bottom-color: #5897fb;
}
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden,
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input{
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border:0;
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input {
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border: 0;
}
/* Selectize theme */
/* Helper class to show styles when focus */
.selectize-input.selectize-focus{
.selectize-input.selectize-focus {
border-color: #007FBB !important;
}
@ -116,23 +120,23 @@ body > .select2-container.open {
/* Mark invalid Selectize */
.ng-dirty.ng-invalid > div.selectize-input {
border-color: #D44950;
border-color: #D44950;
}
/* Handle up direction Selectize */
.ui-select-container[theme="selectize"].direction-up .ui-select-dropdown {
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
margin-top: -2px; /* FIXME hardcoded value :-/ */
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
margin-top: -2px; /* FIXME hardcoded value :-/ */
}
.ui-select-container[theme="selectize"] input.ui-select-search-hidden{
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border:0;
width: 0;
.ui-select-container[theme="selectize"] input.ui-select-search-hidden {
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border: 0;
width: 0;
}
/* Bootstrap theme */
@ -171,22 +175,23 @@ body > .select2-container.open {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up {
border-radius: 4px !important; /* FIXME hardcoded value :-/ */
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.ui-select-bootstrap .ui-select-search-hidden{
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border:0;
.ui-select-bootstrap .ui-select-search-hidden {
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border: 0;
}
.ui-select-bootstrap > .ui-select-match > .btn{
.ui-select-bootstrap > .ui-select-match > .btn {
/* Instead of center because of .btn */
text-align: left !important;
}
@ -198,7 +203,7 @@ body > .select2-container.open {
}
/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */
.ui-select-bootstrap > .ui-select-choices ,.ui-select-bootstrap > .ui-select-no-choice {
.ui-select-bootstrap > .ui-select-choices, .ui-select-bootstrap > .ui-select-no-choice {
width: 100%;
height: auto;
max-height: 200px;
@ -261,62 +266,64 @@ body > .ui-select-bootstrap.open {
border-right: 1px solid #428bca;
}
.ui-select-bootstrap .ui-select-choices-row>span {
cursor: pointer;
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
.ui-select-bootstrap .ui-select-choices-row > span {
cursor: pointer;
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus {
text-decoration: none;
color: #262626;
background-color: #f5f5f5;
.ui-select-bootstrap .ui-select-choices-row > span:hover, .ui-select-bootstrap .ui-select-choices-row > span:focus {
text-decoration: none;
color: #262626;
background-color: #f5f5f5;
}
.ui-select-bootstrap .ui-select-choices-row.active>span {
color: #fff;
text-decoration: none;
outline: 0;
background-color: #428bca;
.ui-select-bootstrap .ui-select-choices-row.active > span {
color: #fff;
text-decoration: none;
outline: 0;
background-color: #428bca;
}
.ui-select-bootstrap .ui-select-choices-row.disabled>span,
.ui-select-bootstrap .ui-select-choices-row.active.disabled>span {
color: #777;
cursor: not-allowed;
background-color: #fff;
.ui-select-bootstrap .ui-select-choices-row.disabled > span,
.ui-select-bootstrap .ui-select-choices-row.active.disabled > span {
color: #777;
cursor: not-allowed;
background-color: #fff;
}
/* fix hide/show angular animation */
.ui-select-match.ng-hide-add,
.ui-select-search.ng-hide-add {
display: none !important;
display: none !important;
}
/* Mark invalid Bootstrap */
.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {
border-color: #D44950;
border-color: #D44950;
}
/* Handle up direction Bootstrap */
.ui-select-container[theme="bootstrap"].direction-up .ui-select-dropdown {
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
}
.ui-select-bootstrap .ui-select-match-text {
width: 100%;
padding-right: 1em;
width: 100%;
padding-right: 1em;
}
.ui-select-bootstrap .ui-select-match-text span {
display: inline-block;
width: 100%;
overflow: hidden;
display: inline-block;
width: 100%;
overflow: hidden;
}
.ui-select-bootstrap .ui-select-toggle > a.btn {
position: absolute;
height: 10px;
@ -326,10 +333,10 @@ body > .ui-select-bootstrap.open {
/* Spinner */
.ui-select-refreshing.glyphicon {
position: absolute;
right: 0;
padding: 8px 27px;
}
position: absolute;
right: 0;
padding: 8px 27px;
}
@-webkit-keyframes ui-select-spin {
0% {
@ -341,6 +348,7 @@ body > .ui-select-bootstrap.open {
transform: rotate(359deg);
}
}
@keyframes ui-select-spin {
0% {
-webkit-transform: rotate(0deg);

View file

@ -65,7 +65,7 @@ describe('email actions - make primary', function () {
screen.getByRole('button', { name: /sending/i })
)
screen.getByText(/an error has occurred while performing your request/i)
screen.getByText(/sorry, something went wrong/i)
screen.getByRole('button', { name: /make primary/i })
})
})
@ -114,7 +114,7 @@ describe('email actions - delete', function () {
screen.getByRole('button', { name: /deleting/i })
)
screen.getByText(/an error has occurred while performing your request/i)
screen.getByText(/sorry, something went wrong/i)
screen.getByRole('button', { name: /remove/i })
})
})

View file

@ -60,6 +60,7 @@ describe('<EmailsSection />', function () {
hasSamlFeature: true,
samlInitPath: 'saml/init',
})
fetchMock.reset()
})
afterEach(function () {
@ -67,10 +68,12 @@ describe('<EmailsSection />', function () {
resetFetchMock()
})
it('renders "add another email" button', function () {
it('renders "add another email" button', async function () {
fetchMock.get('/user/emails?ensureAffiliation=true', [])
render(<EmailsSection />)
await fetchMock.flush(true)
screen.getByRole('button', { name: /add another email/i })
})
@ -146,7 +149,7 @@ describe('<EmailsSection />', function () {
resetFetchMock()
fetchMock
.get('/user/emails?ensureAffiliation=true', [])
.post('/user/emails', 500)
.post('/user/emails', 400)
const addAnotherEmailBtn = screen.getByRole('button', {
name: /add another email/i,
@ -170,18 +173,20 @@ describe('<EmailsSection />', function () {
expect(submitBtn.disabled).to.be.true
await screen.findByText(
/an error has occurred while performing your request/i
/Invalid Request. Please correct the data and try again./i
)
expect(submitBtn).to.not.be.null
expect(submitBtn.disabled).to.be.false
})
it('can link email address to an existing SSO institution', async function () {
fetchMock.reset()
fetchMock.get('/user/emails?ensureAffiliation=true', [])
fetchMock.get('express:/institutions/domains', institutionDomainData)
render(<EmailsSection />)
await fetchMock.flush(true)
fetchMock.reset()
fetchMock.get('express:/institutions/domains', institutionDomainData)
await userEvent.click(
screen.getByRole('button', {
name: /add another email/i,

View file

@ -198,7 +198,7 @@ describe('<EmailsSection />', function () {
await waitForElementToBeRemoved(() => screen.getByText(/sending/i))
screen.getByText(/an error has occurred while performing your request/i)
screen.getByText(/sorry, something went wrong/i)
screen.getByRole('button', { name: /resend confirmation email/i })
})
})