diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index 3265d8a1c4..3a26dc3c9d 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(messages.optionalContent)}
, {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/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx
index 54b6caa9c6..1f29ed3e94 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.optionalContent)}
{intl.formatMessage(messages.headerPlaceholder)}