diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js index 1ff83241ce..839837e373 100644 --- a/src/course-home/data/__factories__/progressTabData.factory.js +++ b/src/course-home/data/__factories__/progressTabData.factory.js @@ -12,6 +12,11 @@ Factory.define('progressTabData') incomplete_count: 1, locked_count: 0, }, + optional_completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 0, + }, course_grade: { letter_grade: 'pass', percent: 1, diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 4644d7b142..3f8b64924f 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -428,6 +428,7 @@ Object { "complete": false, "courseId": "course-v1:edX+DemoX+Demo_Course", "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", + "optional": undefined, "resumeBlock": false, "sequenceIds": Array [ "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", @@ -444,6 +445,7 @@ Object { "effortTime": 15, "icon": null, "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", + "optional": undefined, "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "showLink": true, "title": "Title of Sequence", @@ -636,6 +638,11 @@ Object { }, "hasScheduledContent": false, "id": "course-v1:edX+DemoX+Demo_Course", + "optionalCompletionSummary": Object { + "completeCount": 1, + "incompleteCount": 1, + "lockedCount": 0, + }, "sectionScores": Array [ Object { "displayName": "First section", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 3265d8a1c4..76fd785d32 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -136,6 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { title: block.display_name, resumeBlock: block.resume_block, sequenceIds: block.children || [], + optional: block.optional_content, }; break; @@ -152,6 +153,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { // link in the outline (even though we ignore the given url and use an internal to ourselves). showLink: !!block.lms_web_url, title: block.display_name, + optional: block.optional_content, }; break; diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index 3de888a89a..668d3b2cc6 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Collapsible, IconButton } from '@edx/paragon'; +import { Badge, Collapsible, IconButton } from '@edx/paragon'; import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -23,6 +23,7 @@ const Section = ({ complete, sequenceIds, title, + optional, } = section; const { courseBlocks: { @@ -64,6 +65,7 @@ const Section = ({
{title} + , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)} diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx index 0530d53e66..a2052ebb1f 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -29,6 +29,7 @@ const SequenceLink = ({ due, showLink, title, + optional, } = sequence; const { userTimezone, @@ -115,6 +116,9 @@ const SequenceLink = ({
+ + {optional ? intl.formatMessage(messages.optionalContent) : ''} + {due ? dueDateMessage : noDueDateMessage} diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index 8d3204594f..98a76ffe5d 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -99,6 +99,11 @@ const messages = defineMessages({ defaultMessage: 'Open', description: 'A button to open the given section of the course outline', }, + optionalContent: { + id: 'learning.outline.optionalBlock', + defaultMessage: 'Optional', + description: 'Used as a label to indicate that a section, sequence, or unit is optional.', + }, proctoringInfoPanel: { id: 'learning.proctoringPanel.header', defaultMessage: 'This course contains proctored exams', diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx index 9f6dc3dbf5..37cc3c8a1c 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -262,6 +262,11 @@ describe('Progress Tab', () => { incomplete_count: 1, locked_count: 1, }, + optional_completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 0, + }, verified_mode: { access_expiration_date: '2050-01-01T12:00:00', currency: 'USD', @@ -304,6 +309,11 @@ describe('Progress Tab', () => { incomplete_count: 1, locked_count: 1, }, + optional_completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 0, + }, verified_mode: { access_expiration_date: '2050-01-01T12:00:00', currency: 'USD', @@ -364,6 +374,11 @@ describe('Progress Tab', () => { incomplete_count: 1, locked_count: 1, }, + optional_completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 0, + }, section_scores: [ { display_name: 'First section', @@ -402,6 +417,11 @@ describe('Progress Tab', () => { incomplete_count: 1, locked_count: 1, }, + optional_completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 0, + }, verified_mode: { access_expiration_date: '2050-01-01T12:00:00', currency: 'USD', diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx index 54b6caa9c6..a5e4ae08ed 100644 --- a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx +++ b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { getLocale, injectIntl, intlShape, isRtl, } from '@edx/frontend-platform/i18n'; +import PropTypes from 'prop-types'; import { useModel } from '../../../generic/model-store'; import CompleteDonutSegment from './CompleteDonutSegment'; @@ -10,18 +11,20 @@ import IncompleteDonutSegment from './IncompleteDonutSegment'; import LockedDonutSegment from './LockedDonutSegment'; import messages from './messages'; -const CompletionDonutChart = ({ intl }) => { +const CompletionDonutChart = ({ intl, optional }) => { const { courseId, } = useSelector(state => state.courseHome); + const key = optional ? 'optionalCompletionSummary' : 'completionSummary'; + const label = optional ? intl.formatMessage(messages.optionalDonutLabel) : intl.formatMessage(messages.donutLabel); + + const progress = useModel('progress', courseId); const { - completionSummary: { - completeCount, - incompleteCount, - lockedCount, - }, - } = useModel('progress', courseId); + completeCount, + incompleteCount, + lockedCount, + } = progress[key]; const numTotalUnits = completeCount + incompleteCount + lockedCount; const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0; @@ -30,6 +33,10 @@ const CompletionDonutChart = ({ intl }) => { const isLocaleRtl = isRtl(getLocale()); + if (optional && numTotalUnits === 0) { + return <>; + } + return ( <>
- + +
diff --git a/src/course-home/progress-tab/course-completion/messages.js b/src/course-home/progress-tab/course-completion/messages.js index 08bb8f59db..81151ec8ab 100644 --- a/src/course-home/progress-tab/course-completion/messages.js +++ b/src/course-home/progress-tab/course-completion/messages.js @@ -6,6 +6,11 @@ const messages = defineMessages({ defaultMessage: 'completed', description: 'Label text for progress donut chart', }, + optionalDonutLabel: { + id: 'progress.completion.optionalDonut.label', + defaultMessage: 'optional', + description: 'Label text for optional progress donut chart', + }, completionBody: { id: 'progress.completion.body', defaultMessage: 'This represents how much of the course content you have completed. Note that some content may not yet be released.', diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx index 1926a36c46..64755242b5 100644 --- a/src/courseware/course/sequence/Unit.jsx +++ b/src/courseware/course/sequence/Unit.jsx @@ -1,7 +1,7 @@ import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { AppContext, ErrorPage } from '@edx/frontend-platform/react'; -import { Modal } from '@edx/paragon'; +import { Modal, Badge } from '@edx/paragon'; import PropTypes from 'prop-types'; import React, { Suspense, useCallback, useContext, useEffect, useLayoutEffect, useState, @@ -155,6 +155,7 @@ const Unit = ({ return (

{unit.title}

+

{intl.formatMessage(messages.headerPlaceholder)}