mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-14 23:45:30 +00:00
Merge pull request #7841 from overleaf/ii-refactor-add-email
Refactor add email section GitOrigin-RevId: 41de440caaf3baf43673c4a5f07a18b990f28c7b
This commit is contained in:
parent
a147d045b8
commit
c807bedb65
18 changed files with 377 additions and 525 deletions
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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 {
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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')}
|
||||
|
|
|
@ -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']
|
|
@ -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
|
|
@ -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
|
23
services/web/frontend/js/features/settings/utils/sso.ts
Normal file
23
services/web/frontend/js/features/settings/utils/sso.ts
Normal 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))
|
||||
}
|
|
@ -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' },
|
||||
},
|
||||
|
|
|
@ -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' },
|
||||
})
|
||||
|
|
|
@ -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'
|
|
@ -1,4 +1,4 @@
|
|||
import { CountryCode } from './country'
|
||||
import { CountryCode } from '../frontend/js/features/settings/data/countries-list'
|
||||
|
||||
export type University = {
|
||||
id: number
|
||||
|
|
Loading…
Add table
Reference in a new issue