From d76c0cc6ea7b0e2465f5f9b11f15d19ec9053adb Mon Sep 17 00:00:00 2001 From: Ihor Romaniuk Date: Thu, 30 May 2024 18:28:07 +0200 Subject: [PATCH] feat: [FC-0056] courseware sidebar enhancement (#1398) - Display section and sequence progress - Add tracking event to the unit button - Hide the horizontal unit navigation with enabled sidebar navigation --- src/courseware/course/sequence/Sequence.jsx | 75 ++++++++++--------- .../course/sequence/Sequence.test.jsx | 30 +++++--- .../sequence-navigation/UnitNavigation.jsx | 9 ++- .../course/sidebar/SidebarTriggers.jsx | 2 +- .../course/sidebar/common/SidebarBase.jsx | 2 +- .../course-outline/CourseOutlineTray.jsx | 2 +- .../course-outline/CourseOutlineTray.scss | 1 - .../course-outline/CourseOutlineTrigger.jsx | 4 +- .../components/CompletionIcon.jsx | 30 ++++++++ .../components/CompletionIcon.test.jsx | 23 ++++++ .../components/SidebarSection.jsx | 14 ++-- .../components/SidebarSection.test.jsx | 12 +-- .../components/SidebarSequence.jsx | 12 +-- .../components/SidebarSequence.test.jsx | 2 +- .../course-outline/components/SidebarUnit.jsx | 29 ++++++- .../components/SidebarUnit.test.jsx | 28 ++++++- .../course-outline/icons/DashedCircleIcon.jsx | 40 ++++++++++ .../sidebars/course-outline/icons/index.js | 2 + src/courseware/data/slice.js | 10 +++ src/courseware/data/utils.js | 8 ++ src/index.scss | 27 ++++++- 21 files changed, 288 insertions(+), 74 deletions(-) create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/icons/DashedCircleIcon.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/icons/index.js diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx index d847c85656..b54954dd9e 100644 --- a/src/courseware/course/sequence/Sequence.jsx +++ b/src/courseware/course/sequence/Sequence.jsx @@ -15,6 +15,7 @@ import PageLoading from '@src/generic/PageLoading'; import { useModel } from '@src/generic/model-store'; import { useSequenceBannerTextAlert, useSequenceEntranceExamAlert } from '@src/alerts/sequence-alerts/hooks'; +import { getCoursewareOutlineSidebarSettings } from '../../data/selectors'; import CourseLicense from '../course-license'; import Sidebar from '../sidebar/Sidebar'; import NewSidebar from '../new-sidebar/Sidebar'; @@ -49,6 +50,7 @@ const Sequence = ({ const unit = useModel('units', unitId); const sequenceStatus = useSelector(state => state.courseware.sequenceStatus); const sequenceMightBeUnit = useSelector(state => state.courseware.sequenceMightBeUnit); + const { enableNavigationSidebar: isEnabledOutlineSidebar } = useSelector(getCoursewareOutlineSidebarSettings); const handleNext = () => { const nextIndex = sequence.unitIds.indexOf(unitId) + 1; @@ -144,33 +146,50 @@ const Sequence = ({ const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated; + const renderUnitNavigation = (isAtTop) => ( + { + logEvent('edx.ui.lms.sequence.previous_selected', 'bottom'); + handlePrevious(); + }} + onClickNext={() => { + logEvent('edx.ui.lms.sequence.next_selected', 'bottom'); + handleNext(); + }} + /> + ); + const defaultContent = ( <>
-
- { - logEvent('edx.ui.lms.sequence.next_selected', 'top'); - handleNext(); - }} - onNavigate={(destinationUnitId) => { - logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId); - handleNavigate(destinationUnitId); - }} - previousHandler={() => { - logEvent('edx.ui.lms.sequence.previous_selected', 'top'); - handlePrevious(); - }} - /> -
+ {!isEnabledOutlineSidebar && ( +
+ { + logEvent('edx.ui.lms.sequence.next_selected', 'top'); + handleNext(); + }} + onNavigate={(destinationUnitId) => { + logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId); + handleNavigate(destinationUnitId); + }} + previousHandler={() => { + logEvent('edx.ui.lms.sequence.previous_selected', 'top'); + handlePrevious(); + }} + /> +
+ )} -
+
- {unitHasLoaded && ( - { - logEvent('edx.ui.lms.sequence.previous_selected', 'bottom'); - handlePrevious(); - }} - onClickNext={() => { - logEvent('edx.ui.lms.sequence.next_selected', 'bottom'); - handleNext(); - }} - /> - )} + {unitHasLoaded && renderUnitNavigation(false)}
{isNewDiscussionSidebarViewEnabled ? : } @@ -216,6 +222,7 @@ const Sequence = ({ originalUserIsStaff={originalUserIsStaff} canAccessProctoredExams={canAccessProctoredExams} > + {isEnabledOutlineSidebar && renderUnitNavigation(true)} {defaultContent} diff --git a/src/courseware/course/sequence/Sequence.test.jsx b/src/courseware/course/sequence/Sequence.test.jsx index 1b0e2579e2..8bd7e5d6c0 100644 --- a/src/courseware/course/sequence/Sequence.test.jsx +++ b/src/courseware/course/sequence/Sequence.test.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import PropTypes from 'prop-types'; import { Factory } from 'rosie'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; @@ -25,6 +24,7 @@ describe('Sequence', () => { { type: 'vertical' }, { courseId: courseMetadata.id }, )); + const enableNavigationSidebar = { enable_navigation_sidebar: false }; beforeAll(async () => { const store = await initializeTestStore({ courseMetadata, unitBlocks }); @@ -92,7 +92,11 @@ describe('Sequence', () => { { courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] }, )]; const testStore = await initializeTestStore({ - courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata, + courseMetadata, + unitBlocks, + sequenceBlocks, + sequenceMetadata, + enableNavigationSidebar: { enable_navigation_sidebar: true }, }, false); const { container } = render( , @@ -102,8 +106,8 @@ describe('Sequence', () => { await waitFor(() => expect(screen.queryByText('Loading locked content messaging...')).toBeInTheDocument()); // `Previous`, `Prerequisite` and `Close Tray` buttons. expect(screen.getAllByRole('button').length).toEqual(3); - // `Active` and `Next` buttons. - expect(screen.getAllByRole('link').length).toEqual(2); + // `Next` button. + expect(screen.getAllByRole('link').length).toEqual(1); expect(screen.getByText('Content Locked')).toBeInTheDocument(); const unitContainer = container.querySelector('.unit-container'); @@ -125,7 +129,7 @@ describe('Sequence', () => { { courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] }, )]; const testStore = await initializeTestStore({ - courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata, + courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata, enableNavigationSidebar, }, false); render( , @@ -156,14 +160,16 @@ describe('Sequence', () => { expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument(); // `Previous`, `Prerequisite` and `Close Tray` buttons. expect(screen.getAllByRole('button')).toHaveLength(3); - // Renders `Next` button plus one button for each unit. - expect(screen.getAllByRole('link')).toHaveLength(1 + unitBlocks.length); + // Renders `Next` button. + expect(screen.getAllByRole('link')).toHaveLength(1); loadUnit(); await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument()); // At this point there will be 2 `Previous` and 2 `Next` buttons. expect(screen.getAllByRole('button', { name: /previous/i }).length).toEqual(2); expect(screen.getAllByRole('link', { name: /next/i }).length).toEqual(2); + // Renders two `Next` buttons for top and bottom unit navigations. + expect(screen.getAllByRole('link')).toHaveLength(2); }); describe('sequence and unit navigation buttons', () => { @@ -179,7 +185,9 @@ describe('Sequence', () => { )]; beforeAll(async () => { - testStore = await initializeTestStore({ courseMetadata, unitBlocks, sequenceBlocks }, false); + testStore = await initializeTestStore({ + courseMetadata, unitBlocks, sequenceBlocks, enableNavigationSidebar, + }, false); }); beforeEach(() => { @@ -340,7 +348,11 @@ describe('Sequence', () => { { courseId: courseMetadata.id, unitBlocks: block.children.length ? unitBlocks : [], sequenceBlock: block }, )); const innerTestStore = await initializeTestStore({ - courseMetadata, unitBlocks, sequenceBlocks: testSequenceBlocks, sequenceMetadata: testSequenceMetadata, + courseMetadata, + unitBlocks, + sequenceBlocks: testSequenceBlocks, + sequenceMetadata: testSequenceMetadata, + enableNavigationSidebar, }, false); const testData = { ...mockData, diff --git a/src/courseware/course/sequence/sequence-navigation/UnitNavigation.jsx b/src/courseware/course/sequence/sequence-navigation/UnitNavigation.jsx index d2cf2d0923..9a338d58f0 100644 --- a/src/courseware/course/sequence/sequence-navigation/UnitNavigation.jsx +++ b/src/courseware/course/sequence/sequence-navigation/UnitNavigation.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import classNames from 'classnames'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import { Button } from '@openedx/paragon'; @@ -21,6 +21,7 @@ const UnitNavigation = ({ unitId, onClickPrevious, onClickNext, + isAtTop, }) => { const { isFirstUnit, isLastUnit, nextLink, previousLink, @@ -33,7 +34,7 @@ const UnitNavigation = ({ return (