feat: Breadcrumb Jump Navigation STAGE ONLY (#641)

Enable faster movement through the course content for learners and course instructors familiar with their course structure using jump navigation selectors in dropdown menus that augment our existing breadcrumbs in the learner sequence experience. When learners/instructors click on sections or subsections these menus are revealed and can be selected to jump to this part of the course.

Implemented using paragon's Selectmenu component, and data from the learning_sequences API.

Note: as the L_S api does not yet have completion data, we are holding off on accepting the completion ACs.

Smoke testing and QA testing will be required, as this feature is prominent in the learner experience. 

The feature is presently only rolled out on stage, but will FF to roll out to instructors on test soon.
This commit is contained in:
connorhaugh
2021-09-17 15:39:06 -04:00
committed by GitHub
parent 8b9dfd2f08
commit 8c0e98ad4f

View File

@@ -5,29 +5,57 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHome } from '@fortawesome/free-solid-svg-icons';
import { useSelector } from 'react-redux';
import { useModel } from '../../generic/model-store';
import { Hyperlink, MenuItem, SelectMenu } from '@edx/paragon';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useModel, useModels } from '../../generic/model-store';
/** [MM-P2P] Experiment */
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
function CourseBreadcrumb({
url, children, withSeparator, ...attrs
content, withSeparator,
}) {
const defaultContent = content.filter(destination => destination.default)[0];
const { administrator } = getAuthenticatedUser();
return (
<>
{withSeparator && (
<li className="mx-2 text-primary-500" role="presentation" aria-hidden>/</li>
)}
<li {...attrs}>
<a className="text-primary-500" href={url}>{children}</a>
<li>
{process.env.NODE_ENV !== 'test' || content.length < 2 || !administrator
? (
<a className="text-primary-500" href={defaultContent.url}>{defaultContent.label}
</a>
)
: (
<SelectMenu isLink defaultMessage={defaultContent.label}>
{content.map(item => (
<MenuItem
as={Hyperlink}
defaultSelected={item.default}
href={item.url}
>
{item.label}
</MenuItem>
))}
</SelectMenu>
)}
</li>
</>
);
}
CourseBreadcrumb.propTypes = {
url: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
content: PropTypes.arrayOf(
PropTypes.shape({
default: PropTypes.bool,
url: PropTypes.string,
id: PropTypes.string,
label: PropTypes.string,
}),
).isRequired,
withSeparator: PropTypes.bool,
};
@@ -43,51 +71,55 @@ export default function CourseBreadcrumbs({
mmp2p,
}) {
const course = useModel('coursewareMeta', courseId);
const sequence = useModel('sequences', sequenceId);
const section = useModel('sections', sectionId);
const courseStatus = useSelector(state => state.courseware.courseStatus);
const sections = Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, section]));
const possibleSequences = sections && sectionId ? sections[sectionId].sequenceIds : [];
const sequences = Object.fromEntries(useModels('sequences', possibleSequences).map(sequence => [sequence.id, sequence]));
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
const links = useMemo(() => {
const temp = [];
if (courseStatus === 'loaded' && sequenceStatus === 'loaded') {
return [section, sequence].filter(node => !!node).map((node) => ({
id: node.id,
label: node.title,
url: `${getConfig().LMS_BASE_URL}/courses/${course.id}/course/#${node.id}`,
}));
temp.push(course.sectionIds.map(id => ({
id,
label: sections[id].title,
default: (id === sectionId),
// navigate to first sequence in section, (TODO: navigate to first incomplete sequence in section)
url: `${getConfig().BASE_URL}/course/${courseId}/${sections[id].sequenceIds[0]}`,
})));
temp.push(sections[sectionId].sequenceIds.map(id => ({
id,
label: sequences[id].title,
default: id === sequenceId,
// first unit it section (TODO: navigate to first incomplete in sequence)
url: `${getConfig().BASE_URL}/course/${courseId}/${sequences[id].id}/${sequences[id].unitIds[0]}`,
})));
}
return [];
}, [courseStatus, sequenceStatus]);
return temp;
}, [courseStatus, sections, sequences]);
return (
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
<ol className="list-unstyled d-flex m-0">
<CourseBreadcrumb
url={`${getConfig().LMS_BASE_URL}/courses/${course.id}/course/`}
className="flex-shrink-0"
>
<FontAwesomeIcon icon={faHome} className="mr-2" />
<FormattedMessage
id="learn.breadcrumb.navigation.course.home"
description="The course home link in breadcrumbs nav"
defaultMessage="Course"
/>
</CourseBreadcrumb>
{links.map(({ id, url, label }) => (
<CourseBreadcrumb
key={id}
url={url}
withSeparator
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
<nav aria-label="breadcrumb" className="my-1 d-inline-block col-sm-10">
<ol className="list-unstyled d-flex align-items-center m-0">
<li>
<a
href={`${getConfig().LMS_BASE_URL}/courses/${course.id}/course/`}
className="flex-shrink-0 text-primary"
>
{label}
</CourseBreadcrumb>
<FontAwesomeIcon icon={faHome} className="mr-2" />
<FormattedMessage
id="learn.breadcrumb.navigation.course.home"
description="The course home link in breadcrumbs nav"
defaultMessage="Course"
/>
</a>
</li>
{links.map(content => (
<CourseBreadcrumb
content={content}
withSeparator
/>
))}
{/** [MM-P2P] Experiment */}
{mmp2p.state.isEnabled && (
<MMP2PFlyoverTrigger options={mmp2p} />