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

View file

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

View file

@ -4,6 +4,7 @@ import {
useCallback, useCallback,
useEffect, useEffect,
useState, useState,
forwardRef,
} from 'react' } from 'react'
import { getJSON } from '../../../../infrastructure/fetch-json' import { getJSON } from '../../../../infrastructure/fetch-json'
import useAbortController from '../../../../shared/hooks/use-abort-controller' import useAbortController from '../../../../shared/hooks/use-abort-controller'
@ -38,13 +39,14 @@ export function clearDomainCache() {
type AddEmailInputProps = { type AddEmailInputProps = {
onChange: (value: string, institution?: InstitutionInfo) => void onChange: (value: string, institution?: InstitutionInfo) => void
inputRef?: React.ForwardedRef<HTMLInputElement>
} }
export function AddEmailInput({ onChange }: AddEmailInputProps) { function AddEmailInputBase({ onChange, inputRef }: AddEmailInputProps) {
const { signal } = useAbortController() const { signal } = useAbortController()
const [suggestion, setSuggestion] = useState<string>(null) const [suggestion, setSuggestion] = useState<string | null>(null)
const [inputValue, setInputValue] = useState<string>(null) const [inputValue, setInputValue] = useState<string | null>(null)
const [matchedInstitution, setMatchedInstitution] = const [matchedInstitution, setMatchedInstitution] =
useState<InstitutionInfo>(null) useState<InstitutionInfo>(null)
@ -52,7 +54,10 @@ export function AddEmailInput({ onChange }: AddEmailInputProps) {
if (inputValue == null) { if (inputValue == null) {
return return
} }
if (matchedInstitution && suggestion === inputValue) { if (
matchedInstitution &&
inputValue.endsWith(matchedInstitution.hostname)
) {
onChange(inputValue, matchedInstitution) onChange(inputValue, matchedInstitution)
} else { } else {
onChange(inputValue) onChange(inputValue)
@ -103,12 +108,23 @@ export function AddEmailInput({ onChange }: AddEmailInputProps) {
const handleKeyDownEvent = useCallback( const handleKeyDownEvent = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if (event.key === 'Tab' || event.key === 'Enter') { const setInputValueAndResetSuggestion = () => {
setInputValue(suggestion)
setSuggestion(null)
}
if (event.key === 'Enter') {
event.preventDefault() event.preventDefault()
if (suggestion) { if (suggestion) {
setInputValue(suggestion) setInputValueAndResetSuggestion()
} }
} }
if (event.key === 'Tab' && suggestion) {
event.preventDefault()
setInputValueAndResetSuggestion()
}
}, },
[suggestion] [suggestion]
) )
@ -129,7 +145,17 @@ export function AddEmailInput({ onChange }: AddEmailInputProps) {
onKeyDown={handleKeyDownEvent} onKeyDown={handleKeyDownEvent}
value={inputValue || ''} value={inputValue || ''}
placeholder="e.g. johndoe@mit.edu" placeholder="e.g. johndoe@mit.edu"
ref={inputRef}
/> />
</div> </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 { useTranslation } from 'react-i18next'
import { Button, Row, Col } from 'react-bootstrap' import { Button, Row, Col, Alert } from 'react-bootstrap'
import Cell from './cell' import Cell from './cell'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import DownshiftInput from './downshift-input' import DownshiftInput from './downshift-input'
@ -39,6 +39,8 @@ function AddEmail() {
const [isFormVisible, setIsFormVisible] = useState( const [isFormVisible, setIsFormVisible] = useState(
() => window.location.hash === '#add-email' () => window.location.hash === '#add-email'
) )
const emailRef = useRef<HTMLInputElement | null>(null)
const countryRef = useRef<HTMLInputElement | null>(null)
const [newEmail, setNewEmail] = useState('') const [newEmail, setNewEmail] = useState('')
const [newEmailMatchedInstitution, setNewEmailMatchedInstitution] = const [newEmailMatchedInstitution, setNewEmailMatchedInstitution] =
useState<InstitutionInfo | null>(null) useState<InstitutionInfo | null>(null)
@ -53,7 +55,7 @@ function AddEmail() {
const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] = const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] =
useState(false) useState(false)
const [isUniversityDirty, setIsUniversityDirty] = useState(false) const [isUniversityDirty, setIsUniversityDirty] = useState(false)
const { isLoading, isError, runAsync } = useAsync() const { isLoading, isError, error, runAsync } = useAsync()
const { runAsync: institutionRunAsync } = useAsync() const { runAsync: institutionRunAsync } = useAsync()
const { const {
state, state,
@ -65,6 +67,18 @@ function AddEmail() {
setUserEmailsContextLoading(isLoading) setUserEmailsContextLoading(isLoading)
}, [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(() => { useEffect(() => {
if (university) { if (university) {
setIsUniversityDirty(true) setIsUniversityDirty(true)
@ -194,7 +208,7 @@ function AddEmail() {
<label htmlFor="affiliations-email" className="sr-only"> <label htmlFor="affiliations-email" className="sr-only">
{t('email')} {t('email')}
</label> </label>
<AddEmailInput onChange={handleEmailChange} /> <AddEmailInput onChange={handleEmailChange} ref={emailRef} />
</Cell> </Cell>
</Col> </Col>
@ -211,7 +225,7 @@ function AddEmail() {
{!ssoAvailable && ( {!ssoAvailable && (
<> <>
<Col md={4}> <Col md={5}>
<Cell> <Cell>
{isInstitutionFieldsVisible ? ( {isInstitutionFieldsVisible ? (
<> <>
@ -219,6 +233,7 @@ function AddEmail() {
<CountryInput <CountryInput
id="new-email-country-input" id="new-email-country-input"
setValue={setCountryCode} setValue={setCountryCode}
ref={countryRef}
/> />
</div> </div>
<div className="form-group mb-2"> <div className="form-group mb-2">
@ -269,7 +284,7 @@ function AddEmail() {
</Cell> </Cell>
</Col> </Col>
<Col md={4}> <Col md={3}>
<Cell className="text-md-right"> <Cell className="text-md-right">
<Button <Button
bsSize="small" bsSize="small"
@ -281,12 +296,6 @@ function AddEmail() {
> >
{t('add_new_email')} {t('add_new_email')}
</Button> </Button>
{isError && (
<div className="text-danger small">
<Icon type="exclamation-triangle" fw />{' '}
{t('error_performing_request')}
</div>
)}
</Cell> </Cell>
</Col> </Col>
</> </>
@ -294,6 +303,11 @@ function AddEmail() {
</form> </form>
)} )}
</Row> </Row>
{isError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw /> {error.getUserFacingMessage()}
</Alert>
)}
</div> </div>
) )
} }

View file

@ -1,5 +1,5 @@
import { useState, forwardRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useCombobox } from 'downshift' import { useCombobox } from 'downshift'
import classnames from 'classnames' import classnames from 'classnames'
import { defaults as countries } from '../../countries-list' import { defaults as countries } from '../../countries-list'
@ -7,11 +7,12 @@ import { CountryCode } from '../../../../../../types/country'
type CountryInputProps = { type CountryInputProps = {
setValue: React.Dispatch<React.SetStateAction<CountryCode | null>> setValue: React.Dispatch<React.SetStateAction<CountryCode | null>>
inputRef?: React.ForwardedRef<HTMLInputElement>
} & React.InputHTMLAttributes<HTMLInputElement> } & React.InputHTMLAttributes<HTMLInputElement>
const itemToString = (item: typeof countries[number] | null) => item?.name ?? '' const itemToString = (item: typeof countries[number] | null) => item?.name ?? ''
function CountryInput({ setValue }: CountryInputProps) { function Downshift({ setValue, inputRef }: CountryInputProps) {
const { t } = useTranslation() const { t } = useTranslation()
const [inputItems, setInputItems] = useState(() => countries) const [inputItems, setInputItems] = useState(() => countries)
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('')
@ -23,6 +24,7 @@ function CountryInput({ setValue }: CountryInputProps) {
getInputProps, getInputProps,
getComboboxProps, getComboboxProps,
getItemProps, getItemProps,
highlightedIndex,
openMenu, openMenu,
selectedItem, selectedItem,
} = useCombobox({ } = useCombobox({
@ -66,6 +68,7 @@ function CountryInput({ setValue }: CountryInputProps) {
openMenu() openMenu()
} }
}, },
ref: inputRef,
})} })}
className="form-control" className="form-control"
type="text" type="text"
@ -86,6 +89,8 @@ function CountryInput({ setValue }: CountryInputProps) {
<div <div
className={classnames('ui-select-choices-row', { className={classnames('ui-select-choices-row', {
active: selectedItem?.name === item.name, active: selectedItem?.name === item.name,
'ui-select-choices-row--highlighted':
highlightedIndex === index,
})} })}
> >
<span className="ui-select-choices-row-inner"> <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 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 { useCombobox } from 'downshift'
import classnames from 'classnames' import classnames from 'classnames'
@ -7,6 +7,7 @@ type DownshiftInputProps = {
inputValue: string inputValue: string
label: string label: string
setValue: React.Dispatch<React.SetStateAction<string>> setValue: React.Dispatch<React.SetStateAction<string>>
inputRef?: React.ForwardedRef<HTMLInputElement>
} & React.InputHTMLAttributes<HTMLInputElement> } & React.InputHTMLAttributes<HTMLInputElement>
const filterItemsByInputValue = ( const filterItemsByInputValue = (
@ -14,13 +15,14 @@ const filterItemsByInputValue = (
inputValue: DownshiftInputProps['inputValue'] inputValue: DownshiftInputProps['inputValue']
) => items.filter(item => item.toLowerCase().includes(inputValue.toLowerCase())) ) => items.filter(item => item.toLowerCase().includes(inputValue.toLowerCase()))
function DownshiftInput({ function Downshift({
items, items,
inputValue, inputValue,
placeholder, placeholder,
label, label,
setValue, setValue,
disabled, disabled,
inputRef,
}: DownshiftInputProps) { }: DownshiftInputProps) {
const [inputItems, setInputItems] = useState(items) const [inputItems, setInputItems] = useState(items)
@ -35,6 +37,7 @@ function DownshiftInput({
getInputProps, getInputProps,
getComboboxProps, getComboboxProps,
getItemProps, getItemProps,
highlightedIndex,
openMenu, openMenu,
selectedItem, selectedItem,
} = useCombobox({ } = useCombobox({
@ -78,6 +81,7 @@ function DownshiftInput({
openMenu() openMenu()
} }
}, },
ref: inputRef,
})} })}
className="form-control" className="form-control"
type="text" type="text"
@ -98,6 +102,8 @@ function DownshiftInput({
<div <div
className={classnames('ui-select-choices-row', { className={classnames('ui-select-choices-row', {
active: selectedItem === item, active: selectedItem === item,
'ui-select-choices-row--highlighted':
highlightedIndex === index,
})} })}
> >
<span className="ui-select-choices-row-inner"> <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 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 { useTranslation } from 'react-i18next'
import { UserEmailData } from '../../../../../../types/user-email' import { UserEmailData } from '../../../../../../types/user-email'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import { isChangingAffiliation } from '../../utils/selectors' import { isChangingAffiliation } from '../../utils/selectors'
import { useUserEmailsContext } from '../../context/user-email-context' import { useUserEmailsContext } from '../../context/user-email-context'
import DownshiftInput from './downshift-input' import DownshiftInput from './downshift-input'
import Icon from '../../../../shared/components/icon'
import useAsync from '../../../../shared/hooks/use-async' import useAsync from '../../../../shared/hooks/use-async'
import { getJSON, postJSON } from '../../../../infrastructure/fetch-json' import { getJSON, postJSON } from '../../../../infrastructure/fetch-json'
import { defaults as defaultRoles } from '../../roles' import { defaults as defaultRoles } from '../../roles'
@ -30,11 +29,22 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
const [role, setRole] = useState(affiliation?.role || '') const [role, setRole] = useState(affiliation?.role || '')
const [department, setDepartment] = useState(affiliation?.department || '') const [department, setDepartment] = useState(affiliation?.department || '')
const [departments, setDepartments] = useState(defaultDepartments) const [departments, setDepartments] = useState(defaultDepartments)
const roleRef = useRef<HTMLInputElement | null>(null)
const isChangingAffiliationInProgress = isChangingAffiliation(
state,
userEmailData.email
)
useEffect(() => { useEffect(() => {
setUserEmailsContextLoading(isLoading) setUserEmailsContextLoading(isLoading)
}, [setUserEmailsContextLoading, isLoading]) }, [setUserEmailsContextLoading, isLoading])
useEffect(() => {
if (isChangingAffiliationInProgress && roleRef.current) {
roleRef.current?.focus()
}
}, [roleRef, isChangingAffiliationInProgress])
const handleChangeAffiliation = () => { const handleChangeAffiliation = () => {
setEmailAffiliationBeingEdited(userEmailData.email) setEmailAffiliationBeingEdited(userEmailData.email)
@ -87,7 +97,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
return ( return (
<> <>
<div>{affiliation.institution.name}</div> <div>{affiliation.institution.name}</div>
{!isChangingAffiliation(state, userEmailData.email) ? ( {!isChangingAffiliationInProgress ? (
<div className="small"> <div className="small">
{(affiliation.role || affiliation.department) && ( {(affiliation.role || affiliation.department) && (
<> <>
@ -112,6 +122,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
placeholder={t('role')} placeholder={t('role')}
label={t('role')} label={t('role')}
setValue={setRole} setValue={setRole}
ref={roleRef}
/> />
<DownshiftInput <DownshiftInput
items={departments} items={departments}
@ -130,7 +141,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
</Button> </Button>
{!isLoading && ( {!isLoading && (
<> <>
<span className="mx-2">{t('save_or_cancel-or')}</span> <span className="mx-1">{t('save_or_cancel-or')}</span>
<Button <Button
className="btn-inline-link" className="btn-inline-link"
onClick={handleCancelAffiliationChange} onClick={handleCancelAffiliationChange}
@ -143,10 +154,9 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
</div> </div>
)} )}
{isError && ( {isError && (
<span className="text-danger small"> <div className="text-danger small">
<Icon type="exclamation-triangle" fw />{' '} {t('generic_something_went_wrong')}
{t('error_performing_request')} </div>
</span>
)} )}
</> </>
) )

View file

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

View file

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

View file

@ -196,7 +196,7 @@ const reducer = (state: State, action: Action) => {
function useUserEmails() { function useUserEmails() {
const [state, unsafeDispatch] = useReducer(reducer, initialState) const [state, unsafeDispatch] = useReducer(reducer, initialState)
const dispatch = useSafeDispatch(unsafeDispatch) const dispatch = useSafeDispatch(unsafeDispatch)
const { data, isLoading, isError, runAsync } = useAsync() const { data, isLoading, isError, isSuccess, runAsync } = useAsync()
const getEmails = useCallback(() => { const getEmails = useCallback(() => {
runAsync<UserEmailData[]>(getJSON('/user/emails?ensureAffiliation=true')) runAsync<UserEmailData[]>(getJSON('/user/emails?ensureAffiliation=true'))
@ -214,6 +214,7 @@ function useUserEmails() {
return { return {
state, state,
isInitializing: isLoading && !data, isInitializing: isLoading && !data,
isInitializingSuccess: isSuccess,
isInitializingError: isError, isInitializingError: isError,
getEmails, getEmails,
setLoading: useCallback( 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; background-color: #f5f5f5;
} }
@ -34,19 +35,19 @@
/* Mark invalid Select2 */ /* Mark invalid Select2 */
.ng-dirty.ng-invalid > a.select2-choice { .ng-dirty.ng-invalid > a.select2-choice {
border-color: #D44950; border-color: #D44950;
} }
.select2-result-single { .select2-result-single {
padding-left: 0; padding-left: 0;
} }
.select2-locked > .select2-search-choice-close{ .select2-locked > .select2-search-choice-close {
display:none; display: none;
} }
.select-locked > .ui-select-match-close{ .select-locked > .ui-select-match-close {
display:none; display: none;
} }
body > .select2-container.open { body > .select2-container.open {
@ -56,46 +57,49 @@ body > .select2-container.open {
/* Handle up direction Select2 */ /* Handle up direction Select2 */
.ui-select-container[theme="select2"].direction-up .ui-select-match, .ui-select-container[theme="select2"].direction-up .ui-select-match,
.ui-select-container.select2.direction-up .ui-select-match { .ui-select-container.select2.direction-up .ui-select-match {
border-radius: 4px; /* FIXME hardcoded value :-/ */ border-radius: 4px; /* FIXME hardcoded value :-/ */
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown, .ui-select-container[theme="select2"].direction-up .ui-select-dropdown,
.ui-select-container.select2.direction-up .ui-select-dropdown { .ui-select-container.select2.direction-up .ui-select-dropdown {
border-radius: 4px; /* FIXME hardcoded value :-/ */ border-radius: 4px; /* FIXME hardcoded value :-/ */
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-top-width: 1px; /* FIXME hardcoded value :-/ */ border-top-width: 1px; /* FIXME hardcoded value :-/ */
border-top-style: solid; 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[theme="select2"].direction-up .ui-select-dropdown .select2-search,
.ui-select-container.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[theme="select2"].direction-up.select2-dropdown-open .ui-select-match,
.ui-select-container.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,
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input{ .ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input {
opacity: 0; opacity: 0;
height: 0; height: 0;
min-height: 0; min-height: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
border:0; border: 0;
} }
/* Selectize theme */ /* Selectize theme */
/* Helper class to show styles when focus */ /* Helper class to show styles when focus */
.selectize-input.selectize-focus{ .selectize-input.selectize-focus {
border-color: #007FBB !important; border-color: #007FBB !important;
} }
@ -116,23 +120,23 @@ body > .select2-container.open {
/* Mark invalid Selectize */ /* Mark invalid Selectize */
.ng-dirty.ng-invalid > div.selectize-input { .ng-dirty.ng-invalid > div.selectize-input {
border-color: #D44950; border-color: #D44950;
} }
/* Handle up direction Selectize */ /* Handle up direction Selectize */
.ui-select-container[theme="selectize"].direction-up .ui-select-dropdown { .ui-select-container[theme="selectize"].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);
margin-top: -2px; /* FIXME hardcoded value :-/ */ margin-top: -2px; /* FIXME hardcoded value :-/ */
} }
.ui-select-container[theme="selectize"] input.ui-select-search-hidden{ .ui-select-container[theme="selectize"] input.ui-select-search-hidden {
opacity: 0; opacity: 0;
height: 0; height: 0;
min-height: 0; min-height: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
border:0; border: 0;
width: 0; width: 0;
} }
/* Bootstrap theme */ /* Bootstrap theme */
@ -171,22 +175,23 @@ body > .select2-container.open {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up { .input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up {
border-radius: 4px !important; /* FIXME hardcoded value :-/ */ border-radius: 4px !important; /* FIXME hardcoded value :-/ */
border-top-right-radius: 0 !important; border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important; border-bottom-right-radius: 0 !important;
} }
.ui-select-bootstrap .ui-select-search-hidden{ .ui-select-bootstrap .ui-select-search-hidden {
opacity: 0; opacity: 0;
height: 0; height: 0;
min-height: 0; min-height: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
border:0; border: 0;
} }
.ui-select-bootstrap > .ui-select-match > .btn{ .ui-select-bootstrap > .ui-select-match > .btn {
/* Instead of center because of .btn */ /* Instead of center because of .btn */
text-align: left !important; text-align: left !important;
} }
@ -198,7 +203,7 @@ body > .select2-container.open {
} }
/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */ /* 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%; width: 100%;
height: auto; height: auto;
max-height: 200px; max-height: 200px;
@ -261,62 +266,64 @@ body > .ui-select-bootstrap.open {
border-right: 1px solid #428bca; border-right: 1px solid #428bca;
} }
.ui-select-bootstrap .ui-select-choices-row>span { .ui-select-bootstrap .ui-select-choices-row > span {
cursor: pointer; cursor: pointer;
display: block; display: block;
padding: 3px 20px; padding: 3px 20px;
clear: both; clear: both;
font-weight: 400; font-weight: 400;
line-height: 1.42857143; line-height: 1.42857143;
color: #333; color: #333;
white-space: nowrap; white-space: nowrap;
} }
.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus { .ui-select-bootstrap .ui-select-choices-row > span:hover, .ui-select-bootstrap .ui-select-choices-row > span:focus {
text-decoration: none; text-decoration: none;
color: #262626; color: #262626;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
.ui-select-bootstrap .ui-select-choices-row.active>span { .ui-select-bootstrap .ui-select-choices-row.active > span {
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
outline: 0; outline: 0;
background-color: #428bca; background-color: #428bca;
} }
.ui-select-bootstrap .ui-select-choices-row.disabled>span, .ui-select-bootstrap .ui-select-choices-row.disabled > span,
.ui-select-bootstrap .ui-select-choices-row.active.disabled>span { .ui-select-bootstrap .ui-select-choices-row.active.disabled > span {
color: #777; color: #777;
cursor: not-allowed; cursor: not-allowed;
background-color: #fff; background-color: #fff;
} }
/* fix hide/show angular animation */ /* fix hide/show angular animation */
.ui-select-match.ng-hide-add, .ui-select-match.ng-hide-add,
.ui-select-search.ng-hide-add { .ui-select-search.ng-hide-add {
display: none !important; display: none !important;
} }
/* Mark invalid Bootstrap */ /* Mark invalid Bootstrap */
.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match { .ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {
border-color: #D44950; border-color: #D44950;
} }
/* Handle up direction Bootstrap */ /* Handle up direction Bootstrap */
.ui-select-container[theme="bootstrap"].direction-up .ui-select-dropdown { .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 { .ui-select-bootstrap .ui-select-match-text {
width: 100%; width: 100%;
padding-right: 1em; padding-right: 1em;
} }
.ui-select-bootstrap .ui-select-match-text span { .ui-select-bootstrap .ui-select-match-text span {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
.ui-select-bootstrap .ui-select-toggle > a.btn { .ui-select-bootstrap .ui-select-toggle > a.btn {
position: absolute; position: absolute;
height: 10px; height: 10px;
@ -326,10 +333,10 @@ body > .ui-select-bootstrap.open {
/* Spinner */ /* Spinner */
.ui-select-refreshing.glyphicon { .ui-select-refreshing.glyphicon {
position: absolute; position: absolute;
right: 0; right: 0;
padding: 8px 27px; padding: 8px 27px;
} }
@-webkit-keyframes ui-select-spin { @-webkit-keyframes ui-select-spin {
0% { 0% {
@ -341,6 +348,7 @@ body > .ui-select-bootstrap.open {
transform: rotate(359deg); transform: rotate(359deg);
} }
} }
@keyframes ui-select-spin { @keyframes ui-select-spin {
0% { 0% {
-webkit-transform: rotate(0deg); -webkit-transform: rotate(0deg);

View file

@ -65,7 +65,7 @@ describe('email actions - make primary', function () {
screen.getByRole('button', { name: /sending/i }) 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 }) screen.getByRole('button', { name: /make primary/i })
}) })
}) })
@ -114,7 +114,7 @@ describe('email actions - delete', function () {
screen.getByRole('button', { name: /deleting/i }) 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 }) screen.getByRole('button', { name: /remove/i })
}) })
}) })

View file

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

View file

@ -198,7 +198,7 @@ describe('<EmailsSection />', function () {
await waitForElementToBeRemoved(() => screen.getByText(/sending/i)) 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 }) screen.getByRole('button', { name: /resend confirmation email/i })
}) })
}) })