Merge pull request #18763 from overleaf/ii-bs5-projects-notifications

[web] BS5 notifications in projects and welcome pages

GitOrigin-RevId: 25780bb64660ef41c41c007f94f70df273cac716
This commit is contained in:
ilkin-overleaf 2024-06-20 15:18:22 +03:00 committed by Copybot
parent 34311ce0dc
commit 7b47acc486
35 changed files with 388 additions and 527 deletions

View file

@ -415,22 +415,6 @@ async function projectListPage(req, res, next) {
logger.error({ err: error }, 'Failed to get individual subscription') logger.error({ err: error }, 'Failed to get individual subscription')
} }
let newNotificationStyle
try {
const newNotificationStyleAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'new-notification-style'
)
newNotificationStyle = newNotificationStyleAssignment.variant === 'enabled'
} catch (error) {
logger.error(
{ err: error },
'failed to get "new-notification-style" split test assignment'
)
}
try { try {
await SplitTestHandler.promises.getAssignment(req, res, 'paywall-cta') await SplitTestHandler.promises.getAssignment(req, res, 'paywall-cta')
} catch (error) { } catch (error) {
@ -473,7 +457,6 @@ async function projectListPage(req, res, next) {
groupName: subscription.teamName, groupName: subscription.teamName,
})), })),
hasIndividualRecurlySubscription, hasIndividualRecurlySubscription,
newNotificationStyle,
}) })
} }

View file

@ -35,7 +35,6 @@ block append meta
meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner) meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment) meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription) meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
meta(name="ol-newNotificationStyle" data-type="boolean" content=newNotificationStyle)
meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess) meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess)
block content block content

View file

@ -1,9 +1,9 @@
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ProjectsActionModal from './projects-action-modal' import ProjectsActionModal from './projects-action-modal'
import Icon from '../../../../shared/components/icon'
import ProjectsList from './projects-list' import ProjectsList from './projects-list'
import { isLeavableProject, isDeletableProject } from '../../util/project' import { isLeavableProject, isDeletableProject } from '../../util/project'
import Notification from '@/shared/components/notification'
type DeleteLeaveProjectModalProps = Pick< type DeleteLeaveProjectModalProps = Pick<
React.ComponentProps<typeof ProjectsActionModal>, React.ComponentProps<typeof ProjectsActionModal>,
@ -70,10 +70,10 @@ function DeleteLeaveProjectModal({
projects={projectsToLeave} projects={projectsToLeave}
projectsToDisplay={projectsToLeaveDisplay} projectsToDisplay={projectsToLeaveDisplay}
/> />
<div className="project-action-alert alert alert-warning"> <Notification
<Icon type="exclamation-triangle" fw />{' '} content={t('this_action_cannot_be_undone')}
{t('this_action_cannot_be_undone')} type="warning"
</div> />
</ProjectsActionModal> </ProjectsActionModal>
) )
} }

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ProjectsActionModal from './projects-action-modal' import ProjectsActionModal from './projects-action-modal'
import Icon from '../../../../shared/components/icon'
import ProjectsList from './projects-list' import ProjectsList from './projects-list'
import Notification from '@/shared/components/notification'
type DeleteProjectModalProps = Pick< type DeleteProjectModalProps = Pick<
React.ComponentProps<typeof ProjectsActionModal>, React.ComponentProps<typeof ProjectsActionModal>,
@ -41,10 +41,10 @@ function DeleteProjectModal({
> >
<p>{t('about_to_delete_projects')}</p> <p>{t('about_to_delete_projects')}</p>
<ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} /> <ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} />
<div className="project-action-alert alert alert-warning"> <Notification
<Icon type="exclamation-triangle" fw />{' '} content={t('this_action_cannot_be_undone')}
{t('this_action_cannot_be_undone')} type="warning"
</div> />
</ProjectsActionModal> </ProjectsActionModal>
) )
} }

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ProjectsActionModal from './projects-action-modal' import ProjectsActionModal from './projects-action-modal'
import Icon from '../../../../shared/components/icon'
import ProjectsList from './projects-list' import ProjectsList from './projects-list'
import Notification from '@/shared/components/notification'
type LeaveProjectModalProps = Pick< type LeaveProjectModalProps = Pick<
React.ComponentProps<typeof ProjectsActionModal>, React.ComponentProps<typeof ProjectsActionModal>,
@ -41,10 +41,10 @@ function LeaveProjectModal({
> >
<p>{t('about_to_leave_projects')}</p> <p>{t('about_to_leave_projects')}</p>
<ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} /> <ProjectsList projects={projects} projectsToDisplay={projectsToDisplay} />
<div className="project-action-alert alert alert-warning"> <Notification
<Icon type="exclamation-triangle" fw />{' '} content={t('this_action_cannot_be_undone')}
{t('this_action_cannot_be_undone')} type="warning"
</div> />
</ProjectsActionModal> </ProjectsActionModal>
) )
} }

View file

@ -1,14 +1,18 @@
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { Alert, Modal } from 'react-bootstrap' import { Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Project } from '../../../../../../types/project/dashboard/api' import { Project } from '../../../../../../types/project/dashboard/api'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import { getUserFacingMessage } from '../../../../infrastructure/fetch-json' import { getUserFacingMessage } from '../../../../infrastructure/fetch-json'
import useIsMounted from '../../../../shared/hooks/use-is-mounted' import useIsMounted from '../../../../shared/hooks/use-is-mounted'
import * as eventTracking from '../../../../infrastructure/event-tracking' import * as eventTracking from '../../../../infrastructure/event-tracking'
import { isSmallDevice } from '../../../../infrastructure/event-tracking' import { isSmallDevice } from '../../../../infrastructure/event-tracking'
import getMeta from '@/utils/meta'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
} from '@/features/ui/components/ol/ol-modal'
type ProjectsActionModalProps = { type ProjectsActionModalProps = {
title?: string title?: string
@ -68,64 +72,44 @@ function ProjectsActionModal({
}, [action, showModal]) }, [action, showModal])
return ( return (
<AccessibleModal <OLModal
animation animation
show={showModal} show={showModal}
onHide={handleCloseModal} onHide={handleCloseModal}
id="action-project-modal" id="action-project-modal"
backdrop="static" backdrop="static"
> >
<Modal.Header closeButton> <OLModalHeader closeButton>
<Modal.Title>{title}</Modal.Title> <Modal.Title>{title}</Modal.Title>
</Modal.Header> </OLModalHeader>
<Modal.Body>{children}</Modal.Body> <OLModalBody>
<Modal.Footer> {children}
{!isProcessing && {!isProcessing &&
errors.length > 0 && errors.length > 0 &&
errors.map((e, i) => <ErrorNotification error={e} key={i} />)} errors.map((error, i) => (
<button className="btn btn-secondary" onClick={handleCloseModal}> <div className="notification-list" key={i}>
{t('cancel')}
</button>
<button
className="btn btn-danger"
onClick={() => handleActionForProjects(projects)}
disabled={isProcessing}
>
{t('confirm')}
</button>
</Modal.Footer>
</AccessibleModal>
)
}
type ErrorNotificationProps = {
error: any
}
function ErrorNotification({ error }: ErrorNotificationProps) {
const newNotificationStyle = getMeta('ol-newNotificationStyle')
if (newNotificationStyle) {
return (
// `notification-list` sets the margin-bottom correctly also when used individually in each notification.
// Once the legacy alerts are cleaned up we should move the styled div up to the notification list container.
<div className="notification-list">
<Notification <Notification
type="error" type="error"
title={error.projectName} title={error.projectName}
content={getUserFacingMessage(error.error) as string} content={getUserFacingMessage(error.error) as string}
/> />
</div> </div>
))}
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={handleCloseModal}>
{t('cancel')}
</OLButton>
<OLButton
variant="danger"
onClick={() => handleActionForProjects(projects)}
disabled={isProcessing}
>
{t('confirm')}
</OLButton>
</OLModalFooter>
</OLModal>
) )
} else {
return (
<Alert bsStyle="danger" className="text-center" aria-live="polite">
<b>{error.projectName}</b>
<br />
{getUserFacingMessage(error.error)}
</Alert>
)
}
} }
export default memo(ProjectsActionModal) export default memo(ProjectsActionModal)

