feat: updating alerts to use Paragon Alert over custom (AA-914) (#557)

This commit is contained in:
Carla Duarte
2021-07-28 09:39:31 -04:00
committed by GitHub
parent 6a3ad1d659
commit a8348e1568
21 changed files with 229 additions and 315 deletions

22
package-lock.json generated
View File

@@ -1447,9 +1447,9 @@
}
},
"@edx/paragon": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.2.0.tgz",
"integrity": "sha512-13xGUU+BezQ27NvR1gtm7YxbcborgUxX78PlUXjamSM3bqMt4LfviyxZjewyxBuevAF+Tj4PlLzKMLe3SkuwFw==",
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-16.5.0.tgz",
"integrity": "sha512-oXZBL1eaBKtPnypVx3hAoVs/zwTjQP+jfj6Vk610+GlsoYzevho4H2CXvrym5HKYAvYvKwXuIibh/IOdzhk+tQ==",
"requires": {
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
@@ -1467,7 +1467,7 @@
"react-focus-on": "^3.5.0",
"react-popper": "^2.2.4",
"react-proptype-conditional-require": "^1.0.4",
"react-responsive": "^6.1.1",
"react-responsive": "^8.2.0",
"react-table": "^7.6.1",
"react-transition-group": "^4.0.0",
"tabbable": "^4.0.0",
@@ -17689,13 +17689,14 @@
}
},
"react-responsive": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-6.1.2.tgz",
"integrity": "sha512-AXentVC/kN3KED9zhzJv2pu4vZ0i6cSHdTtbCScVV1MT6F5KXaG2qs5D7WLmhdaOvmiMX8UfmS4ZSO+WPwDt4g==",
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz",
"integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==",
"requires": {
"hyphenate-style-name": "^1.0.0",
"matchmediaquery": "^0.3.0",
"prop-types": "^15.6.1"
"prop-types": "^15.6.1",
"shallow-equal": "^1.1.0"
}
},
"react-router": {
@@ -18985,6 +18986,11 @@
"kind-of": "^6.0.2"
}
},
"shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",

View File

@@ -40,7 +40,7 @@
"@edx/frontend-enterprise": "4.2.3",
"@edx/frontend-lib-special-exams": "1.11.0",
"@edx/frontend-platform": "1.11.0",
"@edx/paragon": "16.2.0",
"@edx/paragon": "16.5.0",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.13.1",
"@fortawesome/free-regular-svg-icons": "5.13.1",

View File

