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(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.headerPlaceholder)}