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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user