mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-05 05:18:21 +00:00
Merge pull request #17921 from overleaf/ii-bs5-alert-continue
[web] Replace all alerts with notifications in settings page GitOrigin-RevId: a05755c39d2e54b3f57ffa589885e3e96aee00dc
This commit is contained in:
parent
62969f5fb6
commit
1e7053cb8e
16 changed files with 473 additions and 355 deletions
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { Alert, ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getUserFacingMessage,
|
||||
|
@ -10,6 +10,7 @@ import { ExposedSettings } from '../../../../../types/exposed-settings'
|
|||
import useAsync from '../../../shared/hooks/use-async'
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
function AccountInfoSection() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -104,12 +105,18 @@ function AccountInfoSection() {
|
|||
/>
|
||||
{isSuccess ? (
|
||||
<FormGroup>
|
||||
<Alert bsStyle="success">{t('thanks_settings_updated')}</Alert>
|
||||
<NotificationWrapper
|
||||
type="success"
|
||||
content={t('thanks_settings_updated')}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
{isError ? (
|
||||
<FormGroup>
|
||||
<Alert bsStyle="danger">{getUserFacingMessage(error)}</Alert>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={getUserFacingMessage(error) ?? ''}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
{canUpdateEmail || canUpdateNames ? (
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Alert } from 'react-bootstrap'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
|
||||
import { getUserFacingMessage } from '../../../../../infrastructure/fetch-json'
|
||||
import RowWrapper from '@/features/ui/components/bootstrap-5/wrappers/row-wrapper'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode
|
||||
|
@ -15,9 +15,14 @@ function Layout({ isError, error, children }: LayoutProps) {
|
|||
<div className="affiliations-table-row--highlighted">
|
||||
<RowWrapper>{children}</RowWrapper>
|
||||
{isError && (
|
||||
<Alert bsStyle="danger" className="text-center">
|
||||
<Icon type="exclamation-triangle" fw /> {getUserFacingMessage(error)}
|
||||
</Alert>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={getUserFacingMessage(error) ?? ''}
|
||||
bs3Props={{
|
||||
icon: <Icon type="exclamation-triangle" fw />,
|
||||
className: 'text-center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
import { ReactNode } from 'react'
|
||||
import { useState, useEffect, useLayoutEffect } from 'react'
|
||||
import { UserEmailData } from '../../../../../../types/user-email'
|
||||
import classNames from 'classnames'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import ReconfirmationInfoSuccess from './reconfirmation-info/reconfirmation-info-success'
|
||||
import ReconfirmationInfoPrompt from './reconfirmation-info/reconfirmation-info-prompt'
|
||||
import ReconfirmationInfoPromptText from './reconfirmation-info/reconfirmation-info-prompt-text'
|
||||
import RowWrapper from '@/features/ui/components/bootstrap-5/wrappers/row-wrapper'
|
||||
import ColWrapper from '@/features/ui/components/bootstrap-5/wrappers/col-wrapper'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
|
||||
import Icon from '@/shared/components/icon'
|
||||
import { useUserEmailsContext } from '@/features/settings/context/user-email-context'
|
||||
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { ssoAvailableForInstitution } from '@/features/settings/utils/sso'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { ExposedSettings } from '../../../../../../types/exposed-settings'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||
import classnames from 'classnames'
|
||||
|
||||
type ReconfirmationInfoProps = {
|
||||
userEmailData: UserEmailData
|
||||
|
@ -17,6 +29,48 @@ function ReconfirmationInfo({ userEmailData }: ReconfirmationInfoProps) {
|
|||
) as string
|
||||
const reconfirmedViaSAML = getMeta('ol-reconfirmedViaSAML') as string
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
|
||||
const { error, isLoading, isError, isSuccess, runAsync } = useAsync()
|
||||
const { state, setLoading: setUserEmailsContextLoading } =
|
||||
useUserEmailsContext()
|
||||
const [hasSent, setHasSent] = useState(false)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const location = useLocation()
|
||||
const ssoAvailable = Boolean(
|
||||
ssoAvailableForInstitution(userEmailData.affiliation?.institution ?? null)
|
||||
)
|
||||
|
||||
const handleRequestReconfirmation = () => {
|
||||
if (userEmailData.affiliation?.institution && ssoAvailable) {
|
||||
setIsPending(true)
|
||||
location.assign(
|
||||
`${samlInitPath}?university_id=${userEmailData.affiliation.institution.id}&reconfirm=/user/settings`
|
||||
)
|
||||
} else {
|
||||
runAsync(
|
||||
postJSON('/user/emails/send-reconfirmation', {
|
||||
body: {
|
||||
email: userEmailData.email,
|
||||
},
|
||||
})
|
||||
).catch(debugConsole.error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setUserEmailsContextLoading(isLoading)
|
||||
}, [setUserEmailsContextLoading, isLoading])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isSuccess) {
|
||||
setHasSent(true)
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
const rateLimited =
|
||||
isError && error instanceof FetchError && error.response?.status === 429
|
||||
|
||||
if (!userEmailData.affiliation) {
|
||||
return null
|
||||
}
|
||||
|
@ -26,53 +80,189 @@ function ReconfirmationInfo({ userEmailData }: ReconfirmationInfoProps) {
|
|||
userEmailData.samlProviderId === reconfirmedViaSAML
|
||||
) {
|
||||
return (
|
||||
<ReconfirmationInfoContentWrapper asAlertInfo>
|
||||
<ReconfirmationInfoSuccess
|
||||
institution={userEmailData.affiliation.institution}
|
||||
/>
|
||||
</ReconfirmationInfoContentWrapper>
|
||||
<RowWrapper>
|
||||
<ColWrapper md={12}>
|
||||
<NotificationWrapper
|
||||
type="info"
|
||||
content={
|
||||
<ReconfirmationInfoSuccess
|
||||
institution={userEmailData.affiliation.institution}
|
||||
/>
|
||||
}
|
||||
bs3Props={{ className: 'settings-reconfirm-info small' }}
|
||||
/>
|
||||
</ColWrapper>
|
||||
</RowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
if (userEmailData.affiliation.inReconfirmNotificationPeriod) {
|
||||
return (
|
||||
<ReconfirmationInfoContentWrapper
|
||||
asAlertInfo={reconfirmationRemoveEmail === userEmailData.email}
|
||||
>
|
||||
<ReconfirmationInfoPrompt
|
||||
institution={userEmailData.affiliation.institution}
|
||||
primary={userEmailData.default}
|
||||
email={userEmailData.email}
|
||||
/>
|
||||
</ReconfirmationInfoContentWrapper>
|
||||
<RowWrapper>
|
||||
<ColWrapper md={12}>
|
||||
{isBootstrap5 ? (
|
||||
<NotificationWrapper
|
||||
type="info"
|
||||
content={
|
||||
<>
|
||||
{hasSent ? (
|
||||
<Trans
|
||||
i18nKey="please_check_your_inbox_to_confirm"
|
||||
values={{
|
||||
institutionName:
|
||||
userEmailData.affiliation.institution.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ReconfirmationInfoPromptText
|
||||
institutionName={
|
||||
userEmailData.affiliation.institution.name
|
||||
}
|
||||
primary={userEmailData.default}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{isError && (
|
||||
<div className="text-danger">
|
||||
{rateLimited
|
||||
? t('too_many_requests')
|
||||
: t('generic_something_went_wrong')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
action={
|
||||
hasSent ? (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="refresh" spin fw /> {t('sending')}...
|
||||
</>
|
||||
) : (
|
||||
<ButtonWrapper
|
||||
className="btn-inline-link"
|
||||
disabled={state.isLoading}
|
||||
onClick={handleRequestReconfirmation}
|
||||
>
|
||||
{t('resend_confirmation_email')}
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ButtonWrapper
|
||||
variant="secondary"
|
||||
disabled={state.isLoading || isPending}
|
||||
onClick={handleRequestReconfirmation}
|
||||
bs3Props={{ bsStyle: 'info' }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="refresh" spin fw /> {t('sending')}...
|
||||
</>
|
||||
) : (
|
||||
t('confirm_affiliation')
|
||||
)}
|
||||
</ButtonWrapper>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={classnames('settings-reconfirm-info', 'small', {
|
||||
'alert alert-info':
|
||||
reconfirmationRemoveEmail === userEmailData.email,
|
||||
})}
|
||||
>
|
||||
{hasSent ? (
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="please_check_your_inbox_to_confirm"
|
||||
values={{
|
||||
institutionName:
|
||||
userEmailData.affiliation.institution.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
}
|
||||
/>{' '}
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="refresh" spin fw /> {t('sending')}...
|
||||
</>
|
||||
) : (
|
||||
<ButtonWrapper
|
||||
className="btn-inline-link"
|
||||
disabled={state.isLoading}
|
||||
onClick={handleRequestReconfirmation}
|
||||
>
|
||||
{t('resend_confirmation_email')}
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
<br />
|
||||
{isError && (
|
||||
<div className="text-danger">
|
||||
{rateLimited
|
||||
? t('too_many_requests')
|
||||
: t('generic_something_went_wrong')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<ReconfirmationInfoPromptText
|
||||
institutionName={
|
||||
userEmailData.affiliation.institution.name
|
||||
}
|
||||
primary={userEmailData.default}
|
||||
icon={
|
||||
<Icon type="warning" className="me-1 icon-warning" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="setting-reconfirm-info-right">
|
||||
<ButtonWrapper
|
||||
variant="secondary"
|
||||
disabled={state.isLoading || isPending}
|
||||
onClick={handleRequestReconfirmation}
|
||||
bs3Props={{ bsStyle: 'info' }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="refresh" spin fw /> {t('sending')}...
|
||||
</>
|
||||
) : (
|
||||
t('confirm_affiliation')
|
||||
)}
|
||||
</ButtonWrapper>
|
||||
<br />
|
||||
{isError && (
|
||||
<div className="text-danger">
|
||||
{rateLimited
|
||||
? t('too_many_requests')
|
||||
: t('generic_something_went_wrong')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ColWrapper>
|
||||
</RowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
type ReconfirmationInfoContentWrapperProps = {
|
||||
asAlertInfo: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function ReconfirmationInfoContentWrapper({
|
||||
asAlertInfo,
|
||||
children,
|
||||
}: ReconfirmationInfoContentWrapperProps) {
|
||||
return (
|
||||
<RowWrapper>
|
||||
<ColWrapper md={12}>
|
||||
<div
|
||||
className={classNames('settings-reconfirm-info', 'small', {
|
||||
'alert alert-info': asAlertInfo,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ColWrapper>
|
||||
</RowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReconfirmationInfo
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Institution } from '../../../../../../../types/institution'
|
||||
|
||||
type ReconfirmationInfoPromptTextProps = {
|
||||
primary: boolean
|
||||
institutionName: Institution['name']
|
||||
icon?: React.ReactElement // BS3 only
|
||||
}
|
||||
|
||||
function ReconfirmationInfoPromptText({
|
||||
primary,
|
||||
institutionName,
|
||||
icon,
|
||||
}: ReconfirmationInfoPromptTextProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
{icon}
|
||||
<Trans
|
||||
i18nKey="are_you_still_at"
|
||||
values={{
|
||||
institutionName,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
}
|
||||
/>{' '}
|
||||
<Trans
|
||||
i18nKey="please_reconfirm_institutional_email"
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<span />]
|
||||
}
|
||||
/>{' '}
|
||||
<a
|
||||
href="/learn/how-to/Institutional_Email_Reconfirmation"
|
||||
target="_blank"
|
||||
>
|
||||
{t('learn_more')}
|
||||
</a>
|
||||
<br />
|
||||
{primary ? <i>{t('need_to_add_new_primary_before_remove')}</i> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReconfirmationInfoPromptText
|
|
@ -1,186 +0,0 @@
|
|||
import { useState, useEffect, useLayoutEffect } from 'react'
|
||||
import useAsync from '../../../../../shared/hooks/use-async'
|
||||
import { FetchError, postJSON } from '../../../../../infrastructure/fetch-json'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Institution } from '../../../../../../../types/institution'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useUserEmailsContext } from '../../../context/user-email-context'
|
||||
import getMeta from '../../../../../utils/meta'
|
||||
import { ExposedSettings } from '../../../../../../../types/exposed-settings'
|
||||
import { ssoAvailableForInstitution } from '../../../utils/sso'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import { useLocation } from '../../../../../shared/hooks/use-location'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
type ReconfirmationInfoPromptProps = {
|
||||
email: string
|
||||
primary: boolean
|
||||
institution: Institution
|
||||
}
|
||||
|
||||
function ReconfirmationInfoPrompt({
|
||||
email,
|
||||
primary,
|
||||
institution,
|
||||
}: ReconfirmationInfoPromptProps) {
|
||||
const { t } = useTranslation()
|
||||
const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
|
||||
const { error, isLoading, isError, isSuccess, runAsync } = useAsync()
|
||||
const { state, setLoading: setUserEmailsContextLoading } =
|
||||
useUserEmailsContext()
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [hasSent, setHasSent] = useState(false)
|
||||
const ssoAvailable = Boolean(ssoAvailableForInstitution(institution))
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
setUserEmailsContextLoading(isLoading)
|
||||
}, [setUserEmailsContextLoading, isLoading])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isSuccess) {
|
||||
setHasSent(true)
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
const handleRequestReconfirmation = () => {
|
||||
if (ssoAvailable) {
|
||||
setIsPending(true)
|
||||
location.assign(
|
||||
`${samlInitPath}?university_id=${institution.id}&reconfirm=/user/settings`
|
||||
)
|
||||
} else {
|
||||
runAsync(
|
||||
postJSON('/user/emails/send-reconfirmation', {
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
).catch(debugConsole.error)
|
||||
}
|
||||
}
|
||||
|
||||
const rateLimited =
|
||||
isError && error instanceof FetchError && error.response?.status === 429
|
||||
|
||||
if (hasSent) {
|
||||
return (
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="please_check_your_inbox_to_confirm"
|
||||
values={{
|
||||
institutionName: institution.name,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
}
|
||||
/>{' '}
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="refresh" spin fw /> {t('sending')}...
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
className="btn-inline-link"
|
||||
disabled={state.isLoading}
|
||||
onClick={handleRequestReconfirmation}
|
||||
>
|
||||
{t('resend_confirmation_email')}
|
||||
</Button>
|
||||
)}
|
||||
<br />
|
||||
{isError && (
|
||||
<div className="text-danger">
|
||||
{rateLimited
|
||||
? t('too_many_requests')
|
||||
: t('generic_something_went_wrong')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<ReconfirmationInfoPromptText
|
||||
institutionName={institution.name}
|
||||
primary={primary}
|
||||
/>
|
||||
</div>
|
||||
<div className="setting-reconfirm-info-right">
|
||||
<Button
|
||||
bsStyle="info"
|
||||
disabled={state.isLoading || isPending}
|
||||
onClick={handleRequestReconfirmation}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Icon type="refresh" spin fw /> {t('sending')}...
|
||||
</>
|
||||
) : (
|
||||
t('confirm_affiliation')
|
||||
)}
|
||||
</Button>
|
||||
<br />
|
||||
{isError && (
|
||||
<div className="text-danger">
|
||||
{rateLimited
|
||||
? t('too_many_requests')
|
||||
: t('generic_something_went_wrong')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type ReconfirmationInfoPromptTextProps = {
|
||||
primary: boolean
|
||||
institutionName: Institution['name']
|
||||
}
|
||||
|
||||
function ReconfirmationInfoPromptText({
|
||||
primary,
|
||||
institutionName,
|
||||
}: ReconfirmationInfoPromptTextProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Icon type="warning" className="me-1 icon-warning" />
|
||||
<Trans
|
||||
i18nKey="are_you_still_at"
|
||||
values={{
|
||||
institutionName,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<strong />]
|
||||
}
|
||||
/>{' '}
|
||||
<Trans
|
||||
i18nKey="please_reconfirm_institutional_email"
|
||||
components={
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
[<span />]
|
||||
}
|
||||
/>{' '}
|
||||
<a
|
||||
href="/learn/how-to/Institutional_Email_Reconfirmation"
|
||||
target="_blank"
|
||||
>
|
||||
{t('learn_more')}
|
||||
</a>
|
||||
<br />
|
||||
{primary ? <i>{t('need_to_add_new_primary_before_remove')}</i> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReconfirmationInfoPrompt
|
|
@ -14,14 +14,16 @@ function ReconfirmationInfoSuccess({
|
|||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Trans
|
||||
i18nKey="your_affiliation_is_confirmed"
|
||||
values={{ institutionName: institution.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>{' '}
|
||||
{t('thank_you_exclamation')}
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="your_affiliation_is_confirmed"
|
||||
values={{ institutionName: institution.name }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>{' '}
|
||||
{t('thank_you_exclamation')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import Icon from '../../../../shared/components/icon'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
type InstitutionLink = {
|
||||
universityName: string
|
||||
|
@ -36,18 +36,28 @@ export function SSOAlert() {
|
|||
|
||||
if (samlError) {
|
||||
return !errorClosed ? (
|
||||
<Alert bsStyle="danger" className="mb-0" onDismiss={handleErrorClosed}>
|
||||
<p className="text-center">
|
||||
<Icon
|
||||
type="exclamation-triangle"
|
||||
accessibilityLabel={t('generic_something_went_wrong')}
|
||||
/>{' '}
|
||||
{samlError.translatedMessage
|
||||
? samlError.translatedMessage
|
||||
: samlError.message}
|
||||
</p>
|
||||
{samlError.tryAgain && <p className="text-center">{t('try_again')}</p>}
|
||||
</Alert>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={
|
||||
<>
|
||||
{samlError.translatedMessage
|
||||
? samlError.translatedMessage
|
||||
: samlError.message}
|
||||
{samlError.tryAgain && <p>{t('try_again')}</p>}
|
||||
</>
|
||||
}
|
||||
isDismissible
|
||||
onDismiss={handleErrorClosed}
|
||||
bs3Props={{
|
||||
icon: (
|
||||
<Icon
|
||||
type="exclamation-triangle"
|
||||
accessibilityLabel={t('generic_something_went_wrong')}
|
||||
/>
|
||||
),
|
||||
className: 'mb-0 text-center',
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
|
@ -58,40 +68,43 @@ export function SSOAlert() {
|
|||
return (
|
||||
<>
|
||||
{!infoClosed && (
|
||||
<Alert bsStyle="info" className="mb-0" onDismiss={handleInfoClosed}>
|
||||
<p className="text-center">
|
||||
<Trans
|
||||
i18nKey="institution_acct_successfully_linked_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institutionLinked.universityName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
{institutionLinked.hasEntitlement && (
|
||||
<p className="text-center">
|
||||
<Trans
|
||||
i18nKey="this_grants_access_to_features_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ featureType: t('professional') }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</Alert>
|
||||
<NotificationWrapper
|
||||
type="info"
|
||||
content={
|
||||
<>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="institution_acct_successfully_linked_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institutionLinked.universityName }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
{institutionLinked.hasEntitlement && (
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="this_grants_access_to_features_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ featureType: t('professional') }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
isDismissible
|
||||
onDismiss={handleInfoClosed}
|
||||
bs3Props={{
|
||||
className: 'mb-0 text-center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!warningClosed && institutionEmailNonCanonical && (
|
||||
<Alert
|
||||
bsStyle="warning"
|
||||
className="mb-0"
|
||||
onDismiss={handleWarningClosed}
|
||||
>
|
||||
<p className="text-center">
|
||||
<Icon
|
||||
type="exclamation-triangle"
|
||||
accessibilityLabel={t('generic_something_went_wrong')}
|
||||
/>{' '}
|
||||
<NotificationWrapper
|
||||
type="warning"
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="in_order_to_match_institutional_metadata_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
|
@ -99,8 +112,20 @@ export function SSOAlert() {
|
|||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
}
|
||||
isDismissible
|
||||
onDismiss={handleWarningClosed}
|
||||
bs3Props={{
|
||||
icon: (
|
||||
<Icon
|
||||
type="exclamation-triangle"
|
||||
accessibilityLabel={t('generic_something_went_wrong')}
|
||||
fw
|
||||
/>
|
||||
),
|
||||
className: 'text-center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Alert } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import { FetchError } from '../../../../infrastructure/fetch-json'
|
||||
import { ExposedSettings } from '../../../../../../types/exposed-settings'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
type LeaveModalFormErrorProps = {
|
||||
error: FetchError
|
||||
|
@ -32,15 +32,20 @@ function LeaveModalFormError({ error }: LeaveModalFormErrorProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Alert bsStyle="danger">
|
||||
{errorMessage}
|
||||
{errorTip ? (
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={
|
||||
<>
|
||||
<br />
|
||||
{errorTip}
|
||||
{errorMessage}
|
||||
{errorTip ? (
|
||||
<>
|
||||
<br />
|
||||
{errorTip}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import { useUserEmailsContext } from '../context/user-email-context'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
function sendMetrics(segmentation: 'view' | 'click' | 'close') {
|
||||
sendMB('institutional-leavers-survey-notification', { type: segmentation })
|
||||
|
@ -47,19 +47,25 @@ export function LeaversSurveyAlert() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Alert className="mb-0" bsStyle="info" onDismiss={handleDismiss}>
|
||||
<p>
|
||||
<strong>{t('limited_offer')}</strong>
|
||||
{`: ${t('institutional_leavers_survey_notification')} `}
|
||||
<a
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSfYdeeoY5p1d31r5iUx1jw0O-Gd66vcsBi_Ntu3lJRMjV2EJA/viewform"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{t('take_short_survey')}
|
||||
</a>
|
||||
</p>
|
||||
</Alert>
|
||||
<NotificationWrapper
|
||||
type="info"
|
||||
content={
|
||||
<>
|
||||
<strong>{t('limited_offer')}</strong>
|
||||
{`: ${t('institutional_leavers_survey_notification')} `}
|
||||
<a
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSfYdeeoY5p1d31r5iUx1jw0O-Gd66vcsBi_Ntu3lJRMjV2EJA/viewform"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{t('take_short_survey')}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
isDismissible
|
||||
onDismiss={handleDismiss}
|
||||
className="mb-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useState, ElementType } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
|
||||
import { useSSOContext, SSOSubscription } from '../context/sso-context'
|
||||
|
@ -7,6 +6,7 @@ import { SSOLinkingWidget } from './linking/sso-widget'
|
|||
import getMeta from '../../../utils/meta'
|
||||
import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
|
||||
import { useSplitTestContext } from '@/shared/context/split-test-context'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
function LinkingSection() {
|
||||
useBroadcastUser()
|
||||
|
@ -126,7 +126,10 @@ function LinkingSection() {
|
|||
{t('project_synchronisation')}
|
||||
</h3>
|
||||
{projectSyncSuccessMessage ? (
|
||||
<Alert bsStyle="success">{projectSyncSuccessMessage}</Alert>
|
||||
<NotificationWrapper
|
||||
type="success"
|
||||
content={projectSyncSuccessMessage}
|
||||
/>
|
||||
) : null}
|
||||
<div className="settings-widgets-container">
|
||||
{allIntegrationLinkingWidgets.map(
|
||||
|
@ -167,9 +170,10 @@ function LinkingSection() {
|
|||
{t('linked_accounts')}
|
||||
</h3>
|
||||
{ssoErrorMessage ? (
|
||||
<Alert bsStyle="danger">
|
||||
{t('sso_link_error')}: {ssoErrorMessage}
|
||||
</Alert>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={`${t('sso_link_error')}: ${ssoErrorMessage}`}
|
||||
/>
|
||||
) : null}
|
||||
<div className="settings-widgets-container">
|
||||
{Object.values(subscriptions).map(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Alert, ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getUserFacingMessage,
|
||||
|
@ -11,6 +11,7 @@ import { ExposedSettings } from '../../../../../types/exposed-settings'
|
|||
import { PasswordStrengthOptions } from '../../../../../types/password-strength-options'
|
||||
import useAsync from '../../../shared/hooks/use-async'
|
||||
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
|
||||
import NotificationWrapper from '@/features/ui/components/bootstrap-5/notification-wrapper'
|
||||
|
||||
type PasswordUpdateResult = {
|
||||
message?: {
|
||||
|
@ -156,41 +157,44 @@ function PasswordForm() {
|
|||
/>
|
||||
{isSuccess && data?.message?.text ? (
|
||||
<FormGroup>
|
||||
<Alert bsStyle="success">{data.message.text}</Alert>
|
||||
<NotificationWrapper type="success" content={data.message.text} />
|
||||
</FormGroup>
|
||||
) : null}
|
||||
{isError ? (
|
||||
<FormGroup>
|
||||
<Alert bsStyle="danger">
|
||||
{getErrorMessageKey(error) === 'password-must-be-strong' ? (
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="password_was_detected_on_a_public_list_of_known_compromised_passwords"
|
||||
components={[
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
<a
|
||||
href="https://haveibeenpwned.com/passwords"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
. {t('use_a_different_password')}.
|
||||
</>
|
||||
) : getErrorMessageKey(error) === 'password-contains-email' ? (
|
||||
<>
|
||||
{t('invalid_password_contains_email')}.{' '}
|
||||
{t('use_a_different_password')}.
|
||||
</>
|
||||
) : getErrorMessageKey(error) === 'password-too-similar' ? (
|
||||
<>
|
||||
{t('invalid_password_too_similar')}.{' '}
|
||||
{t('use_a_different_password')}.
|
||||
</>
|
||||
) : (
|
||||
getUserFacingMessage(error)
|
||||
)}
|
||||
</Alert>
|
||||
<NotificationWrapper
|
||||
type="error"
|
||||
content={
|
||||
getErrorMessageKey(error) === 'password-must-be-strong' ? (
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="password_was_detected_on_a_public_list_of_known_compromised_passwords"
|
||||
components={[
|
||||
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
|
||||
<a
|
||||
href="https://haveibeenpwned.com/passwords"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
. {t('use_a_different_password')}.
|
||||
</>
|
||||
) : getErrorMessageKey(error) === 'password-contains-email' ? (
|
||||
<>
|
||||
{t('invalid_password_contains_email')}.{' '}
|
||||
{t('use_a_different_password')}.
|
||||
</>
|
||||
) : getErrorMessageKey(error) === 'password-too-similar' ? (
|
||||
<>
|
||||
{t('invalid_password_too_similar')}.{' '}
|
||||
{t('use_a_different_password')}.
|
||||
</>
|
||||
) : (
|
||||
getUserFacingMessage(error) ?? ''
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
<ButtonWrapper
|
||||
|
|
|
@ -5,7 +5,7 @@ import classnames from 'classnames'
|
|||
|
||||
type NotificationWrapperProps = React.ComponentProps<typeof Notification> & {
|
||||
bs3Props?: {
|
||||
icon: React.ReactElement
|
||||
icon?: React.ReactElement
|
||||
className?: string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export type NotificationProps = {
|
|||
action?: React.ReactElement
|
||||
ariaLive?: 'polite' | 'off' | 'assertive'
|
||||
className?: string
|
||||
content: React.ReactElement | string
|
||||
content: React.ReactNode
|
||||
customIcon?: React.ReactElement
|
||||
disclaimer?: React.ReactElement | string
|
||||
isDismissible?: boolean
|
||||
|
|
|
@ -196,6 +196,7 @@ tbody > tr.affiliations-table-warning-row > td {
|
|||
}
|
||||
}
|
||||
|
||||
// Should not be migrated to BS5
|
||||
.settings-reconfirm-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -213,6 +214,7 @@ tbody > tr.affiliations-table-warning-row > td {
|
|||
}
|
||||
}
|
||||
|
||||
// Should not be migrated to BS5
|
||||
.setting-reconfirm-info-right {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// will be deprecated once notifications moved to use .notification (see below)
|
||||
flex-grow: 1;
|
||||
width: 90%;
|
||||
@media (min-width: var(--bs-breakpoint-md)) {
|
||||
@include media-breakpoint-up(md) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
|||
// will be deprecated once notifications moved to use .notification (see below)
|
||||
margin-top: calc($line-height-computed / 2); // match paragraph padding
|
||||
order: 1;
|
||||
@media (min-width: var(--bs-breakpoint-md)) {
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 0;
|
||||
order: 0;
|
||||
padding-left: $spacing-05;
|
||||
|
@ -43,7 +43,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: var(--bs-breakpoint-md)) {
|
||||
@include media-breakpoint-up(md) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +167,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: var(--bs-breakpoint-md)) {
|
||||
@include media-breakpoint-up(md) {
|
||||
&:not(.notification-cta-below-content) {
|
||||
.notification-content-and-cta {
|
||||
flex-wrap: nowrap;
|
||||
|
|
|
@ -28,6 +28,9 @@ describe('<ReconfirmationInfo/>', function () {
|
|||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = window.metaAttributesCache || new Map()
|
||||
window.metaAttributesCache.set('ol-ExposedSettings', {
|
||||
samlInitPath: '/saml',
|
||||
})
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
assignStub = sinon.stub()
|
||||
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
|
||||
|
|
Loading…
Reference in a new issue