Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Terry
d67f46865b AA-279: Add course expired alert to outline
It was previously only used in the courseware. But to match the
LMS, we also want to show it on the outline tab.
2020-08-04 14:09:01 -04:00
Michael Terry
ef38667751 AA-278: Add offer alert to outline
It was previously only used in the courseware. But to match the
LMS, we also want to show it on the outline tab.
2020-08-04 13:16:11 -04:00
11 changed files with 61 additions and 41 deletions

View File

@@ -1,14 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert } from '../../generic/user-messages'; import { Alert, ALERT_TYPES } from '../../generic/user-messages';
function AccessExpirationAlert({ payload }) { function AccessExpirationAlert({ payload }) {
const { const {
rawHtml, rawHtml,
} = payload; } = payload;
return rawHtml && ( return rawHtml && (
<Alert type="info"> <Alert type={ALERT_TYPES.INFO}>
{/* eslint-disable-next-line react/no-danger */}
<div dangerouslySetInnerHTML={{ __html: rawHtml }} /> <div dangerouslySetInnerHTML={{ __html: rawHtml }} />
</Alert> </Alert>
); );

View File

@@ -1,18 +1,21 @@
/* eslint-disable import/prefer-default-export */ import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { useModel } from '../../generic/model-store';
import { useAlert } from '../../generic/user-messages'; import { useAlert } from '../../generic/user-messages';
export function useAccessExpirationAlert(courseId) { const AccessExpirationAlert = React.lazy(() => import('./AccessExpirationAlert'));
const course = useModel('courses', courseId);
const rawHtml = (course && course.courseExpiredMessage) || null; function useAccessExpirationAlert(courseExpiredMessage, topic) {
const rawHtml = courseExpiredMessage || null;
const isVisible = !!rawHtml; // If it exists, show it. const isVisible = !!rawHtml; // If it exists, show it.
const payload = useMemo(() => ({ rawHtml }), [rawHtml]); const payload = useMemo(() => ({ rawHtml }), [rawHtml]);
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientAccessExpirationAlert', code: 'clientAccessExpirationAlert',
topic: 'course',
payload, payload,
topic,
}); });
return { clientAccessExpirationAlert: AccessExpirationAlert };
} }
export default useAccessExpirationAlert;

View File

@@ -1,2 +1 @@
export { default as AccessExpirationAlert } from './AccessExpirationAlert'; export { default } from './hooks';
export { useAccessExpirationAlert } from './hooks';

View File

@@ -1,14 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert } from '../../generic/user-messages'; import { Alert, ALERT_TYPES } from '../../generic/user-messages';
function OfferAlert({ payload }) { function OfferAlert({ payload }) {
const { const {
rawHtml, rawHtml,
} = payload; } = payload;
return rawHtml && ( return rawHtml && (
<Alert type="info"> <Alert type={ALERT_TYPES.INFO}>
{/* eslint-disable-next-line react/no-danger */}
<div dangerouslySetInnerHTML={{ __html: rawHtml }} /> <div dangerouslySetInnerHTML={{ __html: rawHtml }} />
</Alert> </Alert>
); );

View File

@@ -1,15 +1,19 @@
/* eslint-disable import/prefer-default-export */ import React from 'react';
import { useModel } from '../../generic/model-store';
import { useAlert } from '../../generic/user-messages'; import { useAlert } from '../../generic/user-messages';
export function useOfferAlert(courseId) { const OfferAlert = React.lazy(() => import('./OfferAlert'));
const course = useModel('courses', courseId);
const rawHtml = (course && course.offerHtml) || null; export function useOfferAlert(offerHtml, topic) {
const rawHtml = offerHtml || null;
const isVisible = !!rawHtml; // if it exists, show it. const isVisible = !!rawHtml; // if it exists, show it.
useAlert(isVisible, { useAlert(isVisible, {
code: 'clientOfferAlert', code: 'clientOfferAlert',
topic: 'course', topic,
payload: { rawHtml }, payload: { rawHtml },
}); });
return { clientOfferAlert: OfferAlert };
} }
export default useOfferAlert;

View File

@@ -1,2 +1 @@
export { default as OfferAlert } from './OfferAlert'; export { default } from './hooks';
export { useOfferAlert } from './hooks';

View File

@@ -20,4 +20,5 @@ Factory.define('outlineTabData')
can_enroll: true, can_enroll: true,
extra_text: 'Contact the administrator.', extra_text: 'Contact the administrator.',
}) })
.attr('handouts_html', [], () => '<ul><li>Handout 1</li></ul>'); .attr('handouts_html', [], () => '<ul><li>Handout 1</li></ul>')
.attr('offer_html', [], () => '<div>Great offer here</div>');

