Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/course-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
resumeBlock: block.resume_block,
sequenceIds: block.children || [],
hideFromTOC: block.hide_from_toc,
optionalCompletion: block.optional_completion,
};
break;

Expand All @@ -155,6 +156,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name,
hideFromTOC: block.hide_from_toc,
navigationDisabled: block.navigation_disabled,
optionalCompletion: block.optional_completion,
};
break;

Expand Down
10 changes: 9 additions & 1 deletion src/course-home/outline-tab/Section.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
import {
Badge, Collapsible, IconButton, Icon,
} from '@openedx/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';
Expand All @@ -25,6 +27,7 @@ const Section = ({
sequenceIds,
title,
hideFromTOC,
optionalCompletion,
} = section;
const {
courseBlocks: {
Expand Down Expand Up @@ -82,6 +85,11 @@ const Section = ({
)}
</div>
)}
{optionalCompletion && (
<Badge className="align-self-center text-uppercase pt-1 border" variant="light" data-testid="optional-completion-badge-outline-section">
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
</div>
);

Expand Down
10 changes: 8 additions & 2 deletions src/course-home/outline-tab/SequenceLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

import { Icon } from '@openedx/paragon';
import { Badge, Icon } from '@openedx/paragon';
import { Block } from '@openedx/paragon/icons';
import EffortEstimate from '../../shared/effort-estimate';
import { useModel } from '../../generic/model-store';
Expand All @@ -31,6 +31,7 @@ const SequenceLink = ({
showLink,
title,
hideFromTOC,
optionalCompletion,
} = sequence;
const {
userTimezone,
Expand Down Expand Up @@ -108,12 +109,17 @@ const SequenceLink = ({
/>
)}
</div>
<div className="col-10 p-0 ml-3 text-break">
<div className="d-flex justify-content-between col-11 p-0 ml-3 text-break">
<span className="align-middle">{displayTitle}</span>
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
</span>
<EffortEstimate className="ml-3 align-middle" block={sequence} />
{optionalCompletion && (
<Badge className="align-self-center text-uppercase mr-5 pt-1 border" variant="light" data-testid="optional-completion-badge-outline-subsection">
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
</div>
</div>
{hideFromTOC && (
Expand Down
5 changes: 5 additions & 0 deletions src/course-home/outline-tab/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ const messages = defineMessages({
defaultMessage: 'Open',
description: 'A button to open the given section of the course outline',
},
optionalCompletion: {
id: 'learning.outline.optionalBlock',
defaultMessage: 'Optional',
description: 'Used as a label to indicate that a section or sequence is optional.',
},
proctoringInfoPanel: {
id: 'learning.proctoringPanel.header',
defaultMessage: 'This course contains proctored exams',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@ 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';
import IncompleteDonutSegment from './IncompleteDonutSegment';
import LockedDonutSegment from './LockedDonutSegment';
import messages from './messages';

const CompletionDonutChart = ({ intl }) => {
const CompletionDonutChart = ({ intl, optional = false }) => {
const {
courseId,
} = useSelector(state => state.courseHome);

const {
completionSummary: {
completeCount,
incompleteCount,
lockedCount,
},
} = useModel('progress', courseId);
const progress = useModel('progress', courseId);
const completionSummary = progress?.completionSummary || {};
const completeCount = optional ? completionSummary.optionalCompleteCount : completionSummary.completeCount;
const incompleteCount = optional ? completionSummary.optionalIncompleteCount : completionSummary.incompleteCount;
const lockedCount = optional ? completionSummary.optionalLockedCount : completionSummary.lockedCount;

const numTotalUnits = completeCount + incompleteCount + lockedCount;
const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0;
Expand All @@ -30,6 +29,25 @@ const CompletionDonutChart = ({ intl }) => {

const isLocaleRtl = isRtl(getLocale());

if (optional && numTotalUnits === 0) {
return <></>;
}

const optionalTotalUnits = (
completionSummary.optionalCompleteCount
+ completionSummary.optionalIncompleteCount
+ completionSummary.optionalLockedCount
);

let label;
if (optional) {
label = intl.formatMessage(messages.optionalDonutLabel);
} else if (optionalTotalUnits > 0) {
label = intl.formatMessage(messages.requiredDonutLabel);
} else {
label = intl.formatMessage(messages.donutLabel);
}

return (
<>
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
Expand All @@ -42,7 +60,7 @@ const CompletionDonutChart = ({ intl }) => {
{completePercentage}{isLocaleRtl && '\u200f'}%
</text>
<text x="50%" y="50%" className="donut-chart-label">
{intl.formatMessage(messages.donutLabel)}
{label}
</text>
</g>
<IncompleteDonutSegment incompletePercentage={incompletePercentage} />
Expand All @@ -62,8 +80,13 @@ const CompletionDonutChart = ({ intl }) => {
);
};

CompletionDonutChart.defaultProps = {
optional: false,
};

CompletionDonutChart.propTypes = {
intl: intlShape.isRequired,
optional: PropTypes.bool,
};

export default injectIntl(CompletionDonutChart);
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const CourseCompletion = ({ intl }) => (
</div>
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
<CompletionDonutChart />
<CompletionDonutChart optional />
</div>
</div>
</section>
Expand Down
12 changes: 11 additions & 1 deletion src/course-home/progress-tab/course-completion/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ const messages = defineMessages({
donutLabel: {
id: 'progress.completion.donut.label',
defaultMessage: 'completed',
description: 'Label text for progress donut chart',
description: 'Label text for progress donut chart when only one donut is shown',
},
requiredDonutLabel: {
id: 'progress.completion.requiredDonut.label',
defaultMessage: 'required',
description: 'Label text for required progress donut chart when optional content is present',
},
optionalDonutLabel: {
id: 'progress.completion.optionalDonut.label',
defaultMessage: 'optional',
description: 'Label text for optional progress donut chart',
},
completionBody: {
id: 'progress.completion.body',
Expand Down
6 changes: 6 additions & 0 deletions src/courseware/course/sequence/Unit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const Unit = ({
const unit = useModel(modelKeys.units, id);
const isProcessing = unit.bookmarkedUpdateState === 'loading';
const view = authenticatedUser ? views.student : views.public;
const { optionalCompletion } = unit;

const getUrl = usePluginsCallback('getIFrameUrl', () => getIFrameUrl({
id,
Expand All @@ -51,6 +52,11 @@ const Unit = ({
isBookmarked={unit.bookmarked}
isProcessing={isProcessing}
/>
{optionalCompletion && (
<div className="alert alert-info small my-3" role="alert" data-testid="optional-completion-unit-alert">
{formatMessage(messages.optionalCompletionUnitAlert)}
</div>
)}
<UnitSuspense {...{ courseId, id }} />
<ContentIFrame
elementId="unit-iframe"
Expand Down
5 changes: 5 additions & 0 deletions src/courseware/course/sequence/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const messages = defineMessages({
defaultMessage: 'There is no content here.',
description: 'Message shown when there is no content to show a user inside a learning sequence.',
},
optionalCompletionUnitAlert: {
id: 'learn.optionalCompletionUnitAlert',
defaultMessage: 'This is optional content and will not affect your course score or completion.',
description: 'Alert message shown in a unit when the unit has optional completion.',
},
});

export default messages;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@openedx/paragon';
import { Badge, Button, Icon } from '@openedx/paragon';
import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons';

import courseOutlineMessages from '@src/course-home/outline-tab/messages';
Expand All @@ -16,6 +16,7 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
title,
sequenceIds,
completionStat,
optionalCompletion,
} = section;

const activeSequenceId = useSelector(getSequenceId);
Expand All @@ -26,13 +27,18 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
<div className="col-auto p-0">
<CompletionIcon completionStat={completionStat} />
</div>
<div className="col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
<div className="d-flex justify-content-between col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
{title}
<span className="sr-only">
, {intl.formatMessage(complete
? courseOutlineMessages.completedSection
: courseOutlineMessages.incompleteSection)}
</span>
{optionalCompletion && (
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-sidebar-section">
{intl.formatMessage(courseOutlineMessages.optionalCompletion)}
</Badge>
)}
</div>
</>
);
Expand Down Expand Up @@ -65,6 +71,7 @@ SidebarSection.propTypes = {
completed: PropTypes.number,
total: PropTypes.number,
}),
optionalCompletion: PropTypes.bool,
}).isRequired,
handleSelectSection: PropTypes.func.isRequired,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@openedx/paragon';
import { Badge, Collapsible } from '@openedx/paragon';

import courseOutlineMessages from '@src/course-home/outline-tab/messages';
import { getCourseOutline, getSequenceId } from '@src/courseware/data/selectors';
Expand All @@ -26,6 +26,7 @@ const SidebarSequence = ({
unitIds,
type,
completionStat,
optionalCompletion,
} = sequence;

const [open, setOpen] = useState(defaultOpen);
Expand All @@ -38,8 +39,15 @@ const SidebarSequence = ({
<div className="col-auto p-0" style={{ fontSize: '1.1rem' }}>
<CompletionIcon completionStat={completionStat} />
</div>
<div className="col-9 d-flex flex-column flex-grow-1 ml-3 mr-auto p-0 text-left">
<span className="align-middle text-dark-500">{title}</span>
<div className="col-10 d-flex flex-column flex-grow-1 ml-3 mr-auto p-0 text-left">
<span className="d-flex justify-content-between align-middle text-dark-500">
{title}
{optionalCompletion && (
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-sidebar-sequence">
{intl.formatMessage(courseOutlineMessages.optionalCompletion)}
</Badge>
)}
</span>
{specialExamInfo && <span className="align-middle small text-muted">{specialExamInfo}</span>}
<span className="sr-only">
, {intl.formatMessage(complete
Expand Down Expand Up @@ -94,6 +102,7 @@ SidebarSequence.propTypes = {
completed: PropTypes.number,
total: PropTypes.number,
}),
optionalCompletion: PropTypes.bool,
}).isRequired,
activeUnitId: PropTypes.string.isRequired,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { Badge } from '@openedx/paragon';

import { checkBlockCompletion } from '@src/courseware/data';
import { getCourseOutline } from '@src/courseware/data/selectors';
Expand All @@ -24,6 +25,7 @@ const SidebarUnit = ({
}) => {
const {
complete,
optionalCompletion,
title,
icon = UNIT_ICON_TYPES.other,
} = unit;
Expand Down Expand Up @@ -74,10 +76,15 @@ const SidebarUnit = ({
<div className="col-auto p-0">
<UnitIcon type={iconType} isCompleted={complete} />
</div>
<div className="col-10 p-0 ml-3 text-break">
<div className="d-flex justify-content-between col-10 p-0 ml-3 text-break">
<span className="align-middle">
{title}
</span>
{optionalCompletion && (
<Badge className="align-self-center text-uppercase mr-1 border" variant="light" data-testid="optional-completion-badge-sidebar-unit">
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
</span>
Expand All @@ -97,6 +104,7 @@ SidebarUnit.propTypes = {
id: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
optionalCompletion: PropTypes.bool,
}).isRequired,
isActive: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const messages = defineMessages({
defaultMessage: 'Incomplete unit',
description: 'Text used to describe the gray checkmark icon in front of a unit title',
},
optionalCompletion: {
id: 'learn.sequence.optionalBlock',
defaultMessage: 'Optional',
description: 'Used as a label to indicate that a unit is optional.',
},
});

export default messages;
Loading
Loading