first commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLButton, { OLButtonProps } from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
function AddAnotherEmailBtn({ onClick, ...props }: OLButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={onClick}
|
||||
className="btn-inline-link"
|
||||
{...props}
|
||||
>
|
||||
{t('add_another_email')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddAnotherEmailBtn
|
@@ -0,0 +1,32 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLButton, { OLButtonProps } from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
return Boolean(email)
|
||||
}
|
||||
|
||||
type AddNewEmailColProps = {
|
||||
email: string
|
||||
} & OLButtonProps
|
||||
|
||||
function AddNewEmailBtn({
|
||||
email,
|
||||
disabled,
|
||||
isLoading,
|
||||
...props
|
||||
}: AddNewEmailColProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<OLButton
|
||||
variant="primary"
|
||||
disabled={(disabled && !isLoading) || !isValidEmail(email)}
|
||||
isLoading={isLoading}
|
||||
{...props}
|
||||
>
|
||||
{t('add_new_email')}
|
||||
</OLButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddNewEmailBtn
|
@@ -0,0 +1,114 @@
|
||||
import { useState, forwardRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCombobox } from 'downshift'
|
||||
import classnames from 'classnames'
|
||||
import countries, { CountryCode } from '../../../data/countries-list'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
|
||||
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 Downshift({ setValue, inputRef }: CountryInputProps) {
|
||||
const { t } = useTranslation()
|
||||
const [inputItems, setInputItems] = useState(() => [...countries])
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
getLabelProps,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getComboboxProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
openMenu,
|
||||
selectedItem,
|
||||
} = useCombobox({
|
||||
inputValue,
|
||||
items: inputItems,
|
||||
itemToString,
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
setValue(selectedItem?.code ?? null)
|
||||
setInputValue(selectedItem?.name ?? '')
|
||||
},
|
||||
onInputValueChange: ({ inputValue = '' }) => {
|
||||
setInputItems(
|
||||
countries.filter(country =>
|
||||
itemToString(country).toLowerCase().includes(inputValue.toLowerCase())
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const shouldOpen = isOpen && inputItems.length
|
||||
|
||||
return (
|
||||
<div className={classnames('dropdown', 'd-block')}>
|
||||
<div {...getComboboxProps()}>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-for */}
|
||||
<label {...getLabelProps()} className="visually-hidden">
|
||||
{t('country')}
|
||||
</label>
|
||||
<OLFormControl
|
||||
{...getInputProps({
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value)
|
||||
},
|
||||
onFocus: () => {
|
||||
if (!isOpen) {
|
||||
openMenu()
|
||||
}
|
||||
},
|
||||
ref: inputRef,
|
||||
})}
|
||||
placeholder={t('country')}
|
||||
/>
|
||||
<i className="caret" />
|
||||
</div>
|
||||
<ul
|
||||
{...getMenuProps()}
|
||||
className={classnames('dropdown-menu', 'select-dropdown-menu', {
|
||||
show: shouldOpen,
|
||||
})}
|
||||
>
|
||||
{inputItems.map((item, index) => (
|
||||
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
|
||||
<li
|
||||
key={`${item.name}-${index}`}
|
||||
{...getItemProps({ item, index })}
|
||||
aria-selected={selectedItem?.name === item.name}
|
||||
>
|
||||
<DropdownItem
|
||||
as="span"
|
||||
role={undefined}
|
||||
className={classnames({
|
||||
active: selectedItem?.name === item.name,
|
||||
'dropdown-item-highlighted': highlightedIndex === index,
|
||||
})}
|
||||
trailingIcon={
|
||||
selectedItem?.name === item.name ? 'check' : undefined
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CountryInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<CountryInputProps, 'inputRef'>
|
||||
>((props, ref) => <Downshift {...props} inputRef={ref} />)
|
||||
|
||||
CountryInput.displayName = 'CountryInput'
|
||||
|
||||
export default CountryInput
|
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLButton, { OLButtonProps } from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
function EmailAffiliatedWithInstitution({ onClick, ...props }: OLButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="mt-1">
|
||||
{t('is_email_affiliated')}
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={onClick}
|
||||
className="btn-inline-link"
|
||||
{...props}
|
||||
>
|
||||
{t('let_us_know')}
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailAffiliatedWithInstitution
|
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { Nullable } from '../../../../../../../types/utils'
|
||||
import { getJSON } from '../../../../../infrastructure/fetch-json'
|
||||
import useAbortController from '../../../../../shared/hooks/use-abort-controller'
|
||||
import domainBlocklist from '../../../domain-blocklist'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
|
||||
const LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
|
||||
|
||||
function matchLocalAndDomain(emailHint: string) {
|
||||
const match = emailHint.match(LOCAL_AND_DOMAIN_REGEX)
|
||||
if (match) {
|
||||
return { local: match[1], domain: match[2] }
|
||||
} else {
|
||||
return { local: null, domain: null }
|
||||
}
|
||||
}
|
||||
|
||||
export type DomainInfo = {
|
||||
hostname: string
|
||||
confirmed?: boolean
|
||||
university: {
|
||||
id: number
|
||||
name: string
|
||||
ssoEnabled?: boolean
|
||||
ssoBeta?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
let domainCache = new Map<string, DomainInfo>()
|
||||
|
||||
export function clearDomainCache() {
|
||||
domainCache = new Map<string, DomainInfo>()
|
||||
}
|
||||
|
||||
type InputProps = {
|
||||
onChange: (value: string, domain?: DomainInfo) => void
|
||||
handleAddNewEmail: () => void
|
||||
}
|
||||
|
||||
function Input({ onChange, handleAddNewEmail }: InputProps) {
|
||||
const { signal } = useAbortController()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [suggestion, setSuggestion] = useState<string | null>(null)
|
||||
const [inputValue, setInputValue] = useState<string | null>(null)
|
||||
const [matchedDomain, setMatchedDomain] = useState<DomainInfo | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
}, [inputRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValue == null) {
|
||||
return
|
||||
}
|
||||
if (matchedDomain && inputValue.endsWith(matchedDomain.hostname)) {
|
||||
onChange(inputValue, matchedDomain)
|
||||
} else {
|
||||
onChange(inputValue)
|
||||
}
|
||||
}, [onChange, inputValue, suggestion, matchedDomain])
|
||||
|
||||
const handleEmailChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const hint = event.target.value
|
||||
setInputValue(hint)
|
||||
const { local, domain } = matchLocalAndDomain(hint)
|
||||
if (domain && !matchedDomain?.hostname.startsWith(domain)) {
|
||||
setSuggestion(null)
|
||||
}
|
||||
if (!domain) {
|
||||
return
|
||||
}
|
||||
if (domainCache.has(domain)) {
|
||||
const cachedDomain = domainCache.get(domain) as DomainInfo
|
||||
setSuggestion(`${local}@${cachedDomain.hostname}`)
|
||||
setMatchedDomain(cachedDomain)
|
||||
return
|
||||
}
|
||||
if (domainBlocklist.some(d => domain.endsWith(d))) {
|
||||
return
|
||||
}
|
||||
const query = `?hostname=${domain}&limit=1`
|
||||
getJSON<Nullable<DomainInfo[]>>(`/institutions/domains${query}`, {
|
||||
signal,
|
||||
})
|
||||
.then(data => {
|
||||
if (!(data && data[0])) {
|
||||
return
|
||||
}
|
||||
if (domainBlocklist.some(d => data[0].hostname.endsWith(d))) {
|
||||
return
|
||||
}
|
||||
const hostname = data[0]?.hostname
|
||||
if (hostname) {
|
||||
domainCache.set(domain, data[0])
|
||||
setSuggestion(`${local}@${hostname}`)
|
||||
setMatchedDomain(data[0])
|
||||
} else {
|
||||
setSuggestion(null)
|
||||
setMatchedDomain(null)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
debugConsole.error(error)
|
||||
setSuggestion(null)
|
||||
setMatchedDomain(null)
|
||||
})
|
||||
},
|
||||
[signal, matchedDomain]
|
||||
)
|
||||
|
||||
const handleKeyDownEvent = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const setInputValueAndResetSuggestion = () => {
|
||||
setInputValue(suggestion)
|
||||
setSuggestion(null)
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
|
||||
if (suggestion) {
|
||||
setInputValueAndResetSuggestion()
|
||||
return
|
||||
}
|
||||
|
||||
if (!inputValue) {
|
||||
return
|
||||
}
|
||||
|
||||
const match = matchLocalAndDomain(inputValue)
|
||||
if (match.local && match.domain) {
|
||||
handleAddNewEmail()
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'Tab' && suggestion) {
|
||||
event.preventDefault()
|
||||
setInputValueAndResetSuggestion()
|
||||
}
|
||||
},
|
||||
[inputValue, suggestion, handleAddNewEmail]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputValue) {
|
||||
setSuggestion(null)
|
||||
} else if (suggestion && !suggestion.startsWith(inputValue)) {
|
||||
setSuggestion(null)
|
||||
}
|
||||
}, [suggestion, inputValue])
|
||||
|
||||
return (
|
||||
<div className="input-suggestions">
|
||||
<OLFormControl
|
||||
data-testid="affiliations-email-shadow"
|
||||
readOnly
|
||||
className="input-suggestions-shadow"
|
||||
value={suggestion || ''}
|
||||
/>
|
||||
<OLFormControl
|
||||
id="affiliations-email"
|
||||
data-testid="affiliations-email"
|
||||
className="input-suggestions-main"
|
||||
type="email"
|
||||
onChange={handleEmailChange}
|
||||
onKeyDown={handleKeyDownEvent}
|
||||
value={inputValue || ''}
|
||||
placeholder="e.g. johndoe@mit.edu"
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
@@ -0,0 +1,200 @@
|
||||
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 { DomainInfo } from './input'
|
||||
import { getJSON } from '../../../../../infrastructure/fetch-json'
|
||||
import useAsync from '../../../../../shared/hooks/use-async'
|
||||
import UniversityName from './university-name'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
|
||||
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>>
|
||||
newEmailMatchedDomain: DomainInfo | null
|
||||
}
|
||||
|
||||
function InstitutionFields({
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
universities,
|
||||
setUniversities,
|
||||
universityName,
|
||||
setUniversityName,
|
||||
role,
|
||||
setRole,
|
||||
department,
|
||||
setDepartment,
|
||||
newEmailMatchedDomain,
|
||||
}: 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<University[]>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isInstitutionFieldsVisible && countryRef.current) {
|
||||
countryRef.current?.focus()
|
||||
}
|
||||
}, [countryRef, isInstitutionFieldsVisible])
|
||||
|
||||
useEffect(() => {
|
||||
if (universityName) {
|
||||
setIsUniversityDirty(true)
|
||||
}
|
||||
}, [setIsUniversityDirty, universityName])
|
||||
|
||||
// If the institution selected by autocompletion has changed
|
||||
// hide the fields visibility and reset values
|
||||
useEffect(() => {
|
||||
if (!newEmailMatchedDomain) {
|
||||
setIsInstitutionFieldsVisible(false)
|
||||
setRole('')
|
||||
setDepartment('')
|
||||
}
|
||||
}, [newEmailMatchedDomain, setRole, setDepartment])
|
||||
|
||||
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(
|
||||
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)
|
||||
.filter(name =>
|
||||
name.trim().toLowerCase().includes(universityName.toLowerCase())
|
||||
) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
const handleShowInstitutionFields = () => {
|
||||
setIsInstitutionFieldsVisible(true)
|
||||
}
|
||||
|
||||
const handleSelectUniversityManually = () => {
|
||||
setRole('')
|
||||
setDepartment('')
|
||||
handleShowInstitutionFields()
|
||||
}
|
||||
|
||||
const isLetUsKnowVisible =
|
||||
!newEmailMatchedDomain && !isInstitutionFieldsVisible
|
||||
const isAutocompletedInstitutionVisible =
|
||||
newEmailMatchedDomain && !isInstitutionFieldsVisible
|
||||
const isRoleAndDepartmentVisible =
|
||||
isAutocompletedInstitutionVisible || isUniversityDirty
|
||||
|
||||
// Is the email affiliated with an institution?
|
||||
if (isLetUsKnowVisible) {
|
||||
return (
|
||||
<EmailAffiliatedWithInstitution onClick={handleShowInstitutionFields} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAutocompletedInstitutionVisible ? (
|
||||
// Display the institution name after autocompletion
|
||||
<UniversityName
|
||||
name={newEmailMatchedDomain.university.name}
|
||||
onClick={handleSelectUniversityManually}
|
||||
/>
|
||||
) : (
|
||||
// Display the country and university fields
|
||||
<>
|
||||
<OLFormGroup className="mb-2">
|
||||
<CountryInput
|
||||
id="new-email-country-input"
|
||||
setValue={setCountryCode}
|
||||
ref={countryRef}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
<OLFormGroup className={isRoleAndDepartmentVisible ? 'mb-2' : 'mb-0'}>
|
||||
<DownshiftInput
|
||||
items={getUniversityItems()}
|
||||
inputValue={universityName}
|
||||
placeholder={t('university')}
|
||||
label={t('university')}
|
||||
setValue={setUniversityName}
|
||||
disabled={!countryCode}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</>
|
||||
)}
|
||||
{isRoleAndDepartmentVisible && (
|
||||
<>
|
||||
<OLFormGroup className="mb-2">
|
||||
<DownshiftInput
|
||||
items={[...defaultRoles]}
|
||||
inputValue={role}
|
||||
placeholder={t('role')}
|
||||
label={t('role')}
|
||||
setValue={setRole}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
<OLFormGroup className="mb-0">
|
||||
<DownshiftInput
|
||||
items={departments}
|
||||
inputValue={department}
|
||||
placeholder={t('department')}
|
||||
label={t('department')}
|
||||
setValue={setDepartment}
|
||||
/>
|
||||
</OLFormGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstitutionFields
|
@@ -0,0 +1,26 @@
|
||||
import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
|
||||
import { getUserFacingMessage } from '../../../../../infrastructure/fetch-json'
|
||||
import OLRow from '@/features/ui/components/ol/ol-row'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode
|
||||
isError: UseAsyncReturnType['isError']
|
||||
error: UseAsyncReturnType['error']
|
||||
}
|
||||
|
||||
function Layout({ isError, error, children }: LayoutProps) {
|
||||
return (
|
||||
<div className="affiliations-table-row-highlighted">
|
||||
<OLRow>{children}</OLRow>
|
||||
{isError && (
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={getUserFacingMessage(error) ?? ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
@@ -0,0 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { DomainInfo } from './input'
|
||||
import getMeta from '../../../../../utils/meta'
|
||||
import { useLocation } from '../../../../../shared/hooks/use-location'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
type SSOLinkingInfoProps = {
|
||||
domainInfo: DomainInfo
|
||||
email: string
|
||||
}
|
||||
|
||||
function SsoLinkingInfo({ domainInfo, email }: SSOLinkingInfoProps) {
|
||||
const { samlInitPath } = getMeta('ol-ExposedSettings')
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
|
||||
const [linkAccountsButtonDisabled, setLinkAccountsButtonDisabled] =
|
||||
useState(false)
|
||||
|
||||
function handleLinkAccountsButtonClick() {
|
||||
setLinkAccountsButtonDisabled(true)
|
||||
location.assign(
|
||||
`${samlInitPath}?university_id=${domainInfo.university.id}&auto=/user/settings&email=${email}`
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="affiliations-table-label">{domainInfo.university.name}</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="to_add_email_accounts_need_to_be_linked_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: domainInfo.university.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="doing_this_will_verify_affiliation_and_allow_log_in_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: domainInfo.university.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<a
|
||||
href="/learn/how-to/Institutional_Login"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('find_out_more_about_institution_login')}.
|
||||
</a>
|
||||
</p>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
className="btn-link-accounts"
|
||||
size="sm"
|
||||
disabled={linkAccountsButtonDisabled}
|
||||
onClick={handleLinkAccountsButtonClick}
|
||||
>
|
||||
{t('link_accounts_and_add_email')}
|
||||
</OLButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SsoLinkingInfo
|
@@ -0,0 +1,25 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
|
||||
type UniversityNameProps = {
|
||||
name: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
function UniversityName({ name, onClick }: UniversityNameProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<p>
|
||||
{name}
|
||||
<span className="small">
|
||||
{' '}
|
||||
<OLButton variant="link" onClick={onClick} className="btn-inline-link">
|
||||
{t('change')}
|
||||
</OLButton>
|
||||
</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default UniversityName
|
Reference in New Issue
Block a user