feat: Add Scheduled content alert

Adds a new alert to the outline page that informs the learner of content
coming soon to the course.
This commit is contained in:
Thomas Tracy
2021-07-01 15:27:21 -04:00
committed by Albert (AJ) St. Aubin
parent d2573a16b1
commit 3ef4daecce
10 changed files with 142 additions and 3 deletions

View File

@@ -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: {

View File

@@ -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": "<ul><li>Handout 1</li></ul>",
"hasEnded": undefined,
"hasScheduledContent": null,
"id": "course-v1:edX+DemoX+Demo_Course_1",
"offer": null,
"resumeCourse": Object {

View File

@@ -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,

View File

@@ -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,
}}
/>
)}

View File

@@ -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', () => {

View File

@@ -111,14 +111,14 @@ function CertificateStatusAlert({ intl, payload }) {
buttonMessage,
}) => (
<Alert variant={variant}>
<div className="row justify-content-between align-items-center">
<div className="d-flex flex-column flex-lg-row justify-content-between align-items-center">
<div className={buttonVisible ? 'col-lg-8' : 'col-auto'}>
<FontAwesomeIcon icon={icon} className={iconClassName} />
<Alert.Heading>{header}</Alert.Heading>
{body}
</div>
{buttonVisible && (
<div className="m-auto m-lg-0 pr-lg-3">
<div className="flex-grow-0 pt-3 pt-lg-0">
<Button
variant="primary"
href={buttonLink}

View File

@@ -0,0 +1,48 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@edx/paragon';
import React from 'react';
import PropTypes from 'prop-types';
function ScheduledContentAlert({ payload }) {
const {
datesTabLink,
} = payload;
return (
<Alert variant="info">
<div className="d-flex flex-column flex-lg-row justify-content-between align-items-center">
<div className="col-lg-7">
<Alert.Heading>
<FormattedMessage
id="learning.outline.alert.scheduled-content.heading"
defaultMessage="More content is coming soon!"
/>
</Alert.Heading>
<FormattedMessage
id="learning.outline.alert.scheduled-content.body"
defaultMessage="This course will have more content released at a future date. Look out for email updates or check back on this course for updates."
/>
</div>
<div className="flex-grow-0 pt-3 pt-lg-0">
{datesTabLink && (
<Button>
<FormattedMessage
id="learning.outline.alert.scheduled-content.button"
defaultMessage="View Course Schedule"
href={datesTabLink}
/>
</Button>
)}
</div>
</div>
</Alert>
);
}
ScheduledContentAlert.propTypes = {
payload: PropTypes.shape({
datesTabLink: PropTypes.string,
}).isRequired,
};
export default ScheduledContentAlert;

View File

@@ -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;

View File

@@ -0,0 +1 @@
export { default } from './hooks';

View File

@@ -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 {