View File

@@ -204,6 +204,7 @@ Object {
}, },
"handoutsHtml": "<ul><li>Handout 1</li></ul>", "handoutsHtml": "<ul><li>Handout 1</li></ul>",
"id": "course-v1:edX+DemoX+Demo_Course_1", "id": "course-v1:edX+DemoX+Demo_Course_1",
"offerHtml": "<div>Great offer here</div>",
"welcomeMessageHtml": undefined, "welcomeMessageHtml": undefined,
}, },
}, },

View File

@@ -69,18 +69,22 @@ export async function getOutlineTabData(courseId) {
data, data,
} = tabData; } = tabData;
const courseBlocks = normalizeBlocks(courseId, data.course_blocks.blocks); const courseBlocks = normalizeBlocks(courseId, data.course_blocks.blocks);
const courseExpiredHtml = data.course_expired_html;
const courseTools = camelCaseObject(data.course_tools); const courseTools = camelCaseObject(data.course_tools);
const datesWidget = camelCaseObject(data.dates_widget); const datesWidget = camelCaseObject(data.dates_widget);
const enrollAlert = camelCaseObject(data.enroll_alert); const enrollAlert = camelCaseObject(data.enroll_alert);
const handoutsHtml = data.handouts_html; const handoutsHtml = data.handouts_html;
const offerHtml = data.offer_html;
const welcomeMessageHtml = data.welcome_message_html; const welcomeMessageHtml = data.welcome_message_html;
return { return {
courseTools,
courseBlocks, courseBlocks,
courseExpiredHtml,
courseTools,
datesWidget, datesWidget,
enrollAlert, enrollAlert,
handoutsHtml, handoutsHtml,
offerHtml,
welcomeMessageHtml, welcomeMessageHtml,
}; };
} }

View File

