Merge pull request #7841 from overleaf/ii-refactor-add-email

Refactor add email section

GitOrigin-RevId: 41de440caaf3baf43673c4a5f07a18b990f28c7b
This commit is contained in:
ilkin-overleaf 2022-05-11 12:19:10 +03:00 committed by Copybot
parent a147d045b8
commit c807bedb65
18 changed files with 377 additions and 525 deletions

View file

@ -1,46 +1,25 @@
import { useState, useEffect, useRef } from 'react'
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Row, Col, Alert } from 'react-bootstrap'
import { Col } from 'react-bootstrap'
import Cell from './cell'
import Icon from '../../../../shared/components/icon'
import DownshiftInput from './downshift-input'
import CountryInput from './country-input'
import { AddEmailInput, InstitutionInfo } from './add-email-input'
import Layout from './add-email/layout'
import Input, { InstitutionInfo } from './add-email/input'
import AddAnotherEmailBtn from './add-email/add-another-email-btn'
import InstitutionFields from './add-email/institution-fields'
import SsoLinkingInfo from './add-email/sso-linking-info'
import AddNewEmailBtn from './add-email/add-new-email-btn'
import useAsync from '../../../../shared/hooks/use-async'
import { useUserEmailsContext } from '../../context/user-email-context'
import { getJSON, postJSON } from '../../../../infrastructure/fetch-json'
import { defaults as defaultRoles } from '../../roles'
import { defaults as defaultDepartments } from '../../departments'
import { isSsoAvailable } from '../../utils/sso'
import { postJSON } from '../../../../infrastructure/fetch-json'
import { University } from '../../../../../../types/university'
import { CountryCode } from '../../../../../../types/country'
import { ExposedSettings } from '../../../../../../types/exposed-settings'
import getMeta from '../../../../utils/meta'
import { AddEmailSSOLinkingInfo } from './add-email-sso-linking-info'
const isValidEmail = (email: string) => {
return Boolean(email)
}
const ssoAvailableForDomain = (domain: InstitutionInfo | null) => {
const { hasSamlBeta, hasSamlFeature } = getMeta(
'ol-ExposedSettings'
) as ExposedSettings
if (!hasSamlFeature || !domain || !domain.confirmed || !domain.university) {
return false
}
if (domain.university.ssoEnabled) {
return true
}
return hasSamlBeta && domain.university.ssoBeta
}
import { CountryCode } from '../../data/countries-list'
function AddEmail() {
const { t } = useTranslation()
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)
@ -48,15 +27,10 @@ function AddEmail() {
const [universities, setUniversities] = useState<
Partial<Record<CountryCode, University[]>>
>({})
const [university, setUniversity] = useState('')
const [universityName, setUniversityName] = useState('')
const [role, setRole] = useState('')
const [department, setDepartment] = useState('')
const [departments, setDepartments] = useState(defaultDepartments)
const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] =
useState(false)
const [isUniversityDirty, setIsUniversityDirty] = useState(false)
const { isLoading, isError, error, runAsync } = useAsync()
const { runAsync: institutionRunAsync } = useAsync()
const {
state,
setLoading: setUserEmailsContextLoading,
@ -67,61 +41,10 @@ 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)
}
}, [setIsUniversityDirty, university])
useEffect(() => {
const selectedKnownUniversity = countryCode
? universities[countryCode]?.find(({ name }) => name === university)
: undefined
if (selectedKnownUniversity && selectedKnownUniversity.departments.length) {
setDepartments(selectedKnownUniversity.departments)
} else {
setDepartments(defaultDepartments)
}
}, [countryCode, universities, university])
// Fetch country institution
useEffect(() => {
// Skip if country not selected or universities for
// that country are already fetched
if (!countryCode || universities[countryCode]) {
return
}
institutionRunAsync<University[]>(
getJSON(`/institutions/list?country_code=${countryCode}`)
)
.then(data => {
setUniversities(state => ({ ...state, [countryCode]: data }))
})
.catch(() => {})
}, [countryCode, universities, setUniversities, institutionRunAsync])
const handleShowAddEmailForm = () => {
setIsFormVisible(true)
}
const handleShowInstitutionFields = () => {
setIsInstitutionFieldsVisible(true)
}
const handleEmailChange = (value: string, institution?: InstitutionInfo) => {
setNewEmail(value)
setNewEmailMatchedInstitution(institution || null)
@ -129,10 +52,10 @@ function AddEmail() {
const handleAddNewEmail = () => {
const selectedKnownUniversity = countryCode
? universities[countryCode]?.find(({ name }) => name === university)
? universities[countryCode]?.find(({ name }) => name === universityName)
: undefined
const knownUniversityData = university &&
const knownUniversityData = universityName &&
selectedKnownUniversity && {
university: {
id: selectedKnownUniversity.id,
@ -141,10 +64,10 @@ function AddEmail() {
department,
}
const unknownUniversityData = university &&
const unknownUniversityData = universityName &&
!selectedKnownUniversity && {
university: {
name: university,
name: universityName,
country_code: countryCode,
},
role,
@ -162,153 +85,73 @@ function AddEmail() {
)
.then(() => {
getEmails()
setIsFormVisible(false)
setNewEmail('')
setNewEmailMatchedInstitution(null)
setCountryCode(null)
setIsUniversityDirty(false)
setUniversity('')
setRole('')
setDepartment('')
setIsInstitutionFieldsVisible(false)
})
.catch(() => {})
}
const getUniversityItems = () => {
if (!countryCode) {
return []
}
return universities[countryCode]?.map(({ name }) => name) ?? []
if (!isFormVisible) {
return (
<Layout isError={isError} error={error}>
<Col md={4}>
<Cell>
<AddAnotherEmailBtn onClick={handleShowAddEmailForm} />
</Cell>
</Col>
</Layout>
)
}
const ssoAvailable =
newEmailMatchedInstitution &&
ssoAvailableForDomain(newEmailMatchedInstitution)
return (
<div className="affiliations-table-row--highlighted">
<Row>
{!isFormVisible ? (
<Col md={4}>
<Layout isError={isError} error={error}>
<form>
<Col md={4}>
<Cell>
<label htmlFor="affiliations-email" className="sr-only">
{t('email')}
</label>
<Input onChange={handleEmailChange} />
</Cell>
</Col>
{isSsoAvailable(newEmailMatchedInstitution) ? (
<Col md={8}>
<Cell>
<Button
className="btn-inline-link"
onClick={handleShowAddEmailForm}
>
{t('add_another_email')}
</Button>
<SsoLinkingInfo
email={newEmail}
institutionInfo={newEmailMatchedInstitution}
/>
</Cell>
</Col>
) : (
<form>
<Col md={4}>
<>
<Col md={5}>
<Cell>
<label htmlFor="affiliations-email" className="sr-only">
{t('email')}
</label>
<AddEmailInput onChange={handleEmailChange} ref={emailRef} />
<InstitutionFields
countryCode={countryCode}
setCountryCode={setCountryCode}
universities={universities}
setUniversities={setUniversities}
universityName={universityName}
setUniversityName={setUniversityName}
role={role}
setRole={setRole}
department={department}
setDepartment={setDepartment}
/>
</Cell>
</Col>
{ssoAvailable && (
<Col md={8}>
<Cell>
<AddEmailSSOLinkingInfo
email={newEmail}
institutionInfo={newEmailMatchedInstitution}
/>
</Cell>
</Col>
)}
{!ssoAvailable && (
<>
<Col md={5}>
<Cell>
{isInstitutionFieldsVisible ? (
<>
<div className="form-group mb-2">
<CountryInput
id="new-email-country-input"
setValue={setCountryCode}
ref={countryRef}
/>
</div>
<div className="form-group mb-2">
<DownshiftInput
items={getUniversityItems()}
inputValue={university}
placeholder={t('university')}
label={t('university')}
setValue={setUniversity}
disabled={!countryCode}
/>
</div>
{isUniversityDirty && (
<>
<div className="form-group mb-2">
<DownshiftInput
items={defaultRoles}
inputValue={role}
placeholder={t('role')}
label={t('role')}
setValue={setRole}
/>
</div>
<div className="form-group mb-0">
<DownshiftInput
items={departments}
inputValue={department}
placeholder={t('department')}
label={t('department')}
setValue={setDepartment}
/>
</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={3}>
<Cell className="text-md-right">
<Button
bsSize="small"
bsStyle="success"
disabled={
!isValidEmail(newEmail) || isLoading || state.isLoading
}
onClick={handleAddNewEmail}
>
{t('add_new_email')}
</Button>
</Cell>
</Col>
</>
)}
</form>
<Col md={3}>
<Cell className="text-md-right">
<AddNewEmailBtn
email={newEmail}
disabled={isLoading || state.isLoading}
onClick={handleAddNewEmail}
/>
</Cell>
</Col>
</>
)}
</Row>
{isError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw /> {error.getUserFacingMessage()}
</Alert>
)}
</div>
</form>
</Layout>
)
}

View file

@ -0,0 +1,14 @@
import { useTranslation } from 'react-i18next'
import { Button, ButtonProps } from 'react-bootstrap'
function AddAnotherEmailBtn({ onClick, ...props }: ButtonProps) {
const { t } = useTranslation()
return (
<Button className="btn-inline-link" onClick={onClick} {...props}>
{t('add_another_email')}
</Button>
)
}
export default AddAnotherEmailBtn

View file

@ -0,0 +1,27 @@
import { useTranslation } from 'react-i18next'
import { Button, ButtonProps } from 'react-bootstrap'
const isValidEmail = (email: string) => {
return Boolean(email)
}
type AddNewEmailColProps = {
email: string
} & ButtonProps
function AddNewEmailBtn({ email, disabled, ...props }: AddNewEmailColProps) {
const { t } = useTranslation()
return (
<Button
bsSize="small"
bsStyle="success"
disabled={disabled || !isValidEmail(email)}
{...props}
>
{t('add_new_email')}
</Button>
)
}
export default AddNewEmailBtn

View file

@ -2,8 +2,7 @@ import { useState, forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useCombobox } from 'downshift'
import classnames from 'classnames'
import { defaults as countries } from '../../countries-list'
import { CountryCode } from '../../../../../../types/country'
import countries, { CountryCode } from '../../../data/countries-list'
type CountryInputProps = {
setValue: React.Dispatch<React.SetStateAction<CountryCode | null>>
@ -14,7 +13,7 @@ const itemToString = (item: typeof countries[number] | null) => item?.name ?? ''
function Downshift({ setValue, inputRef }: CountryInputProps) {
const { t } = useTranslation()
const [inputItems, setInputItems] = useState(() => countries)
const [inputItems, setInputItems] = useState(() => [...countries])
const [inputValue, setInputValue] = useState('')
const {

View file

@ -0,0 +1,18 @@
import { useTranslation } from 'react-i18next'
import { Button, ButtonProps } from 'react-bootstrap'
function EmailAffiliatedWithInstitution({ onClick, ...props }: ButtonProps) {
const { t } = useTranslation()
return (
<div className="mt-1">
{t('is_email_affiliated')}
<br />
<Button className="btn-inline-link" onClick={onClick} {...props}>
{t('let_us_know')}
</Button>
</div>
)
}
export default EmailAffiliatedWithInstitution

View file

@ -4,10 +4,10 @@ import {
useCallback,
useEffect,
useState,
forwardRef,
useRef,
} from 'react'
import { getJSON } from '../../../../infrastructure/fetch-json'
import useAbortController from '../../../../shared/hooks/use-abort-controller'
import { getJSON } from '../../../../../infrastructure/fetch-json'
import useAbortController from '../../../../../shared/hooks/use-abort-controller'
const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
@ -37,18 +37,22 @@ export function clearDomainCache() {
domainCache = new Map<string, InstitutionInfo>()
}
type AddEmailInputProps = {
type InputProps = {
onChange: (value: string, institution?: InstitutionInfo) => void
inputRef?: React.ForwardedRef<HTMLInputElement>
}
function AddEmailInputBase({ onChange, inputRef }: AddEmailInputProps) {
function Input({ onChange }: InputProps) {
const { signal } = useAbortController()
const inputRef = useRef<HTMLInputElement | null>(null)
const [suggestion, setSuggestion] = useState<string | null>(null)
const [inputValue, setInputValue] = useState<string | null>(null)
const [matchedInstitution, setMatchedInstitution] =
useState<InstitutionInfo>(null)
useState<InstitutionInfo | null>(null)
useEffect(() => {
inputRef.current?.focus()
}, [inputRef])
useEffect(() => {
if (inputValue == null) {
@ -151,11 +155,4 @@ function AddEmailInputBase({ onChange, inputRef }: AddEmailInputProps) {
)
}
const AddEmailInput = forwardRef<
HTMLInputElement,
Omit<AddEmailInputProps, 'inputRef'>
>((props, ref) => <AddEmailInputBase {...props} inputRef={ref} />)
AddEmailInput.displayName = 'AddEmailInput'
export { AddEmailInput }
export default Input

View file

@ -0,0 +1,154 @@
import { useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import CountryInput from './country-input'
import DownshiftInput from '../downshift-input'
import EmailAffiliatedWithInstitution from './email-affiliated-with-institution'
import defaultRoles from '../../../data/roles'
import defaultDepartments from '../../../data/departments'
import { CountryCode } from '../../../data/countries-list'
import { University } from '../../../../../../../types/university'
import { getJSON } from '../../../../../infrastructure/fetch-json'
import useAsync from '../../../../../shared/hooks/use-async'
type InstitutionFieldsProps = {
countryCode: CountryCode | null
setCountryCode: React.Dispatch<React.SetStateAction<CountryCode | null>>
universities: Partial<Record<CountryCode, University[]>>
setUniversities: React.Dispatch<
React.SetStateAction<Partial<Record<CountryCode, University[]>>>
>
universityName: string
setUniversityName: React.Dispatch<React.SetStateAction<string>>
role: string
setRole: React.Dispatch<React.SetStateAction<string>>
department: string
setDepartment: React.Dispatch<React.SetStateAction<string>>
}
function InstitutionFields({
countryCode,
setCountryCode,
universities,
setUniversities,
universityName,
setUniversityName,
role,
setRole,
department,
setDepartment,
}: InstitutionFieldsProps) {
const { t } = useTranslation()
const countryRef = useRef<HTMLInputElement | null>(null)
const [departments, setDepartments] = useState<string[]>([
...defaultDepartments,
])
const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] =
useState(false)
const [isUniversityDirty, setIsUniversityDirty] = useState(false)
const { runAsync: institutionRunAsync } = useAsync()
useEffect(() => {
if (isInstitutionFieldsVisible && countryRef.current) {
countryRef.current?.focus()
}
}, [countryRef, isInstitutionFieldsVisible])
useEffect(() => {
if (universityName) {
setIsUniversityDirty(true)
}
}, [setIsUniversityDirty, universityName])
useEffect(() => {
const selectedKnownUniversity = countryCode
? universities[countryCode]?.find(({ name }) => name === universityName)
: undefined
if (selectedKnownUniversity && selectedKnownUniversity.departments.length) {
setDepartments(selectedKnownUniversity.departments)
} else {
setDepartments([...defaultDepartments])
}
}, [countryCode, universities, universityName])
// Fetch country institution
useEffect(() => {
// Skip if country not selected or universities for
// that country are already fetched
if (!countryCode || universities[countryCode]) {
return
}
institutionRunAsync<University[]>(
getJSON(`/institutions/list?country_code=${countryCode}`)
)
.then(data => {
setUniversities(state => ({ ...state, [countryCode]: data }))
})
.catch(() => {})
}, [countryCode, universities, setUniversities, institutionRunAsync])
const getUniversityItems = () => {
if (!countryCode) {
return []
}
return universities[countryCode]?.map(({ name }) => name) ?? []
}
const handleShowInstitutionFields = () => {
setIsInstitutionFieldsVisible(true)
}
if (!isInstitutionFieldsVisible) {
return (
<EmailAffiliatedWithInstitution onClick={handleShowInstitutionFields} />
)
}
return (
<>
<div className="form-group mb-2">
<CountryInput
id="new-email-country-input"
setValue={setCountryCode}
ref={countryRef}
/>
</div>
<div className="form-group mb-2">
<DownshiftInput
items={getUniversityItems()}
inputValue={universityName}
placeholder={t('university')}
label={t('university')}
setValue={setUniversityName}
disabled={!countryCode}
/>
</div>
{isUniversityDirty && (
<>
<div className="form-group mb-2">
<DownshiftInput
items={[...defaultRoles]}
inputValue={role}
placeholder={t('role')}
label={t('role')}
setValue={setRole}
/>
</div>
<div className="form-group mb-0">
<DownshiftInput
items={departments}
inputValue={department}
placeholder={t('department')}
label={t('department')}
setValue={setDepartment}
/>
</div>
</>
)}
</>
)
}
export default InstitutionFields

View file

@ -0,0 +1,24 @@
import { Row, Alert } from 'react-bootstrap'
import Icon from '../../../../../shared/components/icon'
import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
type LayoutProps = {
children: React.ReactNode
isError: UseAsyncReturnType['isError']
error: UseAsyncReturnType['error']
}
function Layout({ isError, error, children }: LayoutProps) {
return (
<div className="affiliations-table-row--highlighted">
<Row>{children}</Row>
{isError && (
<Alert bsStyle="danger" className="text-center">
<Icon type="exclamation-triangle" fw /> {error.getUserFacingMessage()}
</Alert>
)}
</div>
)
}
export default Layout

View file

@ -1,17 +1,14 @@
import { Trans, useTranslation } from 'react-i18next'
import { InstitutionInfo } from './add-email-input'
import { ExposedSettings } from '../../../../../../types/exposed-settings'
import getMeta from '../../../../utils/meta'
import { InstitutionInfo } from './input'
import { ExposedSettings } from '../../../../../../../types/exposed-settings'
import getMeta from '../../../../../utils/meta'
type AddEmailSSOLinkingInfoProps = {
type SSOLinkingInfoProps = {
institutionInfo: InstitutionInfo
email: string
}
export function AddEmailSSOLinkingInfo({
institutionInfo,
email,
}: AddEmailSSOLinkingInfoProps) {
function SsoLinkingInfo({ institutionInfo, email }: SSOLinkingInfoProps) {
const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
const { t } = useTranslation()
@ -50,3 +47,5 @@ export function AddEmailSSOLinkingInfo({
</>
)
}
export default SsoLinkingInfo

View file

@ -7,8 +7,8 @@ import { useUserEmailsContext } from '../../context/user-email-context'
import DownshiftInput from './downshift-input'
import useAsync from '../../../../shared/hooks/use-async'
import { getJSON, postJSON } from '../../../../infrastructure/fetch-json'
import { defaults as defaultRoles } from '../../roles'
import { defaults as defaultDepartments } from '../../departments'
import defaultRoles from '../../data/roles'
import defaultDepartments from '../../data/departments'
import { University } from '../../../../../../types/university'
type InstitutionAndRoleProps = {
@ -28,7 +28,9 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
} = useUserEmailsContext()
const [role, setRole] = useState(affiliation?.role || '')
const [department, setDepartment] = useState(affiliation?.department || '')
const [departments, setDepartments] = useState(defaultDepartments)
const [departments, setDepartments] = useState<string[]>(() => [
...defaultDepartments,
])
const roleRef = useRef<HTMLInputElement | null>(null)
const isChangingAffiliationInProgress = isChangingAffiliation(
state,
@ -62,7 +64,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
}
})
.catch(() => {
setDepartments(defaultDepartments)
setDepartments([...defaultDepartments])
})
}
@ -117,7 +119,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
<div className="affiliation-change-container small">
<form onSubmit={handleSubmit}>
<DownshiftInput
items={defaultRoles}
items={[...defaultRoles]}
inputValue={role}
placeholder={t('role')}
label={t('role')}

View file

@ -1,6 +1,4 @@
import { CountryCode } from '../../../../types/country'
export const defaults: { code: CountryCode; name: string }[] = [
const countries = <const>[
{ code: 'af', name: 'Afghanistan' },
{ code: 'ax', name: 'Åland Islands' },
{ code: 'al', name: 'Albania' },
@ -253,3 +251,6 @@ export const defaults: { code: CountryCode; name: string }[] = [
{ code: 'zm', name: 'Zambia' },
{ code: 'zw', name: 'Zimbabwe' },
]
export default countries
export type CountryCode = typeof countries[number]['code']

View file

@ -1,4 +1,4 @@
export const defaults = [
const departments = <const>[
'Aeronautics & Astronautics',
'Anesthesia',
'Anthropology',
@ -74,3 +74,5 @@ export const defaults = [
'Theater and Performance Studies',
'Urology',
]
export default departments

View file

@ -1,4 +1,4 @@
export const defaults = [
const roles = <const>[
'Undergraduate Student',
'Masters Student (MSc, MA, ...)',
'Doctoral Student (PhD, EngD, ...)',
@ -11,3 +11,5 @@ export const defaults = [
'Professor',
'Emeritus Professor',
]
export default roles

View file

@ -0,0 +1,23 @@
import getMeta from '../../../utils/meta'
import { InstitutionInfo } from '../components/emails/add-email/input'
import { ExposedSettings } from '../../../../../types/exposed-settings'
import { Nullable } from '../../../../../types/utils'
const ssoAvailableForDomain = (domain: InstitutionInfo | null) => {
const { hasSamlBeta, hasSamlFeature } = getMeta(
'ol-ExposedSettings'
) as ExposedSettings
if (!hasSamlFeature || !domain || !domain.confirmed || !domain.university) {
return false
}
if (domain.university.ssoEnabled) {
return true
}
return hasSamlBeta && domain.university.ssoBeta
}
export const isSsoAvailable = (
institutionInfo: Nullable<InstitutionInfo>
): institutionInfo is InstitutionInfo => {
return Boolean(institutionInfo && ssoAvailableForDomain(institutionInfo))
}

View file

@ -1,5 +1,5 @@
import useFetchMock from './../hooks/use-fetch-mock'
import { AddEmailInput } from '../../js/features/settings/components/emails/add-email-input'
import Input from '../../js/features/settings/components/emails/add-email/input'
export const EmailInput = args => {
useFetchMock(fetchMock =>
@ -12,7 +12,7 @@ export const EmailInput = args => {
)
return (
<>
<AddEmailInput {...args} />
<Input {...args} />
<br />
<div>
Use <code>autocomplete.edu</code> as domain to trigger an autocomplete
@ -23,7 +23,7 @@ export const EmailInput = args => {
export default {
title: 'Account Settings / Emails and Affiliations',
component: AddEmailInput,
component: Input,
argTypes: {
onChange: { action: 'change' },
},

View file

@ -7,10 +7,9 @@ import {
import { expect } from 'chai'
import sinon from 'sinon'
import fetchMock from 'fetch-mock'
import {
AddEmailInput,
import Input, {
clearDomainCache,
} from '../../../../../../frontend/js/features/settings/components/emails/add-email-input'
} from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
const testInstitutionData = [
{ university: { id: 124 }, hostname: 'domain.edu' },
@ -28,13 +27,13 @@ describe('<AddEmailInput/>', function () {
describe('on initial render', function () {
it('should render an input with a placeholder', function () {
render(<AddEmailInput {...defaultProps} />)
render(<Input {...defaultProps} />)
screen.getByPlaceholderText('e.g. johndoe@mit.edu')
})
it('should not dispatch any `change` event', function () {
const onChangeStub = sinon.stub()
render(<AddEmailInput {...defaultProps} onChange={onChangeStub} />)
render(<Input {...defaultProps} onChange={onChangeStub} />)
expect(onChangeStub.called).to.equal(false)
})
})
@ -45,7 +44,7 @@ describe('<AddEmailInput/>', function () {
beforeEach(function () {
fetchMock.get('express:/institutions/domains', 200)
onChangeStub = sinon.stub()
render(<AddEmailInput {...defaultProps} onChange={onChangeStub} />)
render(<Input {...defaultProps} onChange={onChangeStub} />)
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'user' },
})
@ -74,7 +73,7 @@ describe('<AddEmailInput/>', function () {
beforeEach(function () {
onChangeStub = sinon.stub()
render(<AddEmailInput onChange={onChangeStub} />)
render(<Input onChange={onChangeStub} />)
})
describe('when there are no matches', function () {
@ -228,7 +227,7 @@ describe('<AddEmailInput/>', function () {
// initial request populates the suggestion
fetchMock.get('express:/institutions/domains', testInstitutionData)
onChangeStub = sinon.stub()
render(<AddEmailInput {...defaultProps} onChange={onChangeStub} />)
render(<Input {...defaultProps} onChange={onChangeStub} />)
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'user@d' },
})

View file

@ -1,252 +0,0 @@
export type CountryCode =
| 'af'
| 'ax'
| 'al'
| 'dz'
| 'as'
| 'ad'
| 'ao'
| 'ai'
| 'aq'
| 'ag'
| 'ar'
| 'am'
| 'aw'
| 'au'
| 'at'
| 'az'
| 'bs'
| 'bh'
| 'bd'
| 'bb'
| 'by'
| 'be'
| 'bz'
| 'bj'
| 'bm'
| 'bt'
| 'bo'
| 'bq'
| 'ba'
| 'bw'
| 'bv'
| 'br'
| 'io'
| 'vg'
| 'bn'
| 'bg'
| 'bf'
| 'bi'
| 'kh'
| 'cm'
| 'ca'
| 'cv'
| 'ky'
| 'cf'
| 'td'
| 'cl'
| 'cn'
| 'cx'
| 'cc'
| 'co'
| 'km'
| 'cg'
| 'ck'
| 'cr'
| 'ci'
| 'hr'
| 'cu'
| 'cw'
| 'cy'
| 'cz'
| 'kp'
| 'cd'
| 'dk'
| 'dj'
| 'dm'
| 'do'
| 'ec'
| 'eg'
| 'sv'
| 'gq'
| 'er'
| 'ee'
| 'et'
| 'fk'
| 'fo'
| 'fj'
| 'fi'
| 'fr'
| 'gf'
| 'pf'
| 'tf'
| 'ga'
| 'gm'
| 'ge'
| 'de'
| 'gh'
| 'gi'
| 'gr'
| 'gl'
| 'gd'
| 'gp'
| 'gu'
| 'gt'
| 'gg'
| 'gn'
| 'gw'
| 'gy'
| 'ht'
| 'hm'
| 'va'
| 'hn'
| 'hk'
| 'hu'
| 'is'
| 'in'
| 'id'
| 'ir'
| 'iq'
| 'ie'
| 'im'
| 'il'
| 'it'
| 'jm'
| 'jp'
| 'je'
| 'jo'
| 'kz'
| 'ke'
| 'ki'
| 'xk'
| 'kw'
| 'kg'
| 'la'
| 'lv'
| 'lb'
| 'ls'
| 'lr'
| 'ly'
| 'li'
| 'lt'
| 'lu'
| 'mo'
| 'mk'
| 'mg'
| 'mw'
| 'my'
| 'mv'
| 'ml'
| 'mt'
| 'mh'
| 'mq'
| 'mr'
| 'mu'
| 'yt'
| 'mx'
| 'fm'
| 'md'
| 'mc'
| 'mn'
| 'me'
| 'ms'
| 'ma'
| 'mz'
| 'mm'
| 'na'
| 'nr'
| 'np'
| 'nl'
| 'an'
| 'nc'
| 'nz'
| 'ni'
| 'ne'
| 'ng'
| 'nu'
| 'nf'
| 'mp'
| 'no'
| 'om'
| 'pk'
| 'pw'
| 'ps'
| 'pa'
| 'pg'
| 'py'
| 'pe'
| 'ph'
| 'pn'
| 'pl'
| 'pt'
| 'pr'
| 'qa'
| 'kr'
| 're'
| 'ro'
| 'ru'
| 'rw'
| 'bl'
| 'sh'
| 'kn'
| 'lc'
| 'mf'
| 'pm'
| 'vc'
| 'ws'
| 'sm'
| 'st'
| 'sa'
| 'sn'
| 'rs'
| 'sc'
| 'sl'
| 'sg'
| 'sx'
| 'sk'
| 'si'
| 'sb'
| 'so'
| 'za'
| 'gs'
| 'ss'
| 'es'
| 'lk'
| 'sd'
| 'sr'
| 'sj'
| 'sz'
| 'se'
| 'ch'
| 'sy'
| 'tw'
| 'tj'
| 'tz'
| 'th'
| 'tl'
| 'tg'
| 'tk'
| 'to'
| 'tt'
| 'tn'
| 'tr'
| 'tm'
| 'tc'
| 'tv'
| 'vi'
| 'ug'
| 'ua'
| 'ae'
| 'gb'
| 'us'
| 'um'
| 'uy'
| 'uz'
| 'vu'
| 've'
| 'vn'
| 'wf'
| 'eh'
| 'ye'
| 'zm'
| 'zw'

View file

@ -1,4 +1,4 @@
import { CountryCode } from './country'
import { CountryCode } from '../frontend/js/features/settings/data/countries-list'
export type University = {
id: number