From 3ef4daeccef0710adbaf08cb9761eeeb8789fdbf Mon Sep 17 00:00:00 2001 From: Thomas Tracy Date: Thu, 1 Jul 2021 15:27:21 -0400 Subject: [PATCH] feat: Add Scheduled content alert Adds a new alert to the outline page that informs the learner of content coming soon to the course. --- .../__factories__/outlineTabData.factory.js | 1 + .../data/__snapshots__/redux.test.js.snap | 2 + src/course-home/data/api.js | 3 ++ src/course-home/outline-tab/OutlineTab.jsx | 3 ++ .../outline-tab/OutlineTab.test.jsx | 41 ++++++++++++++++ .../CertificateStatusAlert.jsx | 4 +- .../ScheduledCotentAlert.jsx | 48 +++++++++++++++++++ .../alerts/scheduled-content-alert/hooks.js | 35 ++++++++++++++ .../alerts/scheduled-content-alert/index.js | 1 + .../__factories__/courseBlocks.factory.js | 7 ++- 10 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/course-home/outline-tab/alerts/scheduled-content-alert/ScheduledCotentAlert.jsx create mode 100644 src/course-home/outline-tab/alerts/scheduled-content-alert/hooks.js create mode 100644 src/course-home/outline-tab/alerts/scheduled-content-alert/index.js diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js index 21a9ecd0..0a740c3d 100644 --- a/src/course-home/data/__factories__/outlineTabData.factory.js +++ b/src/course-home/data/__factories__/outlineTabData.factory.js @@ -29,6 +29,7 @@ Factory.define('outlineTabData') upgrade_url: `${host}/dashboard`, })) .attrs({ + has_scheduled_content: null, access_expiration: null, can_show_upgrade_sock: false, cert_data: { diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index b6e46238..283e0455 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -382,6 +382,7 @@ Object { "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object { "effortActivities": undefined, "effortTime": undefined, + "hasScheduledContent": false, "id": "course-v1:edX+DemoX+Demo_Course_1", "sectionIds": Array [ "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", @@ -450,6 +451,7 @@ Object { }, "handoutsHtml": "", "hasEnded": undefined, + "hasScheduledContent": null, "id": "course-v1:edX+DemoX+Demo_Course_1", "offer": null, "resumeCourse": Object { diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index c06b6804..f1f59060 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -116,6 +116,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { id: courseId, title: block.display_name, sectionIds: block.children || [], + hasScheduledContent: block.has_scheduled_content, }; break; @@ -323,6 +324,7 @@ export async function getOutlineTabData(courseId) { const datesWidget = camelCaseObject(data.dates_widget); const enrollAlert = camelCaseObject(data.enroll_alert); const handoutsHtml = data.handouts_html; + const hasScheduledContent = data.has_scheduled_content; const hasEnded = data.has_ended; const offer = camelCaseObject(data.offer); const resumeCourse = camelCaseObject(data.resume_course); @@ -341,6 +343,7 @@ export async function getOutlineTabData(courseId) { datesWidget, enrollAlert, handoutsHtml, + hasScheduledContent, hasEnded, offer, resumeCourse, diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index d7d65715..a6183bfb 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -23,6 +23,7 @@ import useCertificateAvailableAlert from './alerts/certificate-status-alert'; import useCourseEndAlert from './alerts/course-end-alert'; import useCourseStartAlert from './alerts/course-start-alert'; import usePrivateCourseAlert from './alerts/private-course-alert'; +import useScheduledContentAlert from './alerts/scheduled-content-alert'; import { useModel } from '../../generic/model-store'; import WelcomeMessage from './widgets/WelcomeMessage'; import ProctoringInfoPanel from './widgets/ProctoringInfoPanel'; @@ -90,6 +91,7 @@ function OutlineTab({ intl }) { const courseEndAlert = useCourseEndAlert(courseId); const certificateAvailableAlert = useCertificateAvailableAlert(courseId); const privateCourseAlert = usePrivateCourseAlert(courseId); + const scheduledContentAlert = useScheduledContentAlert(courseId); const rootCourseId = courses && Object.keys(courses)[0]; @@ -152,6 +154,7 @@ function OutlineTab({ intl }) { ...certificateAvailableAlert, ...courseEndAlert, ...courseStartAlert, + ...scheduledContentAlert, }} /> )} diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index a0b81b59..138db7b4 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -694,6 +694,47 @@ describe('Outline Tab', () => { expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument(); }); }); + + describe('Scheduled Content Alert', () => { + it('appears correctly', async () => { + const now = new Date(); + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { hasScheduledContent: true }); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({ + course_blocks: { blocks: courseBlocks.blocks }, + date_blocks: [ + { + date_type: 'course-end-date', + date: tomorrow.toISOString(), + title: 'End', + }, + ], + }); + await fetchAndRender(); + expect(screen.queryByText('More content is coming soon!')).toBeInTheDocument(); + }); + }); + describe('Scheduled Content Alert not present without courseBlocks', () => { + it('appears correctly', async () => { + const now = new Date(); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({ + course_blocks: null, + date_blocks: [ + { + date_type: 'course-end-date', + date: tomorrow.toISOString(), + title: 'End', + }, + ], + }); + await fetchAndRender(); + expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument(); + expect(screen.queryByText('More content is coming soon!')).not.toBeInTheDocument(); + }); + }); }); describe('Certificate (web) Complete Alert', () => { diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx index 47a1be18..ac375ac7 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx @@ -111,14 +111,14 @@ function CertificateStatusAlert({ intl, payload }) { buttonMessage, }) => ( -
+
{header} {body}
{buttonVisible && ( -
+
+ )} +
+
+ + ); +} + +ScheduledContentAlert.propTypes = { + payload: PropTypes.shape({ + datesTabLink: PropTypes.string, + }).isRequired, +}; + +export default ScheduledContentAlert; diff --git a/src/course-home/outline-tab/alerts/scheduled-content-alert/hooks.js b/src/course-home/outline-tab/alerts/scheduled-content-alert/hooks.js new file mode 100644 index 00000000..fe29a220 --- /dev/null +++ b/src/course-home/outline-tab/alerts/scheduled-content-alert/hooks.js @@ -0,0 +1,35 @@ +import React, { useMemo } from 'react'; + +import { useAlert } from '../../../../generic/user-messages'; +import { useModel } from '../../../../generic/model-store'; + +const ScheduledContentAlert = React.lazy(() => import('./ScheduledCotentAlert')); + +const useScheduledContentAlert = (courseId) => { + const { + courseBlocks: { + courses, + }, + datesWidget: { + datesTabLink, + }, + } = useModel('outline', courseId); + + const hasScheduledContent = ( + !!courses + && !!Object.values(courses).find(course => course.hasScheduledContent === true) + ); + const { isEnrolled } = useModel('courseHomeMeta', courseId); + const payload = { + datesTabLink, + }; + useAlert(hasScheduledContent && isEnrolled, { + code: 'ScheduledContentAlert', + payload: useMemo(() => payload, Object.values(payload).sort()), + topic: 'outline-course-alerts', + }); + + return { ScheduledContentAlert }; +}; + +export default useScheduledContentAlert; diff --git a/src/course-home/outline-tab/alerts/scheduled-content-alert/index.js b/src/course-home/outline-tab/alerts/scheduled-content-alert/index.js new file mode 100644 index 00000000..ed12eb0b --- /dev/null +++ b/src/course-home/outline-tab/alerts/scheduled-content-alert/index.js @@ -0,0 +1 @@ +export { default } from './hooks'; diff --git a/src/shared/data/__factories__/courseBlocks.factory.js b/src/shared/data/__factories__/courseBlocks.factory.js index 79a54931..24d25a3e 100644 --- a/src/shared/data/__factories__/courseBlocks.factory.js +++ b/src/shared/data/__factories__/courseBlocks.factory.js @@ -124,7 +124,12 @@ export function buildMinimalCourseBlocks(courseId, title, options = {}) { )]; const courseBlock = options.courseBlock || Factory.build( 'block', - { type: 'course', display_name: title, children: sectionBlocks.map(block => block.id) }, + { + type: 'course', + display_name: title, + has_scheduled_content: options.hasScheduledContent || false, + children: sectionBlocks.map(block => block.id), + }, { courseId }, ); return {