@@ -10,11 +10,13 @@ import CourseHandouts from './widgets/CourseHandouts';
import CourseTools from './widgets/CourseTools'; import CourseTools from './widgets/CourseTools';
import messages from './messages'; import messages from './messages';
import Section from './Section'; import Section from './Section';
import useAccessExpirationAlert from '../../alerts/access-expiration-alert';
import useCertificateAvailableAlert from './alerts/certificate-available-alert'; import useCertificateAvailableAlert from './alerts/certificate-available-alert';
import useCourseEndAlert from './alerts/course-end-alert'; import useCourseEndAlert from './alerts/course-end-alert';
import useCourseStartAlert from './alerts/course-start-alert'; import useCourseStartAlert from './alerts/course-start-alert';
import useEnrollmentAlert from '../../alerts/enrollment-alert'; import useEnrollmentAlert from '../../alerts/enrollment-alert';
import useLogistrationAlert from '../../alerts/logistration-alert'; import useLogistrationAlert from '../../alerts/logistration-alert';
import useOfferAlert from '../../alerts/offer-alert';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';
import WelcomeMessage from './widgets/WelcomeMessage'; import WelcomeMessage from './widgets/WelcomeMessage';
@@ -38,13 +40,20 @@ function OutlineTab({ intl }) {
courses, courses,
sections, sections,
}, },
courseExpiredHtml,
offerHtml,
} = useModel('outline', courseId); } = useModel('outline', courseId);
const certificateAvailableAlert = useCertificateAvailableAlert(courseId); // Above the tab alerts (appearing in the order listed here)
const courseEndAlert = useCourseEndAlert(courseId);
const courseStartAlert = useCourseStartAlert(courseId);
const enrollmentAlert = useEnrollmentAlert(courseId);
const logistrationAlert = useLogistrationAlert(); const logistrationAlert = useLogistrationAlert();
const enrollmentAlert = useEnrollmentAlert(courseId);
// Below the course title alerts (appearing in the order listed here)
const offerAlert = useOfferAlert(offerHtml, 'outline-course-alerts');
const accessExpirationAlert = useAccessExpirationAlert(courseExpiredHtml, 'outline-course-alerts');
const courseStartAlert = useCourseStartAlert(courseId);
const courseEndAlert = useCourseEndAlert(courseId);
const certificateAvailableAlert = useCertificateAvailableAlert(courseId);
const rootCourseId = Object.keys(courses)[0]; const rootCourseId = Object.keys(courses)[0];
const { sectionIds } = courses[rootCourseId]; const { sectionIds } = courses[rootCourseId];
@@ -70,9 +79,11 @@ function OutlineTab({ intl }) {
topic="outline-course-alerts" topic="outline-course-alerts"
className="mb-3" className="mb-3"
customAlerts={{ customAlerts={{
...accessExpirationAlert,
...certificateAvailableAlert, ...certificateAvailableAlert,
...courseEndAlert, ...courseEndAlert,
...courseStartAlert, ...courseStartAlert,
...offerAlert,
}} }}
/> />
{sectionIds.map((sectionId) => ( {sectionIds.map((sectionId) => (

View File

@@ -5,8 +5,8 @@ import { useDispatch } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { AlertList } from '../../generic/user-messages'; import { AlertList } from '../../generic/user-messages';
import { useAccessExpirationAlert } from '../../alerts/access-expiration-alert'; import useAccessExpirationAlert from '../../alerts/access-expiration-alert';
import { useOfferAlert } from '../../alerts/offer-alert'; import useOfferAlert from '../../alerts/offer-alert';
import Sequence from './sequence'; import Sequence from './sequence';
@@ -16,13 +16,6 @@ import CourseSock from './course-sock';
import ContentTools from './content-tools'; import ContentTools from './content-tools';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';
// Note that we import from the component files themselves in the enrollment-alert package.
// This is because Reacy.lazy() requires that we import() from a file with a Component as it's
// default export.
// See React.lazy docs here: https://reactjs.org/docs/code-splitting.html#reactlazy
const AccessExpirationAlert = React.lazy(() => import('../../alerts/access-expiration-alert/AccessExpirationAlert'));
const OfferAlert = React.lazy(() => import('../../alerts/offer-alert/OfferAlert'));
function Course({ function Course({
courseId, courseId,
sequenceId, sequenceId,
@@ -41,15 +34,18 @@ function Course({
course, course,
].filter(element => element != null).map(element => element.title); ].filter(element => element != null).map(element => element.title);
useOfferAlert(courseId);
useAccessExpirationAlert(courseId);
const { const {
canShowUpgradeSock, canShowUpgradeSock,
celebrations, celebrations,
courseExpiredMessage,
offerHtml,
verifiedMode, verifiedMode,
} = course; } = course;
// Below the tabs, above the breadcrumbs alerts (appearing in the order listed here)
const offerAlert = useOfferAlert(offerHtml, 'course');
const accessExpirationAlert = useAccessExpirationAlert(courseExpiredMessage, 'course');
const dispatch = useDispatch(); const dispatch = useDispatch();
const celebrateFirstSection = celebrations && celebrations.firstSection; const celebrateFirstSection = celebrations && celebrations.firstSection;
const celebrationOpen = shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFirstSection, dispatch); const celebrationOpen = shouldCelebrateOnSectionLoad(courseId, sequenceId, unitId, celebrateFirstSection, dispatch);
@@ -63,8 +59,8 @@ function Course({
className="my-3" className="my-3"
topic="course" topic="course"
customAlerts={{ customAlerts={{
clientAccessExpirationAlert: AccessExpirationAlert, ...accessExpirationAlert,
clientOfferAlert: OfferAlert, ...offerAlert,
}} }}
/> />
<CourseBreadcrumbs <CourseBreadcrumbs