فهرست منبع

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

[SettingsPage] UI Fixes 1

GitOrigin-RevId: 0e0f605191218af4db70a801ff1e50b47f6e0b01
ilkin-overleaf 4 سال پیش
والد
کامیت
1def576973

+ 8 - 6
services/web/frontend/js/features/settings/components/emails-section.tsx

@@ -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 />
     </>
   )
 }

+ 1 - 3
services/web/frontend/js/features/settings/components/emails/actions.tsx

@@ -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>
       )}
     </>

+ 32 - 6
services/web/frontend/js/features/settings/components/emails/add-email-input.tsx

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

+ 26 - 12
services/web/frontend/js/features/settings/components/emails/add-email.tsx

@@ -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>
   )
 }

+ 14 - 2
services/web/frontend/js/features/settings/components/emails/country-input.tsx

@@ -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

+ 15 - 2
services/web/frontend/js/features/settings/components/emails/downshift-input.tsx

@@ -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

+ 18 - 8
services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx

@@ -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>
       )}
     </>
   )

+ 1 - 4
services/web/frontend/js/features/settings/components/emails/resend-confirmation-email-button.tsx

@@ -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>
       )}
     </>
   )

+ 2 - 2
services/web/frontend/js/features/settings/components/emails/row.tsx

@@ -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>

+ 2 - 1
services/web/frontend/js/features/settings/context/user-email-context.tsx

@@ -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(

+ 89 - 81
services/web/frontend/stylesheets/vendor/select/select.css

@@ -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);

+ 2 - 2
services/web/test/frontend/features/settings/components/emails/emails-section-actions.test.tsx

@@ -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 })
   })
 })

+ 10 - 5
services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx

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

+ 1 - 1
services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx

@@ -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 })
   })
 })