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')
}
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 {
await SplitTestHandler.promises.getAssignment(req, res, 'paywall-cta')
} catch (error) {
@ -473,7 +457,6 @@ async function projectListPage(req, res, next) {
groupName: subscription.teamName,
})),
hasIndividualRecurlySubscription,
newNotificationStyle,
})
}

View file

@ -35,7 +35,6 @@ block append meta
meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
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)
block content

View file

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

View file

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

View file

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

View file

@ -1,14 +1,18 @@
import { memo, useEffect, useState } from 'react'
import { Alert, Modal } from 'react-bootstrap'
import { Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Project } from '../../../../../../types/project/dashboard/api'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import { getUserFacingMessage } from '../../../../infrastructure/fetch-json'
import useIsMounted from '../../../../shared/hooks/use-is-mounted'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import { isSmallDevice } from '../../../../infrastructure/event-tracking'
import getMeta from '@/utils/meta'
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 = {
title?: string
@ -68,64 +72,44 @@ function ProjectsActionModal({
}, [action, showModal])
return (
<AccessibleModal
<OLModal
animation
show={showModal}
onHide={handleCloseModal}
id="action-project-modal"
backdrop="static"
>
<Modal.Header closeButton>
<OLModalHeader closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>{children}</Modal.Body>
<Modal.Footer>
</OLModalHeader>
<OLModalBody>
{children}
{!isProcessing &&
errors.length > 0 &&
errors.map((e, i) => <ErrorNotification error={e} key={i} />)}
<button className="btn btn-secondary" onClick={handleCloseModal}>
{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">
errors.map((error, i) => (
<div className="notification-list" key={i}>
<Notification
type="error"
title={error.projectName}
content={getUserFacingMessage(error.error) as string}
/>
</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)

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import { memo, useEffect, useState } from 'react'
import Notification from './notification'
import customLocalStorage from '@/infrastructure/local-storage'
import { useTranslation } from 'react-i18next'
import OLButton from '@/features/ui/components/ol/ol-button'
function AccessibilitySurveyBanner() {
const { t } = useTranslation()
@ -27,18 +28,18 @@ function AccessibilitySurveyBanner() {
return (
<Notification
className="sr-only"
bsStyle="info"
type="info"
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={
<a
className="btn btn-secondary btn-sm pull-right btn-info"
<OLButton
variant="secondary"
href="https://docs.google.com/forms/d/e/1FAIpQLSdxKP_biRXvrkmJzlBjMwI_qPSuv4NbBvYUzSOc3OOTIOTmnQ/viewform"
target="_blank"
rel="noreferrer"
>
{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,
GroupsAndEnterpriseBannerVariants,
} from '../../../../../../types/project/dashboard/notification'
import OLButton from '@/features/ui/components/ol/ol-button'
type urlForVariantsType = {
[key in GroupsAndEnterpriseBannerVariant]: string // eslint-disable-line no-unused-vars
@ -31,7 +32,6 @@ export default function GroupsAndEnterpriseBanner() {
const groupsAndEnterpriseBannerVariant = getMeta(
'ol-groupsAndEnterpriseBannerVariant'
)
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const hasDismissedGroupsAndEnterpriseBanner = hasRecentlyDismissedBanner()
@ -73,23 +73,19 @@ export default function GroupsAndEnterpriseBanner() {
return (
<Notification
bsStyle="info"
type="info"
onDismiss={handleClose}
body={<BannerContent variant={groupsAndEnterpriseBannerVariant} />}
content={<BannerContent variant={groupsAndEnterpriseBannerVariant} />}
action={
<a
className={
newNotificationStyle
? 'btn btn-secondary btn-sm'
: 'pull-right btn btn-info btn-sm'
}
<OLButton
variant="secondary"
href={contactSalesUrl}
target="_blank"
rel="noreferrer"
onClick={handleClickContact}
>
{t('contact_sales')}
</a>
</OLButton>
}
/>
)

View file

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'
import { useTranslation, Trans } from 'react-i18next'
import { Button } from 'react-bootstrap'
import Icon from '../../../../../../shared/components/icon'
import getMeta from '../../../../../../utils/meta'
import useAsync from '../../../../../../shared/hooks/use-async'
@ -12,6 +11,8 @@ import { UserEmailData } from '../../../../../../../../types/user-email'
import { Institution } from '../../../../../../../../types/institution'
import { useLocation } from '../../../../../../shared/hooks/use-location'
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 = {
email: UserEmailData['email']
@ -24,7 +25,6 @@ function ReconfirmAffiliation({
}: ReconfirmAffiliationProps) {
const { t } = useTranslation()
const { samlInitPath } = getMeta('ol-ExposedSettings')
const newNotificationStyle = getMeta('ol-newNotificationStyle')
const { error, isLoading, isError, isSuccess, runAsync } = useAsync()
const [hasSent, setHasSent] = useState(false)
const [isPending, setIsPending] = useState(false)
@ -57,7 +57,10 @@ function ReconfirmAffiliation({
if (hasSent) {
return (
<div className="w-100">
<Notification
type="info"
content={
<>
<Trans
i18nKey="please_check_your_inbox_to_confirm"
components={[<b />]} // eslint-disable-line react/jsx-key
@ -66,19 +69,6 @@ function ReconfirmAffiliation({
tOptions={{ interpolation: { escapeValue: true } }}
/>
&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 && (
<>
<br />
@ -89,28 +79,35 @@ function ReconfirmAffiliation({
</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 (
<div className="w-100">
{!newNotificationStyle && <Icon type="warning" />}
<Button
bsStyle="info"
bsSize="sm"
className="btn-reconfirm"
onClick={handleRequestReconfirmation}
disabled={isLoading || isPending}
>
{isLoading ? (
<Notification
type="info"
content={
<>
<Icon type="refresh" spin fw /> {t('sending')}&hellip;
</>
) : (
t('confirm_affiliation')
)}
</Button>
<Trans
i18nKey="are_you_still_at"
components={[<b />]} // eslint-disable-line react/jsx-key
@ -141,7 +138,27 @@ function ReconfirmAffiliation({
</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 =>
userEmail.affiliation?.institution ? (
<Notification
key={`reconfirmation-period-email-${userEmail.email}`}
bsStyle="info"
body={
<div className="reconfirm-notification">
<ReconfirmAffiliation
email={userEmail.email}
institution={userEmail.affiliation.institution}
/>
</div>
}
key={`reconfirmation-period-email-${userEmail.email}`}
/>
) : null
)}
@ -31,9 +24,9 @@ function ReconfirmationInfo() {
userEmail.affiliation?.institution ? (
<Notification
key={`samlIdentifier-email-${userEmail.email}`}
bsStyle="info"
type="info"
onDismiss={() => {}}
body={
content={
<ReconfirmationInfoSuccess
institution={userEmail.affiliation?.institution}
/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -49,10 +49,9 @@ export default function IEEERetirementBanner({
return (
<Notification
bsStyle="warning"
type="warning"
onDismiss={handleClose}
newNotificationStyle
body={
content={
<Trans
i18nKey="notification_ieee_collabratec_retirement_message"
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 NewNotification, {
NotificationType,
} from '@/shared/components/notification'
import getMeta from '@/utils/meta'
import NewNotification from '@/shared/components/notification'
type NotificationProps = {
bsStyle: AlertProps['bsStyle']
children?: React.ReactNode
body?: React.ReactNode
action?: React.ReactElement
onDismiss?: AlertProps['onDismiss']
className?: string
newNotificationStyle?: boolean
}
type NotificationProps = Pick<
React.ComponentProps<typeof NewNotification>,
'type' | 'action' | 'content' | 'onDismiss' | 'className'
>
/**
* Renders either a legacy-styled notification using Boostrap `Alert`, or a new-styled notification using
* the shared `Notification` component.
*
* 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')
function Notification({ className, ...props }: NotificationProps) {
const notificationComponent = (
<NewNotification isDismissible={props.onDismiss != null} {...props} />
)
const [show, setShow] = useState(true)
const handleDismiss = () => {
if (onDismiss) {
onDismiss()
}
setShow(false)
}
if (!show) {
return null
}
if (newNotificationStyle && body) {
const newNotificationType = (
bsStyle === 'danger' ? 'error' : bsStyle
) as NotificationType
return (
return notificationComponent ? (
<li className={classnames('notification-entry', className)}>
<NewNotification
type={newNotificationType}
isDismissible={onDismiss != null}
onDismiss={handleDismiss}
content={body as React.ReactElement}
action={action}
/>
{notificationComponent}
</li>
)
}
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>
)
}
) : null
}
Notification.Body = Body
Notification.Action = Action
export default Notification

View file

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

View file

@ -3,6 +3,7 @@ import Notification from './notification'
import { sendMB } from '@/infrastructure/event-tracking'
import customLocalStorage from '@/infrastructure/local-storage'
import WritefullLogo from '@/shared/svgs/writefull-logo'
import OLButton from '@/features/ui/components/ol/ol-button'
const eventSegmentation = {
location: 'dashboard-banner',
@ -33,10 +34,9 @@ function WritefullPremiumPromoBanner({
return (
<div data-testid="writefull-premium-promo-banner">
<Notification
bsStyle="info"
newNotificationStyle
type="info"
onDismiss={handleClose}
body={
content={
<>
Enjoying Writefull? Get <strong>10% off Writefull Premium</strong>,
giving you access to TeXGPTAI assistance to generate LaTeX code.
@ -44,8 +44,8 @@ function WritefullPremiumPromoBanner({
</>
}
action={
<a
className="btn btn-secondary btn-sm"
<OLButton
variant="secondary"
href="https://my.writefull.com/overleaf-invite?code=OVERLEAF10&redirect=plans"
target="_blank"
rel="noreferrer"
@ -55,7 +55,7 @@ function WritefullPremiumPromoBanner({
>
<WritefullLogo width="16" height="16" />{' '}
<span>Get Writefull Premium</span>
</a>
</OLButton>
}
/>
</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 { bsVersion } from '@/features/utils/bootstrap-5'
import classnames from 'classnames'
import Notification from '@/shared/components/notification'
function ProjectListRoot() {
const { isReady } = useWaitForI18n()
@ -127,7 +128,12 @@ function ProjectListPageContent() {
/>
</Col>
</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 />
</div>
<div className="visible-xs mt-1">
@ -199,10 +205,12 @@ function DashApiError() {
xs={{ span: 8, offset: 2 }}
bs3Props={{ xs: 8, xsOffset: 2 }}
aria-live="polite"
className="text-center"
>
<div className="alert alert-danger">
{t('generic_something_went_wrong')}
<div className="notification-list">
<Notification
content={t('generic_something_went_wrong')}
type="error"
/>
</div>
</OLCol>
</Row>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,3 @@
@import 'account-settings';
@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(() => {
const alerts = screen.getAllByRole('alert')
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.`
)
expect(alerts[1].textContent).to.equal(
expect(alerts[1].textContent).to.contain(
`${projectsData[3].name}Something went wrong. Please try again.`
)
})