From 5786b5c8367c31797d8deb42517268ffadf1ecea Mon Sep 17 00:00:00 2001 From: Artur Gaspar Date: Fri, 17 Nov 2023 08:34:30 -0300 Subject: [PATCH] feat: legacy course navigation Add an option to enable the legacy course navigation where clicking a breadcrumb leads to the course index page highlighting the selected section. --- .env | 1 + .env.development | 1 + README.rst | 4 ++ src/course-home/outline-tab/OutlineTab.jsx | 13 ++++- .../outline-tab/OutlineTab.test.jsx | 22 +++++++++ src/course-home/outline-tab/Section.jsx | 8 +++- src/course-home/outline-tab/SequenceLink.jsx | 4 +- src/courseware/course/CourseBreadcrumbs.jsx | 15 +++--- .../course/CourseBreadcrumbs.test.jsx | 47 ++++++++++++++----- src/index.jsx | 1 + 10 files changed, 95 insertions(+), 21 deletions(-) diff --git a/.env b/.env index 62cf2ad666..c8064b4664 100644 --- a/.env +++ b/.env @@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL='' DISCUSSIONS_MFE_BASE_URL='' ECOMMERCE_BASE_URL='' ENABLE_JUMPNAV='true' +ENABLE_LEGACY_NAV='' ENABLE_NOTICES='' ENTERPRISE_LEARNER_PORTAL_HOSTNAME='' EXAMS_BASE_URL='' diff --git a/.env.development b/.env.development index 94f8a19332..f0cae5b828 100644 --- a/.env.development +++ b/.env.development @@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL='http://localhost:18381' DISCUSSIONS_MFE_BASE_URL='http://localhost:2002' ECOMMERCE_BASE_URL='http://localhost:18130' ENABLE_JUMPNAV='true' +ENABLE_LEGACY_NAV='' ENABLE_NOTICES='' ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734' EXAMS_BASE_URL='http://localhost:18740' diff --git a/README.rst b/README.rst index b1d2f39678..14b263a16b 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,10 @@ ENABLE_JUMPNAV This feature flag is slated to be removed as jumpnav becomes default. Follow the progress of this ticket here: https://openedx.atlassian.net/browse/TNL-8678 +ENABLE_LEGACY_NAV + Enables the legacy behaviour in the course breadcrumbs, where links lead to + the course index highlighting the selected course section. + SOCIAL_UTM_MILESTONE_CAMPAIGN This value is passed as the ``utm_campaign`` parameter for social-share links when celebrating learning milestones in the course. Optional. diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 072f0d04c6..d1a96b3da1 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -121,6 +121,13 @@ const OutlineTab = ({ intl }) => { } }, [location.search]); + // Remove the initial # sign. + const hashValue = location.hash.substring(1); + const selectedSectionId = courses[rootCourseId].sectionIds.find((sectionId) => ( + // Section is selected or contains selected subsection. + (hashValue === sectionId) || (sections[sectionId].sequenceIds.includes(hashValue)) + )); + return ( <>
@@ -171,7 +178,11 @@ const OutlineTab = ({ intl }) => {
diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index c6fc547a0c..8273d7a19c 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -107,6 +107,28 @@ describe('Outline Tab', () => { expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true'); }); + it('expands selected section', async () => { + const { courseBlocks, sectionBlocks } = await buildMinimalCourseBlocks(courseId, 'Title'); + setTabData({ + course_blocks: { blocks: courseBlocks.blocks }, + }); + window.location.hash = `${sectionBlocks[0].id}`; + await fetchAndRender(); + const expandedSectionNode = screen.getByRole('button', { name: /Title of Section/ }); + expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true'); + }); + + it('expands section that contains selected subsection', async () => { + const { courseBlocks, sequenceBlocks } = await buildMinimalCourseBlocks(courseId, 'Title'); + setTabData({ + course_blocks: { blocks: courseBlocks.blocks }, + }); + window.location.hash = `${sequenceBlocks[0].id}`; + await fetchAndRender(); + const expandedSectionNode = screen.getByRole('button', { name: /Title of Section/ }); + expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true'); + }); + it('handles expand/collapse all button click', async () => { await fetchAndRender(); // Button renders as "Expand All" diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index 3de888a89a..f723229320 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { useLocation } from 'react-router-dom'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Collapsible, IconButton } from '@edx/paragon'; import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; @@ -29,6 +31,9 @@ const Section = ({ sequences, }, } = useModel('outline', courseId); + // Remove the initial # sign. + const hashValue = useLocation().hash.substring(1); + const selected = hashValue === section.id; const [open, setOpen] = useState(defaultOpen); @@ -42,7 +47,7 @@ const Section = ({ }, []); const sectionTitle = ( -
+
{complete ? ( ))} diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx index 0530d53e66..88f007125c 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -22,6 +22,7 @@ const SequenceLink = ({ courseId, first, sequence, + selected, }) => { const { complete, @@ -86,7 +87,7 @@ const SequenceLink = ({ return (
  • -
    +
    {complete ? ( { const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '', sequences: [] }; + let defaultTo; + if (getConfig().ENABLE_LEGACY_NAV === 'true') { + defaultTo = `/course/${courseId}/home#${defaultContent.id}`; + } else if (defaultContent.sequences.length) { + defaultTo = `/course/${courseId}/${defaultContent.sequences[0].id}`; + } else { + defaultTo = `/course/${courseId}/${defaultContent.id}`; + } return ( <> {withSeparator && ( @@ -23,12 +31,7 @@ const CourseBreadcrumb = ({
  • { getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff ? ( - + {defaultContent.label} ) diff --git a/src/courseware/course/CourseBreadcrumbs.test.jsx b/src/courseware/course/CourseBreadcrumbs.test.jsx index 23285f1410..60b70ae617 100644 --- a/src/courseware/course/CourseBreadcrumbs.test.jsx +++ b/src/courseware/course/CourseBreadcrumbs.test.jsx @@ -106,23 +106,46 @@ describe('CourseBreadcrumbs', () => { }, ], ]); - render( - - - - , - , - ); it('renders course breadcrumbs as expected', async () => { + await render( + + + + , + , + ); expect(screen.queryAllByRole('link')).toHaveLength(1); const courseHomeButtonDestination = screen.getAllByRole('link')[0].href; expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home'); expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument(); expect(screen.queryAllByRole('button')).toHaveLength(2); }); + it('renders legacy navigation links as expected', async () => { + getConfig.mockImplementation(() => ({ + ENABLE_JUMPNAV: 'false', + ENABLE_LEGACY_NAV: 'true', + })); + await render( + + + + , + , + ); + expect(screen.queryAllByRole('link')).toHaveLength(3); + const sectionButtonDestination = screen.getAllByRole('link')[1].href; + expect(sectionButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home#block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'); + const sequenceButtonDestination = screen.getAllByRole('link')[2].href; + expect(sequenceButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home#block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'); + expect(screen.queryAllByRole('button')).toHaveLength(0); + }); }); diff --git a/src/index.jsx b/src/index.jsx index 7fec13b112..fe4dc124d2 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -123,6 +123,7 @@ initialize({ DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null, ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null, ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null, + ENABLE_LEGACY_NAV: process.env.ENABLE_LEGACY_NAV || null, ENABLE_NOTICES: process.env.ENABLE_NOTICES || null, INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null, SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,