@@ -4,9 +4,9 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
FormattedMessage, FormattedDate, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
import messages from './messages';
import AccessExpirationAlertMMP2P from './AccessExpirationAlertMMP2P';
import AccessExpirationAlertMasquerade from './AccessExpirationAlertMasquerade';
@@ -100,7 +100,7 @@ function AccessExpirationAlert({ intl, payload }) {
}
return (
<Alert type={ALERT_TYPES.INFO}>
<Alert variant="info" icon={Info}>
<span className="font-weight-bold">
<FormattedMessage
id="learning.accessExpiration.header"

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, injectIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
import messages from './messages';
function AccessExpirationAlertMMP2P({ payload }) {
@@ -52,7 +52,7 @@ function AccessExpirationAlertMMP2P({ payload }) {
}
return (
<Alert type={ALERT_TYPES.INFO}>
<Alert variant="info" icon={Info}>
<span className="font-weight-bold">
Unlock full course content by {formatDate(upgradeDeadline, 'upgradeTitle')}
</span>

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
function AccessExpirationAlertMasquerade({ payload }) {
const {
@@ -26,7 +26,7 @@ function AccessExpirationAlertMasquerade({ payload }) {
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
return (
<Alert type={ALERT_TYPES.INFO}>
<Alert variant="info" icon={Info}>
<FormattedMessage
id="learning.accessExpiration.expired"
defaultMessage="This learner does not have access to this course. Their access expired on {date}."

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { Alert, Button } from '@edx/paragon';
import { Info, WarningFilled } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { useModel } from '../../generic/model-store';
import { Alert, ALERT_TYPES } from '../../generic/user-messages';
import messages from './messages';
import { useEnrollClickHandler } from './hooks';
@@ -30,27 +30,29 @@ function EnrollmentAlert({ intl, payload }) {
);
let text = intl.formatMessage(messages.alert);
let type = ALERT_TYPES.ERROR;
let type = 'warning';
let icon = WarningFilled;
if (isStaff) {
text = intl.formatMessage(messages.staffAlert);
type = ALERT_TYPES.INFO;
type = 'info';
icon = Info;
} else if (extraText) {
text = `${text} ${extraText}`;
}
const button = canEnroll && (
<Button disabled={loading} variant="link" className="p-0 border-0 align-top" style={{ textDecoration: 'underline' }} onClick={enrollClickHandler}>
<Button disabled={loading} variant="link" className="p-0 border-0 align-top mx-1" size="sm" style={{ textDecoration: 'underline' }} onClick={enrollClickHandler}>
{intl.formatMessage(messages.enrollNowSentence)}
</Button>
);
return (
<Alert type={type}>
{text}
{' '}
{button}
{' '}
{loading && <FontAwesomeIcon icon={faSpinner} spin />}
<Alert variant={type} icon={icon}>
<div className="d-flex">
{text}
{button}
{loading && <FontAwesomeIcon icon={faSpinner} spin />}
</div>
</Alert>
);
}

View File

@@ -2,9 +2,9 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Hyperlink } from '@edx/paragon';
import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@edx/paragon/icons';
import { Alert } from '../../generic/user-messages';
import genericMessages from '../../generic/messages';
function LogistrationAlert({ intl }) {
@@ -29,7 +29,7 @@ function LogistrationAlert({ intl }) {
);
return (
<Alert type="error">
<Alert variant="warning" icon={WarningFilled}>
<FormattedMessage
id="learning.logistration.alert"
description="Prompts the user to sign in or register to see course content."

View File

@@ -485,8 +485,8 @@ describe('Outline Tab', () => {
});
await fetchAndRender();
const alert = await screen.findByText('Welcome to Demonstration Course');
expect(alert.parentElement).toHaveAttribute('role', 'alert');
const alert = await screen.findByTestId('private-course-alert');
expect(alert).toHaveAttribute('role', 'alert');
expect(screen.queryByRole('button', { name: 'Enroll now' })).not.toBeInTheDocument();
expect(screen.getByText('You must be enrolled in the course to see course content.')).toBeInTheDocument();
@@ -495,8 +495,8 @@ describe('Outline Tab', () => {
it('displays alert for unenrolled user', async () => {
await fetchAndRender();
const alert = await screen.findByText('Welcome to Demonstration Course');
expect(alert.parentElement).toHaveAttribute('role', 'alert');
const alert = await screen.findByTestId('private-course-alert');
expect(alert).toHaveAttribute('role', 'alert');
expect(screen.getByRole('button', { name: 'Enroll now' })).toBeInTheDocument();
});

View File

@@ -6,8 +6,8 @@ import {
FormattedRelative,
FormattedTime,
} from '@edx/frontend-platform/i18n';
import { Alert, ALERT_TYPES } from '../../../../generic/user-messages';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
const DAY_MS = 24 * 60 * 60 * 1000; // in ms
@@ -78,7 +78,7 @@ function CourseEndAlert({ payload }) {
}
return (
<Alert type={ALERT_TYPES.INFO}>
<Alert variant="info" icon={Info}>
<strong>{msg}</strong><br />
{description}
</Alert>

View File

@@ -6,8 +6,8 @@ import {
FormattedRelative,
FormattedTime,
} from '@edx/frontend-platform/i18n';
import { Alert, ALERT_TYPES } from '../../../../generic/user-messages';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
const DAY_MS = 24 * 60 * 60 * 1000; // in ms
@@ -30,7 +30,7 @@ function CourseStartAlert({ payload }) {
const delta = new Date(startDate) - new Date();
if (delta < DAY_MS) {
return (
<Alert type={ALERT_TYPES.INFO}>
<Alert variant="info" icon={Info}>
<FormattedMessage
id="learning.outline.alert.start.short"
defaultMessage="Course starts {timeRemaining} at {courseStartTime}."
@@ -55,7 +55,7 @@ function CourseStartAlert({ payload }) {
}
return (
<Alert type={ALERT_TYPES.INFO}>
<Alert variant="info" icon={Info}>
<strong>
<FormattedMessage
id="learning.outline.alert.end.long"

View File

@@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Button, Hyperlink } from '@edx/paragon';
import { Alert, Button, Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { Alert } from '../../../../generic/user-messages';
import enrollmentMessages from '../../../../alerts/enrollment-alert/messages';
import genericMessages from '../../../../generic/messages';
import messages from './messages';
import outlineMessages from '../../messages';
import { useEnrollClickHandler } from '../../../../alerts/enrollment-alert/hooks';
import { useModel } from '../../../../generic/model-store';
@@ -32,12 +32,13 @@ function PrivateCourseAlert({ intl, payload }) {
intl.formatMessage(enrollmentMessages.success),
);
const enrollNow = (
const enrollNowButton = (
<Button
disabled={loading}
variant="link"
className="p-0 border-0 align-top"
className="p-0 border-0 align-top mr-1"
style={{ textDecoration: 'underline' }}
size="sm"
onClick={enrollClickHandler}
>
{intl.formatMessage(enrollmentMessages.enrollNowInline)}
@@ -63,7 +64,7 @@ function PrivateCourseAlert({ intl, payload }) {
);
return (
<Alert type="welcome">
<Alert variant="light" data-testid="private-course-alert">
{anonymousUser && (
<>
<p className="font-weight-bold">
@@ -84,15 +85,11 @@ function PrivateCourseAlert({ intl, payload }) {
<>
<p className="font-weight-bold">{intl.formatMessage(outlineMessages.welcomeTo)} {title}</p>
{canEnroll && (
<>
<FormattedMessage
id="learning.privateCourse.canEnroll"
description="Prompts the user to enroll in the course to see course content."
defaultMessage="{enrollNow} to access the full course."
values={{ enrollNow }}
/>
<div className="d-flex">
{enrollNowButton}
{intl.formatMessage(messages.toAccess)}
{loading && <FontAwesomeIcon icon={faSpinner} spin />}
</>
</div>
)}
{!canEnroll && (
<>

View File

@@ -1,10 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
enroll: {
toAccess: {
id: 'alert.enroll',
defaultMessage: 'You must be enrolled in the course to see course content.',
description: 'Text instructing the learner to enroll in the course in order to see course content.',
defaultMessage: ' to access the full course.',
description: 'Text instructing the learner to enroll in the course in order to see course content. The full string'
+ 'would say "Enroll now to access the full course", where "Enroll now" is a button.',
},
});

View File

@@ -2,14 +2,13 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, TransitionReplace } from '@edx/paragon';
import { Alert, Button, TransitionReplace } from '@edx/paragon';
import truncate from 'truncate-html';
import { useDispatch } from 'react-redux';
import LmsHtmlFragment from '../LmsHtmlFragment';
import messages from '../messages';
import { useModel } from '../../../generic/model-store';
import { Alert } from '../../../generic/user-messages';
import { dismissWelcomeMessage } from '../../data/thunks';
function WelcomeMessage({ courseId, intl }) {
@@ -27,52 +26,47 @@ function WelcomeMessage({ courseId, intl }) {
const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
const dispatch = useDispatch();
return (
display && (
<Alert
type="welcome"
dismissible
onDismiss={() => {
setDisplay(false);
dispatch(dismissWelcomeMessage(courseId));
}}
footer={messageCanBeShortened && (
<div className="row w-100 m-0">
<div className="col-12 col-sm-auto p-0">
<Button
block
onClick={() => setShowShortMessage(!showShortMessage)}
variant="outline-primary"
>
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
: intl.formatMessage(messages.welcomeMessageShowLessButton)}
</Button>
</div>
</div>
<Alert
data-testid="alert-container-welcome"
variant="light"
stacked
dismissible
show={display}
onClose={() => {
setDisplay(false);
dispatch(dismissWelcomeMessage(courseId));
}}
actions={messageCanBeShortened ? [
<Button
onClick={() => setShowShortMessage(!showShortMessage)}
variant="outline-primary"
>
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
: intl.formatMessage(messages.welcomeMessageShowLessButton)}
</Button>,
] : []}
>
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
{showShortMessage ? (
<LmsHtmlFragment
className="inline-link"
data-testid="short-welcome-message-iframe"
key="short-html"
html={shortWelcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)}
/>
) : (
<LmsHtmlFragment
className="inline-link"
data-testid="long-welcome-message-iframe"
key="full-html"
html={welcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)}
/>
)}
>
<TransitionReplace className="mb-3" enterDuration={200} exitDuration={200}>
{showShortMessage ? (
<LmsHtmlFragment
className="inline-link"
data-testid="short-welcome-message-iframe"
key="short-html"
html={shortWelcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)}
/>
) : (
<LmsHtmlFragment
className="inline-link"
data-testid="long-welcome-message-iframe"
key="full-html"
html={welcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)}
/>
)}
</TransitionReplace>
</Alert>
)
</TransitionReplace>
</Alert>
);
}

View File

@@ -9,6 +9,7 @@ import { layoutGenerator } from 'react-break';
import { Helmet } from 'react-helmet';
import { useDispatch, useSelector } from 'react-redux';
import { Alert, Button, Hyperlink } from '@edx/paragon';
import { CheckCircle } from '@edx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
@@ -285,34 +286,37 @@ function CourseCelebration({ intl }) {
</div>
<div className="col-12 px-0 px-md-5">
{certHeader && (
<Alert variant="success" className="row w-100 m-0">
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
<div className="h4">{certHeader}</div>
{message}
<div className="mt-2">
{buttonPrefix}
{buttonLocation && (
<Button
variant={buttonVariant}
href={buttonLocation}
onClick={() => logClick(org, courseId, administrator, buttonEvent)}
>
{buttonText}
</Button>
)}
{buttonSuffix}
<Alert variant="success" icon={CheckCircle}>
<div className="row w-100 m-0">
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
<div className="h4">{certHeader}</div>
{message}
<div className="mt-2">
{buttonPrefix}
{buttonLocation && (
<Button
variant={buttonVariant}
href={buttonLocation}
className="w-xs-100 w-md-auto"
onClick={() => logClick(org, courseId, administrator, buttonEvent)}
>
{buttonText}
</Button>
)}
{buttonSuffix}
</div>
</div>
{certStatus !== 'unverified' && (
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
<img
src={certificateImage}
alt={`${intl.formatMessage(messages.certificateImage)}`}
className="w-100"
style={{ maxWidth: '13rem' }}
/>
</div>
)}
</div>
{certStatus !== 'unverified' && (
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
<img
src={certificateImage}
alt={`${intl.formatMessage(messages.certificateImage)}`}
className="w-100"
style={{ maxWidth: '13rem' }}
/>
</div>
)}
</Alert>
)}
{relatedPrograms && relatedPrograms.map(program => (

View File

@@ -34,13 +34,13 @@ function CourseInProgress({ intl }) {
<div className="col-12 p-0 h2 text-center">
{ intl.formatMessage(messages.courseInProgressHeader) }
</div>
<Alert variant="primary" className="col col-lg-10 mt-4 d-flex">
<Alert variant="primary" className="mt-4">
<div className="row w-100 m-0 align-items-start">
<div className="flex-grow-1 col-md p-0">{ intl.formatMessage(messages.courseInProgressDescription) }</div>
<div className="col-md p-0">{ intl.formatMessage(messages.courseInProgressDescription) }</div>
{datesTabLink && (
<Button
variant="primary"
className="flex-shrink-0 mt-3 mt-md-0 mb-1 mb-md-0 ml-md-5"
className="mt-3 my-md-0 mb-1 ml-md-5 w-xs-100 w-md-auto"
href={datesTabLink}
onClick={() => logClick(org, courseId, administrator, 'view_dates_tab')}
>

View File

@@ -34,7 +34,7 @@ function CourseNonPassing({ intl }) {
<div className="col-12 p-0 h2 text-center">
{ intl.formatMessage(messages.endOfCourseHeader) }
</div>
<Alert variant="primary" className="col col-lg-10 mt-4 d-flex">
<Alert variant="primary" className="col col-lg-10 mt-4">
<div className="row w-100 m-0 align-items-start">
<div className="flex-grow-1 col-sm p-0">{ intl.formatMessage(messages.endOfCourseDescription) }</div>
{progressLink && (

View File

@@ -41,54 +41,56 @@ function ProgramCompletion({
);
return (
<Alert variant="primary" className="row w-100 mx-0 my-3" data-testid="program-completion">
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
<div className="h4">{intl.formatMessage(messages.programsLastCourseHeader, { title })}</div>
<p>
<FormattedMessage
id="courseExit.programCompletion.dashboardMessage"
defaultMessage="To view your certificate status, check the Programs section of your {programLink}."
values={{ programLink }}
/>
</p>
{type === 'microbachelors' && (
<>
<Alert variant="primary" className="my-3" data-testid="program-completion">
<div className="d-flex">
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
<div className="h4">{intl.formatMessage(messages.programsLastCourseHeader, { title })}</div>
<p>
<FormattedMessage
id="courseExit.programCompletion.dashboardMessage"
defaultMessage="To view your certificate status, check the Programs section of your {programLink}."
values={{ programLink }}
/>
</p>
{type === 'microbachelors' && (
<>
<p>
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={`${getConfig().SUPPORT_URL}/hc/en-us/articles/360004623154`}
className="text-reset"
>
{intl.formatMessage(messages.microBachelorsLearnMore)}
</Hyperlink>
</p>
<Button variant="primary" className="mb-2 mb-sm-0" href={`${getConfig().CREDENTIALS_BASE_URL}/records`}>
{intl.formatMessage(messages.applyForCredit)}
</Button>
</>
)}
{type === 'micromasters' && (
<p>
{intl.formatMessage(messages.microMastersMessage)}
{' '}
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={`${getConfig().SUPPORT_URL}/hc/en-us/articles/360004623154`}
destination={`${getConfig().SUPPORT_URL}/hc/en-us/articles/360010346853-Does-a-Micromasters-certificate-count-towards-the-online-Master-s-degree-`}
className="text-reset"
>
{intl.formatMessage(messages.microBachelorsLearnMore)}
{intl.formatMessage(messages.microMastersLearnMore)}
</Hyperlink>
</p>
<Button variant="primary" className="mb-2 mb-sm-0" href={`${getConfig().CREDENTIALS_BASE_URL}/records`}>
{intl.formatMessage(messages.applyForCredit)}
</Button>
</>
)}
{type === 'micromasters' && (
<p>
{intl.formatMessage(messages.microMastersMessage)}
{' '}
<Hyperlink
style={{ textDecoration: 'underline' }}
destination={`${getConfig().SUPPORT_URL}/hc/en-us/articles/360010346853-Does-a-Micromasters-certificate-count-towards-the-online-Master-s-degree-`}
className="text-reset"
>
{intl.formatMessage(messages.microMastersLearnMore)}
</Hyperlink>
</p>
)}
</div>
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
<img
src={certImage}
alt={`${intl.formatMessage(messages.certificateImage)}`}
className="w-100"
style={{ maxWidth: '13rem' }}
data-testid={type}
/>
)}
</div>
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
<img
src={certImage}
alt={`${intl.formatMessage(messages.certificateImage)}`}
className="w-100"
style={{ maxWidth: '13rem' }}
data-testid={type}
/>
</div>
</div>
</Alert>
);

View File

@@ -1,82 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
faExclamationTriangle, faInfoCircle, faCheckCircle, faMinusCircle, faTimes,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconButton } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert as ParagonAlert } from '@edx/paragon';
import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons';
import { ALERT_TYPES } from './UserMessagesProvider';
import './Alert.scss';
import messages from '../messages';
function getAlertClass(type) {
if (type === ALERT_TYPES.ERROR) {
return 'alert-warning';
function getAlertVariant(type) {
switch (type) {
case ALERT_TYPES.ERROR:
return 'warning';
case ALERT_TYPES.DANGER:
return 'danger';
case ALERT_TYPES.SUCCESS:
return 'success';
default:
return 'info';
}
if (type === ALERT_TYPES.DANGER) {
return 'alert-danger';
}
if (type === ALERT_TYPES.SUCCESS) {
return 'alert-success';
}
if (type === ALERT_TYPES.WELCOME) {
return 'alert-welcome alert-light';
}
return 'alert-info';
}
function getAlertIcon(type) {
if (type === ALERT_TYPES.ERROR) {
return faExclamationTriangle;
switch (type) {
case ALERT_TYPES.ERROR:
return WarningFilled;
case ALERT_TYPES.SUCCESS:
return CheckCircle;
default:
return Info;
}
if (type === ALERT_TYPES.DANGER) {
return faMinusCircle;
}
if (type === ALERT_TYPES.SUCCESS) {
return faCheckCircle;
}
return faInfoCircle;
}
function getAlertIconColor(type) {
if (type === ALERT_TYPES.SUCCESS) {
return 'text-success-500';
}
return '';
}
function Alert({
type, dismissible, children, footer, intl, onDismiss,
type, dismissible, children, onDismiss, stacked,
}) {
return (
<div data-testid={`alert-container-${type}`} className={classNames('alert', { 'alert-dismissible': dismissible }, getAlertClass(type))} style={{ padding: '1em' }}>
<div className="row w-100 m-0">
{type !== ALERT_TYPES.WELCOME && (
<div className="col-auto p-0 mr-3">
<FontAwesomeIcon icon={getAlertIcon(type)} className={getAlertIconColor(type)} />
</div>
)}
<div className="col mr-4 p-0 align-items-start">
<div role="alert" className="flex-grow-1">
{children}
</div>
</div>
{dismissible && (
<div className="col-auto p-0" style={{ margin: '-0.2em -0.2em 0em 0em' }}>
<IconButton
icon={faTimes}
onClick={onDismiss}
alt={intl.formatMessage(messages.close)}
variant="primary"
/>
</div>
)}
</div>
{footer}
</div>
<ParagonAlert
data-testid={`alert-container-${type}`}
variant={getAlertVariant(type)}
icon={getAlertIcon(type)}
dismissible={dismissible}
onClose={onDismiss}
stacked={stacked}
>
{children}
</ParagonAlert>
);
}
@@ -86,20 +52,18 @@ Alert.propTypes = {
ALERT_TYPES.DANGER,
ALERT_TYPES.INFO,
ALERT_TYPES.SUCCESS,
ALERT_TYPES.WELCOME,
]).isRequired,
dismissible: PropTypes.bool,
children: PropTypes.node,
footer: PropTypes.node,
intl: intlShape.isRequired,
onDismiss: PropTypes.func,
stacked: PropTypes.bool,
};
Alert.defaultProps = {
dismissible: false,
children: undefined,
footer: null,
onDismiss: null,
stacked: false,
};
export default injectIntl(Alert);
export default Alert;

View File

@@ -1,3 +0,0 @@
.alert-welcome {
border: #b9babe solid 1px !important;
}

View File

@@ -1,55 +0,0 @@
import React from 'react';
import {
render, screen, fireEvent, initializeMockApp,
} from '../../setupTest';
import { Alert, ALERT_TYPES } from './index';
describe('Alert', () => {
const types = {
[ALERT_TYPES.ERROR]: {
alert_class: 'alert-warning',
icon: 'fa-exclamation-triangle',
},
[ALERT_TYPES.DANGER]: {
alert_class: 'alert-danger',
icon: 'fa-minus-circle',
},
[ALERT_TYPES.SUCCESS]: {
alert_class: 'alert-success',
icon: 'fa-check-circle',
},
[ALERT_TYPES.INFO]: {
alert_class: 'alert-info',
icon: 'fa-info-circle',
},
};
beforeAll(async () => {
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
await initializeMockApp();
});
Object.entries(types).forEach(([alert, properties]) => {
it(`renders ${alert} alert`, () => {
const alertContent = 'Test alert.';
const { container } = render(<Alert type={alert}>{alertContent}</Alert>);
expect(container.firstChild).toHaveClass(properties.alert_class);
expect(container.querySelector('svg')).toHaveClass(properties.icon);
expect(screen.getByText(alertContent)).toBeInTheDocument();
});
});
it('is dismissible', () => {
const onDismiss = jest.fn();
const { container } = render(<Alert type={ALERT_TYPES.ERROR} dismissible {...{ onDismiss }} />);
expect(container.firstChild).toHaveClass('alert-dismissible');
const dismissButton = screen.getByRole('button');
expect(container.querySelector('svg')).toHaveClass('fa-exclamation-triangle');
fireEvent.click(dismissButton);
expect(onDismiss).toHaveBeenCalledTimes(1);
});
});

View File

@@ -134,18 +134,20 @@ function StreakModal({
</div>
)}
{ AA759ExperimentEnabled && (
<Alert variant="success" className="px-0 d-flex">
<Icon className="col-small ml-3 text-success-500" src={MoneyFilled} />
<div className="col-11 factoid-wrapper">
<b>{intl.formatMessage(messages.congratulations)}</b>
&nbsp;{intl.formatMessage(messages.streakDiscountMessage)}&nbsp;
<FormattedMessage
id="learning.streakCelebration.streakAA759EndDateMessage"
defaultMessage="Ends {date}."
values={{
date: new Date('2021-7-20 00:00').toLocaleDateString({ timeZone: 'UTC' }),
}}
/>
<Alert variant="success" className="px-0">
<div className="d-flex">
<Icon className="col-small ml-3 text-success-500" src={MoneyFilled} />
<div className="col-11 factoid-wrapper">
<b>{intl.formatMessage(messages.congratulations)}</b>
&nbsp;{intl.formatMessage(messages.streakDiscountMessage)}&nbsp;
<FormattedMessage
id="learning.streakCelebration.streakAA759EndDateMessage"
defaultMessage="Ends {date}."
values={{
date: new Date('2021-7-20 00:00').toLocaleDateString({ timeZone: 'UTC' }),
}}
/>
</div>
</div>
</Alert>
)}