View file

@ -1,6 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { import {
Alert,
Button, Button,
ControlLabel, ControlLabel,
FormControl, FormControl,
@ -18,7 +17,6 @@ import { getUserFacingMessage } from '../../../../infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { isSmallDevice } from '../../../../infrastructure/event-tracking' import { isSmallDevice } from '../../../../infrastructure/event-tracking'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import getMeta from '@/utils/meta'
type RenameProjectModalProps = { type RenameProjectModalProps = {
handleCloseModal: () => void handleCloseModal: () => void
@ -36,7 +34,6 @@ function RenameProjectModal({
const { error, isError, isLoading, runAsync } = useAsync() const { error, isError, isLoading, runAsync } = useAsync()
const { toggleSelectedProject, updateProjectViewData } = const { toggleSelectedProject, updateProjectViewData } =
useProjectListContext() useProjectListContext()
const newNotificationStyle = getMeta('ol-newNotificationStyle')
useEffect(() => { useEffect(() => {
if (showModal) { if (showModal) {
@ -99,19 +96,14 @@ function RenameProjectModal({
<Modal.Title>{t('rename_project')}</Modal.Title> <Modal.Title>{t('rename_project')}</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{isError && {isError && (
(newNotificationStyle ? (
<div className="notification-list"> <div className="notification-list">
<Notification <Notification
type="error" type="error"
content={getUserFacingMessage(error) as string} content={getUserFacingMessage(error) as string}
/> />
</div> </div>
) : ( )}
<Alert bsStyle="danger" className="text-center" aria-live="polite">
{getUserFacingMessage(error)}
</Alert>
))}
<form id="rename-project-form" onSubmit={handleSubmit}> <form id="rename-project-form" onSubmit={handleSubmit}>
<FormGroup> <FormGroup>
<ControlLabel htmlFor="rename-project-form-name"> <ControlLabel htmlFor="rename-project-form-name">

View file

@ -1,5 +1,4 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { Alert } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useAsync from '../../../../shared/hooks/use-async' import useAsync from '../../../../shared/hooks/use-async'
import { import {
@ -8,7 +7,6 @@ import {
} from '../../../../infrastructure/fetch-json' } from '../../../../infrastructure/fetch-json'
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
import { useLocation } from '../../../../shared/hooks/use-location' import { useLocation } from '../../../../shared/hooks/use-location'
import getMeta from '@/utils/meta'
import Notification from '@/shared/components/notification' import Notification from '@/shared/components/notification'
import { import {
OLModalBody, OLModalBody,
@ -42,7 +40,6 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
const [projectName, setProjectName] = useState('') const [projectName, setProjectName] = useState('')
const { isLoading, isError, error, runAsync } = useAsync<NewProjectData>() const { isLoading, isError, error, runAsync } = useAsync<NewProjectData>()
const location = useLocation() const location = useLocation()
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const createNewProject = () => { const createNewProject = () => {
runAsync( runAsync(
@ -77,17 +74,14 @@ function ModalContentNewProjectForm({ onCancel, template = 'none' }: Props) {
</OLModalHeader> </OLModalHeader>
<OLModalBody> <OLModalBody>
{isError && {isError && (
(newNotificationStyle ? (
<div className="notification-list"> <div className="notification-list">
<Notification <Notification
type="error" type="error"
content={getUserFacingMessage(error) as string} content={getUserFacingMessage(error) as string}
/> />
</div> </div>
) : ( )}
<Alert bsStyle="danger">{getUserFacingMessage(error)}</Alert>
))}
<OLForm onSubmit={handleSubmit}> <OLForm onSubmit={handleSubmit}>
<OLFormControl <OLFormControl
type="text" type="text"

View file

@ -2,6 +2,7 @@ import { memo, useEffect, useState } from 'react'
import Notification from './notification' import Notification from './notification'
import customLocalStorage from '@/infrastructure/local-storage' import customLocalStorage from '@/infrastructure/local-storage'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import OLButton from '@/features/ui/components/ol/ol-button'
function AccessibilitySurveyBanner() { function AccessibilitySurveyBanner() {
const { t } = useTranslation() const { t } = useTranslation()
@ -27,18 +28,18 @@ function AccessibilitySurveyBanner() {
return ( return (
<Notification <Notification
className="sr-only" className="sr-only"
bsStyle="info" type="info"
onDismiss={handleClose} onDismiss={handleClose}
body={<p>{t('help_improve_screen_reader_fill_out_this_survey')}</p>} content={<p>{t('help_improve_screen_reader_fill_out_this_survey')}</p>}
action={ action={
<a <OLButton
className="btn btn-secondary btn-sm pull-right btn-info" variant="secondary"
href="https://docs.google.com/forms/d/e/1FAIpQLSdxKP_biRXvrkmJzlBjMwI_qPSuv4NbBvYUzSOc3OOTIOTmnQ/viewform" href="https://docs.google.com/forms/d/e/1FAIpQLSdxKP_biRXvrkmJzlBjMwI_qPSuv4NbBvYUzSOc3OOTIOTmnQ/viewform"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
{t('take_survey')} {t('take_survey')}
</a> </OLButton>
} }
/> />
) )

View file

@ -1,9 +0,0 @@
import classnames from 'classnames'
function Action({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div className={classnames('notification-action', className)} {...props} />
)
}
export default Action

View file

@ -1,9 +0,0 @@
import classnames from 'classnames'
function Body({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div className={classnames('notification-body', className)} {...props} />
)
}
export default Body

View file

@ -9,6 +9,7 @@ import {
GroupsAndEnterpriseBannerVariant, GroupsAndEnterpriseBannerVariant,
GroupsAndEnterpriseBannerVariants, GroupsAndEnterpriseBannerVariants,
} from '../../../../../../types/project/dashboard/notification' } from '../../../../../../types/project/dashboard/notification'
import OLButton from '@/features/ui/components/ol/ol-button'
type urlForVariantsType = { type urlForVariantsType = {
[key in GroupsAndEnterpriseBannerVariant]: string // eslint-disable-line no-unused-vars [key in GroupsAndEnterpriseBannerVariant]: string // eslint-disable-line no-unused-vars
@ -31,7 +32,6 @@ export default function GroupsAndEnterpriseBanner() {
const groupsAndEnterpriseBannerVariant = getMeta( const groupsAndEnterpriseBannerVariant = getMeta(
'ol-groupsAndEnterpriseBannerVariant' 'ol-groupsAndEnterpriseBannerVariant'
) )
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const hasDismissedGroupsAndEnterpriseBanner = hasRecentlyDismissedBanner() const hasDismissedGroupsAndEnterpriseBanner = hasRecentlyDismissedBanner()
@ -73,23 +73,19 @@ export default function GroupsAndEnterpriseBanner() {
return ( return (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={handleClose} onDismiss={handleClose}
body={<BannerContent variant={groupsAndEnterpriseBannerVariant} />} content={<BannerContent variant={groupsAndEnterpriseBannerVariant} />}
action={ action={
<a <OLButton
className={ variant="secondary"
newNotificationStyle
? 'btn btn-secondary btn-sm'
: 'pull-right btn btn-info btn-sm'
}
href={contactSalesUrl} href={contactSalesUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={handleClickContact} onClick={handleClickContact}
> >
{t('contact_sales')} {t('contact_sales')}
</a> </OLButton>
} }
/> />
) )

View file

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Icon from '../../../../../../shared/components/icon' import Icon from '../../../../../../shared/components/icon'
import getMeta from '../../../../../../utils/meta' import getMeta from '../../../../../../utils/meta'
import useAsync from '../../../../../../shared/hooks/use-async' import useAsync from '../../../../../../shared/hooks/use-async'
@ -12,6 +11,8 @@ import { UserEmailData } from '../../../../../../../../types/user-email'
import { Institution } from '../../../../../../../../types/institution' import { Institution } from '../../../../../../../../types/institution'
import { useLocation } from '../../../../../../shared/hooks/use-location' import { useLocation } from '../../../../../../shared/hooks/use-location'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import OLButton from '@/features/ui/components/ol/ol-button'
import Notification from '@/features/project-list/components/notifications/notification'
type ReconfirmAffiliationProps = { type ReconfirmAffiliationProps = {
email: UserEmailData['email'] email: UserEmailData['email']
@ -24,7 +25,6 @@ function ReconfirmAffiliation({
}: ReconfirmAffiliationProps) { }: ReconfirmAffiliationProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { samlInitPath } = getMeta('ol-ExposedSettings') const { samlInitPath } = getMeta('ol-ExposedSettings')
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const { error, isLoading, isError, isSuccess, runAsync } = useAsync() const { error, isLoading, isError, isSuccess, runAsync } = useAsync()
const [hasSent, setHasSent] = useState(false) const [hasSent, setHasSent] = useState(false)
const [isPending, setIsPending] = useState(false) const [isPending, setIsPending] = useState(false)
@ -57,7 +57,10 @@ function ReconfirmAffiliation({
if (hasSent) { if (hasSent) {
return ( return (
<div className="w-100"> <Notification
type="info"
content={
<>
<Trans <Trans
i18nKey="please_check_your_inbox_to_confirm" i18nKey="please_check_your_inbox_to_confirm"
components={[<b />]} // eslint-disable-line react/jsx-key components={[<b />]} // eslint-disable-line react/jsx-key
@ -66,19 +69,6 @@ function ReconfirmAffiliation({
tOptions={{ interpolation: { escapeValue: true } }} tOptions={{ interpolation: { escapeValue: true } }}
/> />
&nbsp; &nbsp;
{isLoading ? (
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : (
<Button
className="btn-inline-link"
disabled={isLoading}
onClick={handleRequestReconfirmation}
>
{t('resend_confirmation_email')}
</Button>
)}
{isError && ( {isError && (
<> <>
<br /> <br />
@ -89,28 +79,35 @@ function ReconfirmAffiliation({
</div> </div>
</> </>
)} )}
</div> </>
}
action={
<OLButton
variant="link"
onClick={handleRequestReconfirmation}
className="btn-inline-link"
disabled={isLoading}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? (
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : null,
}}
>
{t('resend_confirmation_email')}
</OLButton>
}
/>
) )
} }
return ( return (
<div className="w-100"> <Notification
{!newNotificationStyle && <Icon type="warning" />} type="info"
<Button content={
bsStyle="info"
bsSize="sm"
className="btn-reconfirm"
onClick={handleRequestReconfirmation}
disabled={isLoading || isPending}
>
{isLoading ? (
<> <>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : (
t('confirm_affiliation')
)}
</Button>
<Trans <Trans
i18nKey="are_you_still_at" i18nKey="are_you_still_at"
components={[<b />]} // eslint-disable-line react/jsx-key components={[<b />]} // eslint-disable-line react/jsx-key
@ -141,7 +138,27 @@ function ReconfirmAffiliation({
</div> </div>
</> </>
)} )}
</div> </>
}
action={
<OLButton
variant="secondary"
bs3Props={{
loading:
isLoading || isPending ? (
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : null,
}}
isLoading={isLoading || isPending}
disabled={isLoading || isPending}
onClick={handleRequestReconfirmation}
>
{t('confirm_affiliation')}
</OLButton>
}
/>
) )
} }

View file

@ -12,17 +12,10 @@ function ReconfirmationInfo() {
<> <>
{allInReconfirmNotificationPeriods.map(userEmail => {allInReconfirmNotificationPeriods.map(userEmail =>
userEmail.affiliation?.institution ? ( userEmail.affiliation?.institution ? (
<Notification
key={`reconfirmation-period-email-${userEmail.email}`}
bsStyle="info"
body={
<div className="reconfirm-notification">
<ReconfirmAffiliation <ReconfirmAffiliation
email={userEmail.email} email={userEmail.email}
institution={userEmail.affiliation.institution} institution={userEmail.affiliation.institution}
/> key={`reconfirmation-period-email-${userEmail.email}`}
</div>
}
/> />
) : null ) : null
)} )}
@ -31,9 +24,9 @@ function ReconfirmationInfo() {
userEmail.affiliation?.institution ? ( userEmail.affiliation?.institution ? (
<Notification <Notification
key={`samlIdentifier-email-${userEmail.email}`} key={`samlIdentifier-email-${userEmail.email}`}
bsStyle="info" type="info"
onDismiss={() => {}} onDismiss={() => {}}
body={ content={
<ReconfirmationInfoSuccess <ReconfirmationInfoSuccess
institution={userEmail.affiliation?.institution} institution={userEmail.affiliation?.institution}
/> />

View file

@ -1,5 +1,4 @@
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Notification from '../notification' import Notification from '../notification'
import Icon from '../../../../../shared/components/icon' import Icon from '../../../../../shared/components/icon'
import getMeta from '../../../../../utils/meta' import getMeta from '../../../../../utils/meta'
@ -13,6 +12,7 @@ import {
import GroupInvitationNotification from './group-invitation/group-invitation' import GroupInvitationNotification from './group-invitation/group-invitation'
import IEEERetirementBanner from '../ieee-retirement-banner' import IEEERetirementBanner from '../ieee-retirement-banner'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import OLButton from '@/features/ui/components/ol/ol-button'
function Common() { function Common() {
const notifications = getMeta('ol-notifications') || [] const notifications = getMeta('ol-notifications') || []
@ -37,7 +37,6 @@ function CommonNotification({ notification }: CommonNotificationProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { samlInitPath } = getMeta('ol-ExposedSettings') const { samlInitPath } = getMeta('ol-ExposedSettings')
const user = getMeta('ol-user') const user = getMeta('ol-user')
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const { isLoading, isSuccess, error, runAsync } = useAsync< const { isLoading, isSuccess, error, runAsync } = useAsync<
never, never,
FetchError FetchError
@ -63,9 +62,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
<> <>
{templateKey === 'notification_project_invite' ? ( {templateKey === 'notification_project_invite' ? (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
accepted ? ( accepted ? (
<Trans <Trans
i18nKey="notification_project_invite_accepted_message" i18nKey="notification_project_invite_accepted_message"
@ -89,42 +88,36 @@ function CommonNotification({ notification }: CommonNotificationProps) {
} }
action={ action={
accepted ? ( accepted ? (
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
bsSize="sm"
className={
newNotificationStyle ? 'btn-secondary' : 'pull-right'
}
href={`/project/${notification.messageOpts.projectId}`} href={`/project/${notification.messageOpts.projectId}`}
> >
{t('open_project')} {t('open_project')}
</Button> </OLButton>
) : ( ) : (
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
className={ bs3Props={{
newNotificationStyle ? 'btn-secondary' : 'pull-right' loading: isLoading ? (
}
bsSize="sm"
disabled={isLoading}
onClick={() => handleAcceptInvite(notification)}
>
{isLoading ? (
<> <>
<Icon type="spinner" spin /> {t('joining')}&hellip; <Icon type="spinner" spin /> {t('joining')}&hellip;
</> </>
) : ( ) : null,
t('join_project') }}
)} isLoading={isLoading}
</Button> disabled={isLoading}
onClick={() => handleAcceptInvite(notification)}
>
{t('join_project')}
</OLButton>
) )
} }
/> />
) : templateKey === 'wfh_2020_upgrade_offer' ? ( ) : templateKey === 'wfh_2020_upgrade_offer' ? (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
Important notice: Your free WFH2020 upgrade came to an end on June Important notice: Your free WFH2020 upgrade came to an end on June
30th 2020. We're still providing a number of special initiatives 30th 2020. We're still providing a number of special initiatives
@ -132,21 +125,19 @@ function CommonNotification({ notification }: CommonNotificationProps) {
</> </>
} }
action={ action={
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
bsSize="sm"
className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
href="https://www.overleaf.com/events/wfh2020" href="https://www.overleaf.com/events/wfh2020"
> >
View View
</Button> </OLButton>
} }
/> />
) : templateKey === 'notification_ip_matched_affiliation' ? ( ) : templateKey === 'notification_ip_matched_affiliation' ? (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
<Trans <Trans
i18nKey="looks_like_youre_at" i18nKey="looks_like_youre_at"
@ -193,10 +184,8 @@ function CommonNotification({ notification }: CommonNotificationProps) {
</> </>
} }
action={ action={
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
bsSize="sm"
className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
href={ href={
notification.messageOpts.ssoEnabled notification.messageOpts.ssoEnabled
? `${samlInitPath}?university_id=${notification.messageOpts.institutionId}&auto=/project` ? `${samlInitPath}?university_id=${notification.messageOpts.institutionId}&auto=/project`
@ -206,14 +195,14 @@ function CommonNotification({ notification }: CommonNotificationProps) {
{notification.messageOpts.ssoEnabled {notification.messageOpts.ssoEnabled
? t('link_account') ? t('link_account')
: t('add_affiliation')} : t('add_affiliation')}
</Button> </OLButton>
} }
/> />
) : templateKey === 'notification_tpds_file_limit' ? ( ) : templateKey === 'notification_tpds_file_limit' ? (
<Notification <Notification
bsStyle="danger" type="error"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
Error: Your project {notification.messageOpts.projectName} has Error: Your project {notification.messageOpts.projectName} has
gone over the 2000 file limit using an integration (e.g. Dropbox gone over the 2000 file limit using an integration (e.g. Dropbox
@ -223,21 +212,16 @@ function CommonNotification({ notification }: CommonNotificationProps) {
</> </>
} }
action={ action={
<Button <OLButton variant="secondary" href="/user/settings">
bsStyle={newNotificationStyle ? null : 'danger'}
bsSize="sm"
className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
href="/user/settings"
>
Account Settings Account Settings
</Button> </OLButton>
} }
/> />
) : templateKey === 'notification_dropbox_duplicate_project_names' ? ( ) : templateKey === 'notification_dropbox_duplicate_project_names' ? (
<Notification <Notification
bsStyle="warning" type="warning"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
<p> <p>
<Trans <Trans
@ -267,9 +251,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
) : templateKey === ) : templateKey ===
'notification_dropbox_unlinked_due_to_lapsed_reconfirmation' ? ( 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation' ? (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
<Trans <Trans
i18nKey="dropbox_unlinked_premium_feature" i18nKey="dropbox_unlinked_premium_feature"
@ -299,9 +283,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
<IEEERetirementBanner id={id} /> <IEEERetirementBanner id={id} />
) : templateKey === 'notification_personal_and_group_subscriptions' ? ( ) : templateKey === 'notification_personal_and_group_subscriptions' ? (
<Notification <Notification
bsStyle="warning" type="warning"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<Trans <Trans
i18nKey="notification_personal_and_group_subscriptions" i18nKey="notification_personal_and_group_subscriptions"
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */ /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
@ -311,9 +295,9 @@ function CommonNotification({ notification }: CommonNotificationProps) {
/> />
) : ( ) : (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={html} content={html}
/> />
)} )}
</> </>

View file

@ -1,5 +1,4 @@
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Notification from '../notification' import Notification from '../notification'
import Icon from '../../../../../shared/components/icon' import Icon from '../../../../../shared/components/icon'
import getMeta from '../../../../../utils/meta' import getMeta from '../../../../../utils/meta'
@ -11,6 +10,7 @@ import {
} from '../../../../../infrastructure/fetch-json' } from '../../../../../infrastructure/fetch-json'
import { UserEmailData } from '../../../../../../../types/user-email' import { UserEmailData } from '../../../../../../../types/user-email'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import OLButton from '@/features/ui/components/ol/ol-button'
const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => { const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => {
const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings') const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings')
@ -96,8 +96,8 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
if (emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()) { if (emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan()) {
return ( return (
<Notification <Notification
bsStyle="info" type="info"
body={ content={
<div data-testid="notification-body"> <div data-testid="notification-body">
{isLoading ? ( {isLoading ? (
<> <>
@ -112,12 +112,6 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
i18nKey="one_step_away_from_professional_features" i18nKey="one_step_away_from_professional_features"
components={[<strong />]} // eslint-disable-line react/jsx-key components={[<strong />]} // eslint-disable-line react/jsx-key
/> />
<button
className="pull-right btn btn-info btn-sm"
onClick={() => handleResendConfirmationEmail(userEmail)}
>
{t('resend_email')}
</button>
<br /> <br />
<Trans <Trans
i18nKey="institution_has_overleaf_subscription" i18nKey="institution_has_overleaf_subscription"
@ -133,14 +127,22 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
)} )}
</div> </div>
} }
action={
<OLButton
variant="secondary"
onClick={() => handleResendConfirmationEmail(userEmail)}
>
{t('resend_email')}
</OLButton>
}
/> />
) )
} }
return ( return (
<Notification <Notification
bsStyle="warning" type="warning"
body={ content={
<div data-testid="pro-notification-body"> <div data-testid="pro-notification-body">
{isLoading ? ( {isLoading ? (
<> <>
@ -154,13 +156,13 @@ function ConfirmEmailNotification({ userEmail }: { userEmail: UserEmailData }) {
{t('please_confirm_email', { {t('please_confirm_email', {
emailAddress: userEmail.email, emailAddress: userEmail.email,
})}{' '} })}{' '}
<Button <OLButton
bsStyle="link" variant="link"
className="btn-inline-link"
onClick={() => handleResendConfirmationEmail(userEmail)} onClick={() => handleResendConfirmationEmail(userEmail)}
className="btn-inline-link"
> >
({t('resend_confirmation_email')}) {t('resend_confirmation_email')}
</Button> </OLButton>
</> </>
)} )}
</div> </div>

View file

@ -1,10 +1,9 @@
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Dispatch, SetStateAction } from 'react' import type { Dispatch, SetStateAction } from 'react'
import Notification from '../../notification' import Notification from '../../notification'
import { GroupInvitationStatus } from './hooks/use-group-invitation-notification' import { GroupInvitationStatus } from './hooks/use-group-invitation-notification'
import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification' import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification'
import getMeta from '@/utils/meta' import OLButton from '@/features/ui/components/ol/ol-button'
type GroupInvitationCancelIndividualSubscriptionNotificationProps = { type GroupInvitationCancelIndividualSubscriptionNotificationProps = {
setGroupInvitationStatus: Dispatch<SetStateAction<GroupInvitationStatus>> setGroupInvitationStatus: Dispatch<SetStateAction<GroupInvitationStatus>>
@ -20,37 +19,32 @@ export default function GroupInvitationCancelIndividualSubscriptionNotification(
notification, notification,
}: GroupInvitationCancelIndividualSubscriptionNotificationProps) { }: GroupInvitationCancelIndividualSubscriptionNotificationProps) {
const { t } = useTranslation() const { t } = useTranslation()
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const { const {
messageOpts: { inviterName }, messageOpts: { inviterName },
} = notification } = notification
return ( return (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={dismissGroupInviteNotification} onDismiss={dismissGroupInviteNotification}
body={t('invited_to_group_have_individual_subcription', { inviterName })} content={t('invited_to_group_have_individual_subcription', {
inviterName,
})}
action={ action={
<div className="group-invitation-cancel-subscription-notification-buttons"> <>
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
bsSize="sm"
className={newNotificationStyle ? 'me-1 btn-secondary' : 'me-1'}
onClick={() => onClick={() =>
setGroupInvitationStatus(GroupInvitationStatus.AskToJoin) setGroupInvitationStatus(GroupInvitationStatus.AskToJoin)
} }
className="me-1"
> >
{t('not_now')} {t('not_now')}
</Button> </OLButton>
<Button <OLButton variant="secondary" onClick={cancelPersonalSubscription}>
bsStyle={newNotificationStyle ? null : 'info'}
className={newNotificationStyle ? 'btn-secondary' : ''}
bsSize="sm"
onClick={cancelPersonalSubscription}
>
{t('cancel_my_subscription')} {t('cancel_my_subscription')}
</Button> </OLButton>
</div> </>
} }
/> />
) )

View file

@ -1,8 +1,7 @@
import { Button } from 'react-bootstrap'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import Notification from '../../notification' import Notification from '../../notification'
import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification' import type { NotificationGroupInvitation } from '../../../../../../../../types/project/dashboard/notification'
import getMeta from '@/utils/meta' import OLButton from '@/features/ui/components/ol/ol-button'
type GroupInvitationNotificationProps = { type GroupInvitationNotificationProps = {
acceptGroupInvite: () => void acceptGroupInvite: () => void
@ -18,16 +17,15 @@ export default function GroupInvitationNotificationJoin({
dismissGroupInviteNotification, dismissGroupInviteNotification,
}: GroupInvitationNotificationProps) { }: GroupInvitationNotificationProps) {
const { t } = useTranslation() const { t } = useTranslation()
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const { const {
messageOpts: { inviterName }, messageOpts: { inviterName },
} = notification } = notification
return ( return (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={dismissGroupInviteNotification} onDismiss={dismissGroupInviteNotification}
body={ content={
<Trans <Trans
i18nKey="invited_to_group" i18nKey="invited_to_group"
values={{ inviterName }} values={{ inviterName }}
@ -40,15 +38,13 @@ export default function GroupInvitationNotificationJoin({
/> />
} }
action={ action={
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
bsSize="sm"
className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
onClick={acceptGroupInvite} onClick={acceptGroupInvite}
disabled={isAcceptingInvitation} disabled={isAcceptingInvitation}
> >
{t('join_now')} {t('join_now')}
</Button> </OLButton>
} }
/> />
) )

View file

@ -1,7 +1,5 @@
import Notification from '../../notification' import Notification from '../../notification'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../../../../shared/components/icon'
import getMeta from '@/utils/meta'
type GroupInvitationSuccessfulNotificationProps = { type GroupInvitationSuccessfulNotificationProps = {
hideNotification: () => void hideNotification: () => void
@ -11,20 +9,12 @@ export default function GroupInvitationSuccessfulNotification({
hideNotification, hideNotification,
}: GroupInvitationSuccessfulNotificationProps) { }: GroupInvitationSuccessfulNotificationProps) {
const { t } = useTranslation() const { t } = useTranslation()
const newNotificationStyle = getMeta('ol-newNotificationStyle')
return ( return (
<Notification <Notification
bsStyle="success" type="success"
onDismiss={hideNotification} onDismiss={hideNotification}
body={ content={t('congratulations_youve_successfully_join_group')}
<>
{!newNotificationStyle && (
<Icon type="check-circle" fw aria-hidden="true" className="me-1" />
)}
{t('congratulations_youve_successfully_join_group')}
</>
}
/> />
) )
} }

View file

@ -1,16 +1,14 @@
import { Fragment } from 'react' import { Fragment } from 'react'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Notification from '../notification' import Notification from '../notification'
import Icon from '../../../../../shared/components/icon'
import getMeta from '../../../../../utils/meta' import getMeta from '../../../../../utils/meta'
import useAsyncDismiss from '../hooks/useAsyncDismiss' import useAsyncDismiss from '../hooks/useAsyncDismiss'
import OLButton from '@/features/ui/components/ol/ol-button'
function Institution() { function Institution() {
const { t } = useTranslation() const { t } = useTranslation()
const { samlInitPath, appName } = getMeta('ol-ExposedSettings') const { samlInitPath, appName } = getMeta('ol-ExposedSettings')
const notificationsInstitution = getMeta('ol-notificationsInstitution') || [] const notificationsInstitution = getMeta('ol-notificationsInstitution') || []
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const { handleDismiss } = useAsyncDismiss() const { handleDismiss } = useAsyncDismiss()
if (!notificationsInstitution.length) { if (!notificationsInstitution.length) {
@ -36,10 +34,9 @@ function Institution() {
<Fragment key={index}> <Fragment key={index}>
{templateKey === 'notification_institution_sso_available' && ( {templateKey === 'notification_institution_sso_available' && (
<Notification <Notification
bsStyle="info" type="info"
body={ content={
<> <>
{' '}
<p> <p>
<Trans <Trans
i18nKey="can_link_institution_email_acct_to_institution_acct" i18nKey="can_link_institution_email_acct_to_institution_acct"
@ -67,22 +64,20 @@ function Institution() {
</> </>
} }
action={ action={
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
className={newNotificationStyle ? 'btn-secondary' : ''}
bsSize="sm"
href={`${samlInitPath}?university_id=${institutionId}&auto=/project&email=${email}`} href={`${samlInitPath}?university_id=${institutionId}&auto=/project&email=${email}`}
> >
{t('link_account')} {t('link_account')}
</Button> </OLButton>
} }
/> />
)} )}
{templateKey === 'notification_institution_sso_linked' && ( {templateKey === 'notification_institution_sso_linked' && (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<Trans <Trans
i18nKey="account_has_been_link_to_institution_account" i18nKey="account_has_been_link_to_institution_account"
components={{ b: <b /> }} components={{ b: <b /> }}
@ -95,15 +90,10 @@ function Institution() {
)} )}
{templateKey === 'notification_institution_sso_non_canonical' && ( {templateKey === 'notification_institution_sso_non_canonical' && (
<Notification <Notification
bsStyle="warning" type="warning"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
{!newNotificationStyle && (
<>
<Icon type="exclamation-triangle" fw />{' '}
</>
)}
<Trans <Trans
i18nKey="tried_to_log_in_with_email" i18nKey="tried_to_log_in_with_email"
components={{ b: <b /> }} components={{ b: <b /> }}
@ -125,9 +115,9 @@ function Institution() {
{templateKey === {templateKey ===
'notification_institution_sso_already_registered' && ( 'notification_institution_sso_already_registered' && (
<Notification <Notification
bsStyle="info" type="info"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
<Trans <Trans
i18nKey="tried_to_register_with_email" i18nKey="tried_to_register_with_email"
@ -140,29 +130,22 @@ function Institution() {
</> </>
} }
action={ action={
<Button <OLButton
bsStyle={newNotificationStyle ? null : 'info'} variant="secondary"
className={newNotificationStyle ? 'btn-secondary' : ''}
bsSize="sm"
href="/learn/how-to/Institutional_Login" href="/learn/how-to/Institutional_Login"
target="_blank" target="_blank"
> >
{t('find_out_more')} {t('find_out_more')}
</Button> </OLButton>
} }
/> />
)} )}
{templateKey === 'notification_institution_sso_error' && ( {templateKey === 'notification_institution_sso_error' && (
<Notification <Notification
bsStyle="danger" type="error"
onDismiss={() => id && handleDismiss(id)} onDismiss={() => id && handleDismiss(id)}
body={ content={
<> <>
{!newNotificationStyle && (
<>
<Icon type="exclamation-triangle" fw />{' '}
</>
)}
{t('generic_something_went_wrong')}. {t('generic_something_went_wrong')}.
<div> <div>
{error?.translatedMessage {error?.translatedMessage

View file

@ -49,10 +49,9 @@ export default function IEEERetirementBanner({
return ( return (
<Notification <Notification
bsStyle="warning" type="warning"
onDismiss={handleClose} onDismiss={handleClose}
newNotificationStyle content={
body={
<Trans <Trans
i18nKey="notification_ieee_collabratec_retirement_message" i18nKey="notification_ieee_collabratec_retirement_message"
components={[ components={[

View file

@ -1,109 +1,21 @@
import { useState } from 'react'
import { Alert, AlertProps } from 'react-bootstrap'
import Body from './body'
import Action from './action'
import Close from '../../../../shared/components/close'
import classnames from 'classnames' import classnames from 'classnames'
import NewNotification, { import NewNotification from '@/shared/components/notification'
NotificationType,
} from '@/shared/components/notification'
import getMeta from '@/utils/meta'
type NotificationProps = { type NotificationProps = Pick<
bsStyle: AlertProps['bsStyle'] React.ComponentProps<typeof NewNotification>,
children?: React.ReactNode 'type' | 'action' | 'content' | 'onDismiss' | 'className'
body?: React.ReactNode >
action?: React.ReactElement
onDismiss?: AlertProps['onDismiss']
className?: string
newNotificationStyle?: boolean
}
/** function Notification({ className, ...props }: NotificationProps) {
* Renders either a legacy-styled notification using Boostrap `Alert`, or a new-styled notification using const notificationComponent = (
* the shared `Notification` component. <NewNotification isDismissible={props.onDismiss != null} {...props} />
* )
* The content of the notification is provided either with `children` (keeping backwards compatibility),
* or a `body` prop (along with an optional `action`).
*
* When the content is provided via `body` prop the notification is rendered with the new Notification component
* if `ol-newNotificationStyle` meta is set to true.
*/
function Notification({
bsStyle,
children,
onDismiss,
className,
body,
action,
newNotificationStyle,
...props
}: NotificationProps) {
newNotificationStyle =
newNotificationStyle ?? getMeta('ol-newNotificationStyle')
const [show, setShow] = useState(true) return notificationComponent ? (
const handleDismiss = () => {
if (onDismiss) {
onDismiss()
}
setShow(false)
}
if (!show) {
return null
}
if (newNotificationStyle && body) {
const newNotificationType = (
bsStyle === 'danger' ? 'error' : bsStyle
) as NotificationType
return (
<li className={classnames('notification-entry', className)}> <li className={classnames('notification-entry', className)}>
<NewNotification {notificationComponent}
type={newNotificationType}
isDismissible={onDismiss != null}
onDismiss={handleDismiss}
content={body as React.ReactElement}
action={action}
/>
</li> </li>
) ) : null
}
if (body) {
return (
<li className={classnames('notification-entry', className)} {...props}>
<Alert bsStyle={bsStyle}>
<Body>{body}</Body>
{action && <Action>{action}</Action>}
{onDismiss ? (
<div className="notification-close">
<Close onDismiss={handleDismiss} />
</div>
) : null}
</Alert>
</li>
)
} else {
return (
<li className={classnames('notification-entry', className)} {...props}>
<Alert bsStyle={bsStyle}>
{children}
{onDismiss ? (
<div className="notification-close">
<Close onDismiss={handleDismiss} />
</div>
) : null}
</Alert>
</li>
)
}
} }
Notification.Body = Body
Notification.Action = Action
export default Notification export default Notification

View file

@ -10,7 +10,6 @@ import getMeta from '../../../../utils/meta'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import customLocalStorage from '../../../../infrastructure/local-storage' import customLocalStorage from '../../../../infrastructure/local-storage'
import { sendMB } from '../../../../infrastructure/event-tracking' import { sendMB } from '../../../../infrastructure/event-tracking'
import classNames from 'classnames'
import GeoBanners from './geo-banners' import GeoBanners from './geo-banners'
import AccessibilitySurveyBanner from './accessibility-survey-banner' import AccessibilitySurveyBanner from './accessibility-survey-banner'
@ -23,7 +22,6 @@ const EnrollmentNotification: JSXElementConstructor<{
}> = enrollmentNotificationModule?.import.default }> = enrollmentNotificationModule?.import.default
function UserNotifications() { function UserNotifications() {
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const groupSubscriptionsPendingEnrollment = const groupSubscriptionsPendingEnrollment =
getMeta('ol-groupSubscriptionsPendingEnrollment') || [] getMeta('ol-groupSubscriptionsPendingEnrollment') || []
const user = getMeta('ol-user') const user = getMeta('ol-user')
@ -54,11 +52,7 @@ function UserNotifications() {
const [dismissedWritefull, setDismissedWritefull] = useState(false) const [dismissedWritefull, setDismissedWritefull] = useState(false)
return ( return (
<div <div className="user-notifications notification-list">
className={classNames('user-notifications', {
'notification-list': newNotificationStyle,
})}
>
<ul className="list-unstyled"> <ul className="list-unstyled">
{EnrollmentNotification && {EnrollmentNotification &&
groupSubscriptionsPendingEnrollment.map(subscription => ( groupSubscriptionsPendingEnrollment.map(subscription => (

View file

@ -3,6 +3,7 @@ import Notification from './notification'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import customLocalStorage from '@/infrastructure/local-storage' import customLocalStorage from '@/infrastructure/local-storage'
import WritefullLogo from '@/shared/svgs/writefull-logo' import WritefullLogo from '@/shared/svgs/writefull-logo'
import OLButton from '@/features/ui/components/ol/ol-button'
const eventSegmentation = { const eventSegmentation = {
location: 'dashboard-banner', location: 'dashboard-banner',
@ -33,10 +34,9 @@ function WritefullPremiumPromoBanner({
return ( return (
<div data-testid="writefull-premium-promo-banner"> <div data-testid="writefull-premium-promo-banner">
<Notification <Notification
bsStyle="info" type="info"
newNotificationStyle
onDismiss={handleClose} onDismiss={handleClose}
body={ content={
<> <>
Enjoying Writefull? Get <strong>10% off Writefull Premium</strong>, Enjoying Writefull? Get <strong>10% off Writefull Premium</strong>,
giving you access to TeXGPTAI assistance to generate LaTeX code. giving you access to TeXGPTAI assistance to generate LaTeX code.
@ -44,8 +44,8 @@ function WritefullPremiumPromoBanner({
</> </>
} }
action={ action={
<a <OLButton
className="btn btn-secondary btn-sm" variant="secondary"
href="https://my.writefull.com/overleaf-invite?code=OVERLEAF10&redirect=plans" href="https://my.writefull.com/overleaf-invite?code=OVERLEAF10&redirect=plans"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -55,7 +55,7 @@ function WritefullPremiumPromoBanner({
> >
<WritefullLogo width="16" height="16" />{' '} <WritefullLogo width="16" height="16" />{' '}
<span>Get Writefull Premium</span> <span>Get Writefull Premium</span>
</a> </OLButton>
} }
/> />
</div> </div>

View file

@ -29,6 +29,7 @@ import { SplitTestProvider } from '@/shared/context/split-test-context'
import OLCol from '@/features/ui/components/ol/ol-col' import OLCol from '@/features/ui/components/ol/ol-col'
import { bsVersion } from '@/features/utils/bootstrap-5' import { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames' import classnames from 'classnames'
import Notification from '@/shared/components/notification'
function ProjectListRoot() { function ProjectListRoot() {
const { isReady } = useWaitForI18n() const { isReady } = useWaitForI18n()
@ -127,7 +128,12 @@ function ProjectListPageContent() {
/> />
</Col> </Col>
</Row> </Row>
<div className="project-list-sidebar-survey-wrapper visible-xs"> <div
className={classnames(
'project-list-sidebar-survey-wrapper',
bsVersion({ bs5: 'd-md-none', bs3: 'visible-xs' })
)}
>
<SurveyWidget /> <SurveyWidget />
</div> </div>
<div className="visible-xs mt-1"> <div className="visible-xs mt-1">
@ -199,10 +205,12 @@ function DashApiError() {
xs={{ span: 8, offset: 2 }} xs={{ span: 8, offset: 2 }}
bs3Props={{ xs: 8, xsOffset: 2 }} bs3Props={{ xs: 8, xsOffset: 2 }}
aria-live="polite" aria-live="polite"
className="text-center"
> >
<div className="alert alert-danger"> <div className="notification-list">
{t('generic_something_went_wrong')} <Notification
content={t('generic_something_went_wrong')}
type="error"
/>
</div> </div>
</OLCol> </OLCol>
</Row> </Row>

View file

@ -2,6 +2,7 @@ import usePersistedState from '../../../shared/hooks/use-persisted-state'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import { useCallback } from 'react' import { useCallback } from 'react'
import Close from '@/shared/components/close' import Close from '@/shared/components/close'
import { bsVersion } from '@/features/utils/bootstrap-5'
export default function SurveyWidget() { export default function SurveyWidget() {
const survey = getMeta('ol-survey') const survey = getMeta('ol-survey')
@ -21,7 +22,13 @@ export default function SurveyWidget() {
return ( return (
<div className="user-notifications"> <div className="user-notifications">
<div className="notification-entry"> <div className="notification-entry">
<div role="alert" className="alert alert-info-alt"> <div
role="alert"
className={bsVersion({
bs3: 'alert alert-info-alt',
bs5: 'survey-notification',
})}
>
<div className="notification-body"> <div className="notification-body">
{survey.preText}&nbsp; {survey.preText}&nbsp;
<a <a

View file

@ -28,6 +28,8 @@ export function bs3ButtonProps(props: ButtonProps) {
disabled: props.isLoading || props.disabled, disabled: props.isLoading || props.disabled,
form: props.form, form: props.form,
href: props.href, href: props.href,
target: props.target,
rel: props.rel,
onClick: props.onClick, onClick: props.onClick,
type: props.type, type: props.type,
} }

View file

@ -7,6 +7,8 @@ export type ButtonProps = {
form?: string form?: string
leadingIcon?: string leadingIcon?: string
href?: string href?: string
target?: string
rel?: string
isLoading?: boolean isLoading?: boolean
onClick?: MouseEventHandler<HTMLButtonElement> onClick?: MouseEventHandler<HTMLButtonElement>
size?: 'small' | 'default' | 'large' size?: 'small' | 'default' | 'large'

View file

@ -15,7 +15,7 @@ export type NotificationProps = {
ariaLive?: 'polite' | 'off' | 'assertive' ariaLive?: 'polite' | 'off' | 'assertive'
className?: string className?: string
content: React.ReactNode content: React.ReactNode
customIcon?: React.ReactElement customIcon?: React.ReactElement | null
disclaimer?: React.ReactElement | string disclaimer?: React.ReactElement | string
isDismissible?: boolean isDismissible?: boolean
isActionBelowContent?: boolean isActionBelowContent?: boolean
@ -78,6 +78,8 @@ function Notification({
if (onDismiss) onDismiss() if (onDismiss) onDismiss()
} }
// return null
if (!show) { if (!show) {
return null return null
} }
@ -89,7 +91,9 @@ function Notification({
role="alert" role="alert"
id={id} id={id}
> >
{customIcon !== null && (
<NotificationIcon notificationType={type} customIcon={customIcon} /> <NotificationIcon notificationType={type} customIcon={customIcon} />
)}
<div className="notification-content-and-cta"> <div className="notification-content-and-cta">
<div className="notification-content"> <div className="notification-content">

View file

@ -133,7 +133,6 @@ export interface Meta {
'ol-memberGroupSubscriptions': MemberGroupSubscription[] 'ol-memberGroupSubscriptions': MemberGroupSubscription[]
'ol-memberOfSSOEnabledGroups': GroupSSOLinkingStatus[] 'ol-memberOfSSOEnabledGroups': GroupSSOLinkingStatus[]
'ol-members': MinimalUser[] 'ol-members': MinimalUser[]
'ol-newNotificationStyle': boolean
'ol-no-single-dollar': boolean 'ol-no-single-dollar': boolean
'ol-notifications': NotificationType[] 'ol-notifications': NotificationType[]
'ol-notificationsInstitution': InstitutionType[] 'ol-notificationsInstitution': InstitutionType[]

View file

@ -25,7 +25,6 @@ export const ProjectInvite = (args: any) => {
token: 'abcdef', token: 'abcdef',
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -44,7 +43,6 @@ export const ProjectInviteNetworkError = (args: any) => {
token: 'abcdef', token: 'abcdef',
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -58,7 +56,6 @@ export const Wfh2020UpgradeOffer = (args: any) => {
_id: 1, _id: 1,
templateKey: 'wfh_2020_upgrade_offer', templateKey: 'wfh_2020_upgrade_offer',
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -77,7 +74,6 @@ export const IPMatchedAffiliationSsoEnabled = (args: any) => {
ssoEnabled: true, ssoEnabled: true,
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -96,7 +92,6 @@ export const IPMatchedAffiliationSsoDisabled = (args: any) => {
ssoEnabled: false, ssoEnabled: false,
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -113,7 +108,6 @@ export const TpdsFileLimit = (args: any) => {
projectName: 'Abc Project', projectName: 'Abc Project',
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -130,7 +124,6 @@ export const DropBoxDuplicateProjectNames = (args: any) => {
projectName: 'Abc Project', projectName: 'Abc Project',
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -144,7 +137,6 @@ export const DropBoxUnlinkedDueToLapsedReconfirmation = (args: any) => {
_id: 1, _id: 1,
templateKey: 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation', templateKey: 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation',
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -161,7 +153,6 @@ export const NotificationGroupInvitation = (args: any) => {
inviterName: 'John Doe', inviterName: 'John Doe',
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -178,7 +169,6 @@ export const NotificationGroupInvitationCancelSubscription = (args: any) => {
inviterName: 'John Doe', inviterName: 'John Doe',
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
window.metaAttributesCache.set('ol-hasIndividualRecurlySubscription', true) window.metaAttributesCache.set('ol-hasIndividualRecurlySubscription', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
@ -190,7 +180,6 @@ export const NotificationGroupInvitationCancelSubscription = (args: any) => {
export const NonSpecificMessage = (args: any) => { export const NonSpecificMessage = (args: any) => {
useFetchMock(commonSetupMocks) useFetchMock(commonSetupMocks)
setCommonMeta({ _id: 1, html: 'Non specific message' }) setCommonMeta({ _id: 1, html: 'Non specific message' })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -204,7 +193,6 @@ export const InstitutionSsoAvailable = (args: any) => {
_id: 1, _id: 1,
templateKey: 'notification_institution_sso_available', templateKey: 'notification_institution_sso_available',
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -218,7 +206,6 @@ export const InstitutionSsoLinked = (args: any) => {
_id: 1, _id: 1,
templateKey: 'notification_institution_sso_linked', templateKey: 'notification_institution_sso_linked',
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -232,7 +219,6 @@ export const InstitutionSsoNonCanonical = (args: any) => {
_id: 1, _id: 1,
templateKey: 'notification_institution_sso_non_canonical', templateKey: 'notification_institution_sso_non_canonical',
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -246,7 +232,6 @@ export const InstitutionSsoAlreadyRegistered = (args: any) => {
_id: 1, _id: 1,
templateKey: 'notification_institution_sso_already_registered', templateKey: 'notification_institution_sso_already_registered',
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -264,7 +249,6 @@ export const InstitutionSsoError = (args: any) => {
tryAgain: true, tryAgain: true,
}, },
}) })
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -275,7 +259,6 @@ export const InstitutionSsoError = (args: any) => {
export const ResendConfirmationEmail = (args: any) => { export const ResendConfirmationEmail = (args: any) => {
useFetchMock(reconfirmationSetupMocks) useFetchMock(reconfirmationSetupMocks)
setReconfirmationMeta() setReconfirmationMeta()
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -286,7 +269,6 @@ export const ResendConfirmationEmail = (args: any) => {
export const ResendConfirmationEmailNetworkError = (args: any) => { export const ResendConfirmationEmailNetworkError = (args: any) => {
useFetchMock(errorsMocks) useFetchMock(errorsMocks)
setReconfirmationMeta() setReconfirmationMeta()
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -300,7 +282,6 @@ export const ReconfirmAffiliation = (args: any) => {
window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [ window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
fakeReconfirmationUsersData, fakeReconfirmationUsersData,
]) ])
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -314,7 +295,6 @@ export const ReconfirmAffiliationNetworkError = (args: any) => {
window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [ window.metaAttributesCache.set('ol-allInReconfirmNotificationPeriods', [
fakeReconfirmationUsersData, fakeReconfirmationUsersData,
]) ])
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />
@ -326,7 +306,6 @@ export const ReconfirmedAffiliationSuccess = (args: any) => {
useFetchMock(reconfirmAffiliationSetupMocks) useFetchMock(reconfirmAffiliationSetupMocks)
setReconfirmAffiliationMeta() setReconfirmAffiliationMeta()
window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData]) window.metaAttributesCache.set('ol-userEmails', [fakeReconfirmationUsersData])
window.metaAttributesCache.set('ol-newNotificationStyle', true)
return ( return (
<ProjectListProvider> <ProjectListProvider>
<UserNotifications {...args} /> <UserNotifications {...args} />

View file

@ -1,2 +1,3 @@
@import 'account-settings'; @import 'account-settings';
@import 'project-list'; @import 'project-list';
@import 'sidebar-v2-dash-pane';

View file

@ -168,3 +168,54 @@
} }
} }
} }
.survey-notification {
display: flex;
flex-wrap: wrap;
padding: var(--spacing-06);
background-color: var(--bg-dark-tertiary);
border-color: transparent;
color: var(--neutral-20);
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.25);
border-radius: var(--border-radius-base);
@include media-breakpoint-up(md) {
flex-wrap: nowrap;
}
button.close {
@extend .text-white;
padding: 0;
-webkit-appearance: none;
}
}
.project-list-sidebar-survey-wrapper {
position: sticky;
bottom: 0;
.survey-notification {
font-size: var(--font-size-02);
a {
text-decoration: none;
}
}
@include media-breakpoint-down(md) {
position: static;
margin-top: var(--spacing-05);
.survey-notification {
font-size: unset;
.project-list-sidebar-survey-link {
display: block;
align-items: center;
min-width: 48px;
min-height: 48px;
padding-top: var(--spacing-07);
}
}
}
}

View file

@ -0,0 +1,13 @@
.project-list-sidebar-survey-link {
@extend .text-white;
font-weight: bold;
}
.notification-close-button-style button {
&:hover {
background-color: transparent !important;
}
&:focus {
background-color: transparent !important;
}
}

View file

@ -67,10 +67,10 @@ describe('<ProjectsActionModal />', function () {
await waitFor(() => { await waitFor(() => {
const alerts = screen.getAllByRole('alert') const alerts = screen.getAllByRole('alert')
expect(alerts.length).to.equal(2) expect(alerts.length).to.equal(2)
expect(alerts[0].textContent).to.equal( expect(alerts[0].textContent).to.contain(
`${projectsData[2].name}Something went wrong. Please try again.` `${projectsData[2].name}Something went wrong. Please try again.`
) )
expect(alerts[1].textContent).to.equal( expect(alerts[1].textContent).to.contain(
`${projectsData[3].name}Something went wrong. Please try again.` `${projectsData[3].name}Something went wrong. Please try again.`
) )
}) })