From 8c0e98ad4fefe166ac64d4e0bda7d9b0c535b594 Mon Sep 17 00:00:00 2001 From: connorhaugh <49422820+connorhaugh@users.noreply.github.com> Date: Fri, 17 Sep 2021 15:39:06 -0400 Subject: [PATCH] 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. --- src/courseware/course/CourseBreadcrumbs.jsx | 116 +++++++++++++------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/src/courseware/course/CourseBreadcrumbs.jsx b/src/courseware/course/CourseBreadcrumbs.jsx index d8a73c8d..e6b4e85b 100644 --- a/src/courseware/course/CourseBreadcrumbs.jsx +++ b/src/courseware/course/CourseBreadcrumbs.jsx @@ -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 && (
  • /
  • )} -
  • - {children} +
  • + {process.env.NODE_ENV !== 'test' || content.length < 2 || !administrator + ? ( + {defaultContent.label} + + ) + : ( + + {content.map(item => ( + + {item.label} + + ))} + + )} +
  • ); } 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 ( -