From 24db8c162baccadf885be0c5a0280c8eddfb7008 Mon Sep 17 00:00:00 2001 From: Ihor Romaniuk Date: Mon, 11 Mar 2024 09:58:41 +0100 Subject: [PATCH] feat: [FC-0056] create course outline sidebar --- package-lock.json | 15 +- package.json | 2 +- .../data/__snapshots__/redux.test.js.snap | 12 ++ src/course-home/outline-tab/SequenceLink.jsx | 19 +- src/courseware/course/Course.jsx | 36 ++-- src/courseware/course/Course.test.jsx | 38 ++-- src/courseware/course/CourseBreadcrumbs.jsx | 2 +- src/courseware/course/sequence/Sequence.jsx | 66 +++--- .../course/sequence/Sequence.test.jsx | 92 ++++---- src/courseware/course/sidebar/Sidebar.jsx | 31 +-- .../course/sidebar/SidebarContextProvider.jsx | 40 +++- .../course/sidebar/SidebarTriggers.jsx | 4 +- .../course/sidebar/common/SidebarBase.jsx | 22 +- .../course/sidebar/common/SidebarBase.scss | 6 + .../course-outline/CourseOutlineTray.jsx | 156 ++++++++++++++ .../course-outline/CourseOutlineTray.scss | 104 +++++++++ .../course-outline/CourseOutlineTray.test.jsx | 119 ++++++++++ .../course-outline/CourseOutlineTrigger.jsx | 52 +++++ .../CourseOutlineTrigger.test.jsx | 109 ++++++++++ .../components/SidebarSection.jsx | 70 ++++++ .../components/SidebarSection.test.jsx | 67 ++++++ .../components/SidebarSequence.jsx | 99 +++++++++ .../components/SidebarSequence.test.jsx | 84 ++++++++ .../course-outline/components/SidebarUnit.jsx | 76 +++++++ .../components/SidebarUnit.test.jsx | 81 +++++++ .../course-outline/components/UnitIcon.jsx | 56 +++++ .../components/UnitIcon.test.jsx | 21 ++ .../sidebars/course-outline/constants.js | 2 + .../sidebar/sidebars/course-outline/hooks.jsx | 59 +++++ .../sidebar/sidebars/course-outline/index.js | 3 + .../sidebars/course-outline/messages.js | 31 +++ .../discussions/DiscussionsSidebar.jsx | 12 +- .../discussions/DiscussionsTrigger.jsx | 6 +- .../notifications/NotificationTray.jsx | 12 +- .../notifications/NotificationTrigger.jsx | 7 +- src/courseware/course/test-utils.jsx | 15 +- src/courseware/data/api.js | 177 +++------------ src/courseware/data/redux.test.js | 84 +++++++- src/courseware/data/selectors.js | 19 +- src/courseware/data/slice.js | 66 +++++- src/courseware/data/thunks.js | 51 ++++- src/courseware/data/utils.js | 203 ++++++++++++++++++ src/index.scss | 8 +- src/setupTest.js | 24 ++- 44 files changed, 1891 insertions(+), 367 deletions(-) create mode 100644 src/courseware/course/sidebar/common/SidebarBase.scss create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/UnitIcon.test.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/constants.js create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/index.js create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/messages.js create mode 100644 src/courseware/data/utils.js diff --git a/package-lock.json b/package-lock.json index e66b8d0818..a8bed3d210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "^0.1.4", "@openedx/frontend-plugin-framework": "^1.0.2", - "@openedx/paragon": "^22.1.1", + "@openedx/paragon": "^22.3.0", "@popperjs/core": "2.11.8", "@reduxjs/toolkit": "1.8.1", "classnames": "2.3.2", @@ -5076,16 +5076,9 @@ } }, "node_modules/@openedx/paragon": { - "version": "22.2.1", - "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.2.1.tgz", - "integrity": "sha512-Dd7PzvHwNnUokqbFkuOpugJZ9dHaUBOcYwqAA2aMoN7tgi4xEZWsfDFyP1+se2UPuR7NvNGammEesLAwGQ0Ylw==", - "workspaces": [ - "example", - "component-generator", - "www", - "icons", - "dependent-usage-analyzer" - ], + "version": "22.3.0", + "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.3.0.tgz", + "integrity": "sha512-tyPD14nNHfNPUzlbtspiBYFoGtrYa5+ANAVLA5ZXV1Oqunw4Etf8VMTj0DMII+BlZixBpc3gFuVHNbQBNd42Pw==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", diff --git a/package.json b/package.json index c81bd8b345..314f7d0384 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "^0.1.4", "@openedx/frontend-plugin-framework": "^1.0.2", - "@openedx/paragon": "^22.1.1", + "@openedx/paragon": "^22.3.0", "@popperjs/core": "2.11.8", "@reduxjs/toolkit": "1.8.1", "classnames": "2.3.2", diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index c1999d8c08..99605c5176 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -14,7 +14,11 @@ Object { }, "courseware": Object { "courseId": null, + "courseOutline": Object {}, + "courseOutlineShouldUpdate": false, + "courseOutlineStatus": "loading", "courseStatus": "loading", + "coursewareOutlineSidebarSettings": Object {}, "sequenceId": null, "sequenceMightBeUnit": false, "sequenceStatus": "loading", @@ -402,7 +406,11 @@ Object { }, "courseware": Object { "courseId": null, + "courseOutline": Object {}, + "courseOutlineShouldUpdate": false, + "courseOutlineStatus": "loading", "courseStatus": "loading", + "coursewareOutlineSidebarSettings": Object {}, "sequenceId": null, "sequenceMightBeUnit": false, "sequenceStatus": "loading", @@ -671,7 +679,11 @@ Object { }, "courseware": Object { "courseId": null, + "courseOutline": Object {}, + "courseOutlineShouldUpdate": false, + "courseOutlineStatus": "loading", "courseStatus": "loading", + "coursewareOutlineSidebarSettings": Object {}, "sequenceId": null, "sequenceMightBeUnit": false, "sequenceStatus": "loading", diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx index c83e254bc1..41af780c19 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; @@ -96,7 +95,7 @@ const SequenceLink = ({ icon={fasCheckCircle} fixedWidth className="float-left text-success mt-1" - aria-hidden="true" + aria-hidden={complete} title={intl.formatMessage(messages.completedAssignment)} /> ) : ( @@ -104,7 +103,7 @@ const SequenceLink = ({ icon={farCheckCircle} fixedWidth className="float-left text-gray-400 mt-1" - aria-hidden="true" + aria-hidden={complete} title={intl.formatMessage(messages.incompleteAssignment)} /> )} @@ -118,14 +117,14 @@ const SequenceLink = ({ {hideFromTOC && ( -
- - - - {intl.formatMessage(messages.hiddenSequenceLink)} +
+ + + + {intl.formatMessage(messages.hiddenSequenceLink)} + - -
+
)}
diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index 502e9b666d..131db82e09 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -1,24 +1,23 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { breakpoints, useWindowSize } from '@openedx/paragon'; -import { AlertList } from '../../generic/user-messages'; - -import Sequence from './sequence'; - -import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration'; +import { AlertList } from '@src/generic/user-messages'; +import { useModel } from '@src/generic/model-store'; +import { getCoursewareOutlineSidebarSettings } from '../data/selectors'; +import { Trigger as CourseOutlineTrigger } from './sidebar/sidebars/course-outline'; import Chat from './chat/Chat'; -import ContentTools from './content-tools'; -import CourseBreadcrumbs from './CourseBreadcrumbs'; import SidebarProvider from './sidebar/SidebarContextProvider'; import SidebarTriggers from './sidebar/SidebarTriggers'; import NewSidebarProvider from './new-sidebar/SidebarContextProvider'; import NewSidebarTriggers from './new-sidebar/SidebarTriggers'; - -import { useModel } from '../../generic/model-store'; +import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration'; +import CourseBreadcrumbs from './CourseBreadcrumbs'; +import ContentTools from './content-tools'; +import Sequence from './sequence'; const Course = ({ courseId, @@ -37,7 +36,8 @@ const Course = ({ } = useModel('courseHomeMeta', courseId); const sequence = useModel('sequences', sequenceId); const section = useModel('sections', sequence ? sequence.sectionId : null); - const navigationDisabled = sequence?.navigationDisabled ?? false; + const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings); + const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false); const pageTitleBreadCrumbs = [ sequence, @@ -54,7 +54,6 @@ const Course = ({ const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState( celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal, ); - const shouldDisplayTriggers = windowWidth >= breakpoints.small.minWidth; const shouldDisplayChat = windowWidth >= breakpoints.medium.minWidth; const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek; @@ -76,7 +75,7 @@ const Course = ({ {`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`} -
+
{navigationDisabled || ( <> )} - {shouldDisplayTriggers && ( - <> - {isNewDiscussionSidebarViewEnabled ? : } - - )} +
+ + {isNewDiscussionSidebarViewEnabled ? : } +
diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index de8a057874..34d630c9c5 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -5,7 +5,7 @@ import { Factory } from 'rosie'; import { breakpoints } from '@openedx/paragon'; import { - act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor, + fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor, } from '../../setupTest'; import * as celebrationUtils from './celebration/utils'; import { handleNextSectionCelebration } from './celebration'; @@ -59,7 +59,7 @@ describe('Course', () => { it('loads learning sequence', async () => { render(, { wrapWithRouter: true }); - expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument(); + expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument(); expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument(); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); @@ -142,27 +142,32 @@ describe('Course', () => { const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i }); expect(notificationTrigger).toBeInTheDocument(); - expect(notificationTrigger.parentNode).not.toHaveClass('mt-3', { exact: true }); + expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true }); fireEvent.click(notificationTrigger); - expect(notificationTrigger.parentNode).toHaveClass('mt-3'); + expect(notificationTrigger.parentNode).toHaveClass('sidebar-active'); }); it('handles click to open/close discussions sidebar', async () => { await setupDiscussionSidebar(); - const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i }); - const discussionsSideBar = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS')); - expect(discussionsSideBar).not.toHaveClass('d-none'); + await waitFor(() => { + expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument(); + expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none'); + }); + + const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i }); + expect(discussionsTrigger).toBeInTheDocument(); + fireEvent.click(discussionsTrigger); - await act(async () => { - fireEvent.click(discussionsTrigger); + await waitFor(() => { + expect(screen.queryByTestId('sidebar-DISCUSSIONS')).not.toBeInTheDocument(); }); - await expect(discussionsSideBar).toHaveClass('d-none'); - await act(async () => { - fireEvent.click(discussionsTrigger); + fireEvent.click(discussionsTrigger); + + await waitFor(() => { + expect(screen.queryByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument(); }); - await expect(discussionsSideBar).not.toHaveClass('d-none'); }); it('displays discussions sidebar when unit changes', async () => { @@ -192,8 +197,9 @@ describe('Course', () => { it('handles click to open/close notification tray', async () => { await setupDiscussionSidebar(); const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i }); - expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none'); + expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument(); fireEvent.click(notificationShowButton); + expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument(); expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none'); }); @@ -204,7 +210,9 @@ describe('Course', () => { { type: 'vertical' }, { courseId: courseMetadata.id }, )); - const testStore = await initializeTestStore({ courseMetadata, unitBlocks }, false); + const testStore = await initializeTestStore({ + courseMetadata, unitBlocks, enableNavigationSidebar: { enable_navigation_sidebar: false }, + }, false); const { courseware, models } = testStore.getState(); const { courseId, sequenceId } = courseware; const testData = { diff --git a/src/courseware/course/CourseBreadcrumbs.jsx b/src/courseware/course/CourseBreadcrumbs.jsx index 6b29144fea..ab9d031dbd 100644 --- a/src/courseware/course/CourseBreadcrumbs.jsx +++ b/src/courseware/course/CourseBreadcrumbs.jsx @@ -154,7 +154,7 @@ const CourseBreadcrumbs = ({ }, [courseStatus, sequenceStatus, allSequencesInSections]); return ( -