+
+
+
- {shouldDisplayNotificationTriggerInSequence && (
- isNewDiscussionSidebarViewEnabled ? :
- )}
@@ -183,18 +179,18 @@ const Sequence = ({
unitLoadedHandler={handleUnitLoaded}
/>
{unitHasLoaded && (
- {
- logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
- handlePrevious();
- }}
- onClickNext={() => {
- logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
- handleNext();
- }}
- />
+ {
+ logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
+ handlePrevious();
+ }}
+ onClickNext={() => {
+ logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
+ handleNext();
+ }}
+ />
)}
diff --git a/src/courseware/course/sequence/Sequence.test.jsx b/src/courseware/course/sequence/Sequence.test.jsx
index 50a8f7e1f1..1b0e2579e2 100644
--- a/src/courseware/course/sequence/Sequence.test.jsx
+++ b/src/courseware/course/sequence/Sequence.test.jsx
@@ -18,6 +18,7 @@ jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
describe('Sequence', () => {
let mockData;
+ let defaultContextValue;
const courseMetadata = Factory.build('courseMetadata');
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
'block',
@@ -38,12 +39,29 @@ describe('Sequence', () => {
toggleNotificationTray: () => {},
setNotificationStatus: () => {},
};
+ defaultContextValue = { courseId: mockData.courseId, currentSidebar: null, toggleSidebar: jest.fn() };
});
beforeEach(() => {
global.innerWidth = breakpoints.extraLarge.minWidth;
});
+ const SidebarWrapper = ({ contextValue = defaultContextValue, overrideData = {} }) => (
+
+
+
+ );
+
+ SidebarWrapper.defaultProps = {
+ contextValue: defaultContextValue,
+ overrideData: {},
+ };
+
+ SidebarWrapper.propTypes = {
+ contextValue: PropTypes.shape({}),
+ overrideData: PropTypes.shape({}),
+ };
+
it('renders correctly without data', async () => {
const testStore = await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true }, false);
render(
@@ -77,7 +95,7 @@ describe('Sequence', () => {
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata,
}, false);
const { container } = render(
-
,
+
,
{ store: testStore, wrapWithRouter: true },
);
@@ -134,9 +152,9 @@ describe('Sequence', () => {
});
it('handles loading unit', async () => {
- render(
, { wrapWithRouter: true });
+ render(
, { wrapWithRouter: true });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
- // `Previous`, `Bookmark` and `Close Tray` buttons
+ // `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);
@@ -174,13 +192,13 @@ describe('Sequence', () => {
sequenceId: sequenceBlocks[1].id,
previousSequenceHandler: jest.fn(),
};
- render(
, { store: testStore, wrapWithRouter: true });
+ render(
, { store: testStore, wrapWithRouter: true });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
const sequencePreviousButton = screen.getByRole('link', { name: /previous/i });
fireEvent.click(sequencePreviousButton);
expect(testData.previousSequenceHandler).toHaveBeenCalledTimes(1);
- expect(sendTrackEvent).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.sequence.previous_selected', {
current_tab: 1,
id: testData.unitId,
@@ -194,8 +212,8 @@ describe('Sequence', () => {
.filter(button => button !== sequencePreviousButton)[0];
fireEvent.click(unitPreviousButton);
expect(testData.previousSequenceHandler).toHaveBeenCalledTimes(2);
- expect(sendTrackEvent).toHaveBeenCalledTimes(3);
- expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.ui.lms.sequence.previous_selected', {
+ expect(sendTrackEvent).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.previous_selected', {
current_tab: 1,
id: testData.unitId,
tab_count: unitBlocks.length,
@@ -210,7 +228,7 @@ describe('Sequence', () => {
sequenceId: sequenceBlocks[0].id,
nextSequenceHandler: jest.fn(),
};
- render(
, { store: testStore, wrapWithRouter: true });
+ render(
, { store: testStore, wrapWithRouter: true });
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
const sequenceNextButton = screen.getByRole('link', { name: /next/i });
@@ -229,8 +247,8 @@ describe('Sequence', () => {
.filter(button => button !== sequenceNextButton)[0];
fireEvent.click(unitNextButton);
expect(testData.nextSequenceHandler).toHaveBeenCalledTimes(2);
- expect(sendTrackEvent).toHaveBeenCalledTimes(3);
- expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.ui.lms.sequence.next_selected', {
+ expect(sendTrackEvent).toHaveBeenCalledTimes(2);
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.next_selected', {
current_tab: unitBlocks.length,
id: testData.unitId,
tab_count: unitBlocks.length,
@@ -248,7 +266,7 @@ describe('Sequence', () => {
previousSequenceHandler: jest.fn(),
nextSequenceHandler: jest.fn(),
};
- render(
, { store: testStore, wrapWithRouter: true });
+ render(
, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).toBeInTheDocument());
fireEvent.click(screen.getByRole('link', { name: /previous/i }));
@@ -261,7 +279,7 @@ describe('Sequence', () => {
// Therefore the next unit will still be `the initial one + 1`.
expect(testData.unitNavigationHandler).toHaveBeenNthCalledWith(2, unitBlocks[unitNumber + 1].id);
- expect(sendTrackEvent).toHaveBeenCalledTimes(3);
+ expect(sendTrackEvent).toHaveBeenCalledTimes(2);
});
it('handles the `Previous` buttons for the first unit in the first sequence', async () => {
@@ -272,7 +290,7 @@ describe('Sequence', () => {
unitNavigationHandler: jest.fn(),
previousSequenceHandler: jest.fn(),
};
- render(
, { store: testStore, wrapWithRouter: true });
+ render(
, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -280,7 +298,7 @@ describe('Sequence', () => {
expect(testData.previousSequenceHandler).not.toHaveBeenCalled();
expect(testData.unitNavigationHandler).not.toHaveBeenCalled();
- expect(sendTrackEvent).toHaveBeenCalled();
+ expect(sendTrackEvent).not.toHaveBeenCalled();
});
it('handles the `Next` buttons for the last unit in the last sequence', async () => {
@@ -291,7 +309,7 @@ describe('Sequence', () => {
unitNavigationHandler: jest.fn(),
nextSequenceHandler: jest.fn(),
};
- render(
, { store: testStore, wrapWithRouter: true });
+ render(
, { store: testStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
@@ -299,7 +317,7 @@ describe('Sequence', () => {
expect(testData.nextSequenceHandler).not.toHaveBeenCalled();
expect(testData.unitNavigationHandler).not.toHaveBeenCalled();
- expect(sendTrackEvent).toHaveBeenCalled();
+ expect(sendTrackEvent).not.toHaveBeenCalled();
});
it('handles the navigation buttons for empty sequence', async () => {
@@ -333,51 +351,37 @@ describe('Sequence', () => {
nextSequenceHandler: jest.fn(),
};
- render(
, { store: innerTestStore, wrapWithRouter: true });
+ render(
, { store: innerTestStore, wrapWithRouter: true });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
screen.getAllByRole('link', { name: /previous/i }).forEach(button => fireEvent.click(button));
expect(testData.previousSequenceHandler).toHaveBeenCalledTimes(2);
- expect(testData.unitNavigationHandler).not.toHaveBeenCalled();
+ expect(testData.unitNavigationHandler).toHaveBeenCalledTimes(2);
screen.getAllByRole('link', { name: /next/i }).forEach(button => fireEvent.click(button));
expect(testData.nextSequenceHandler).toHaveBeenCalledTimes(2);
- expect(testData.unitNavigationHandler).not.toHaveBeenCalled();
+ expect(testData.unitNavigationHandler).toHaveBeenCalledTimes(4);
- expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'edx.ui.course.upgrade.old_sidebar.notifications', {
- course_end: undefined,
- course_modes: undefined,
- course_start: undefined,
- courserun_key: undefined,
- enrollment_end: undefined,
- enrollment_mode: undefined,
- enrollment_start: undefined,
- is_upgrade_notification_visible: false,
- name: 'Old Sidebar Notification Tray',
- org_key: undefined,
- username: undefined,
- verification_status: undefined,
- });
- expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.previous_selected', {
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'edx.ui.lms.sequence.previous_selected', {
current_tab: 1,
id: testData.unitId,
tab_count: 0,
widget_placement: 'top',
});
- expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.ui.lms.sequence.previous_selected', {
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.sequence.previous_selected', {
current_tab: 1,
id: testData.unitId,
tab_count: 0,
widget_placement: 'bottom',
});
- expect(sendTrackEvent).toHaveBeenNthCalledWith(4, 'edx.ui.lms.sequence.next_selected', {
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(3, 'edx.ui.lms.sequence.next_selected', {
current_tab: 1,
id: testData.unitId,
tab_count: 0,
widget_placement: 'top',
});
- expect(sendTrackEvent).toHaveBeenNthCalledWith(5, 'edx.ui.lms.sequence.next_selected', {
+ expect(sendTrackEvent).toHaveBeenNthCalledWith(4, 'edx.ui.lms.sequence.next_selected', {
current_tab: 1,
id: testData.unitId,
tab_count: 0,
@@ -395,7 +399,7 @@ describe('Sequence', () => {
sequenceId: sequenceBlocks[0].id,
unitNavigationHandler: jest.fn(),
};
- render(
, { store: testStore, wrapWithRouter: true });
+ render(
, { store: testStore, wrapWithRouter: true });
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).toBeInTheDocument());
fireEvent.click(screen.getByRole('link', { name: targetUnit.display_name }));
@@ -410,16 +414,6 @@ describe('Sequence', () => {
});
});
- const SidebarWrapper = ({ contextValue }) => (
-
-
-
- );
-
- SidebarWrapper.propTypes = {
- contextValue: PropTypes.shape({}).isRequired,
- };
-
describe('notification feature', () => {
it('renders notification tray in sequence', async () => {
render(
null }} />, { wrapWithRouter: true });
@@ -431,7 +425,7 @@ describe('Sequence', () => {
render(, { wrapWithRouter: true });
const notificationCloseIconButton = await screen.findByRole('button', { name: /Close notification tray/i });
fireEvent.click(notificationCloseIconButton);
- expect(toggleNotificationTray).toHaveBeenCalledTimes(1);
+ expect(toggleNotificationTray).toHaveBeenCalled();
});
it('does not render notification tray in sequence by default if in responsive view', async () => {
diff --git a/src/courseware/course/sidebar/Sidebar.jsx b/src/courseware/course/sidebar/Sidebar.jsx
index fe943fa540..64113a9d84 100644
--- a/src/courseware/course/sidebar/Sidebar.jsx
+++ b/src/courseware/course/sidebar/Sidebar.jsx
@@ -1,15 +1,20 @@
-import React from 'react';
-import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
-
-const Sidebar = () => (
- <>
- {
- SIDEBAR_ORDER.map((sideBarId) => {
- const SidebarToRender = SIDEBARS[sideBarId].Sidebar;
- return ;
- })
- }
- >
-);
+import { useContext } from 'react';
+
+import SidebarContext from './SidebarContext';
+import { SIDEBARS } from './sidebars';
+
+const Sidebar = () => {
+ const { currentSidebar } = useContext(SidebarContext);
+
+ if (!currentSidebar || !SIDEBARS[currentSidebar]) {
+ return null;
+ }
+
+ const SidebarToRender = SIDEBARS[currentSidebar].Sidebar;
+
+ return (
+
+ );
+};
export default Sidebar;
diff --git a/src/courseware/course/sidebar/SidebarContextProvider.jsx b/src/courseware/course/sidebar/SidebarContextProvider.jsx
index e1d8870b50..84dfa1dfdd 100644
--- a/src/courseware/course/sidebar/SidebarContextProvider.jsx
+++ b/src/courseware/course/sidebar/SidebarContextProvider.jsx
@@ -1,12 +1,16 @@
import { breakpoints, useWindowSize } from '@openedx/paragon';
import PropTypes from 'prop-types';
-import React, {
+import { useSelector } from 'react-redux';
+import {
useEffect, useState, useMemo, useCallback,
} from 'react';
-import { useModel } from '../../../generic/model-store';
-import { getLocalStorage, setLocalStorage } from '../../../data/localStorage';
+import { useModel } from '@src/generic/model-store';
+import { getLocalStorage, setLocalStorage } from '@src/data/localStorage';
+import { getCoursewareOutlineSidebarSettings } from '../../data/selectors';
+import * as discussionsSidebar from './sidebars/discussions';
+import * as notificationsSidebar from './sidebars/notifications';
import SidebarContext from './SidebarContext';
import { SIDEBARS } from './sidebars';
@@ -16,18 +20,35 @@ const SidebarProvider = ({
children,
}) => {
const { verifiedMode } = useModel('courseHomeMeta', courseId);
- const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
- const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
+ const topic = useModel('discussionTopics', unitId);
+ const isUnitHasDiscussionTopics = topic?.id && topic?.enabledInContext;
+ const shouldDisplayFullScreen = useWindowSize().width < breakpoints.extraLarge.minWidth;
+ const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.extraLarge.minWidth;
const query = new URLSearchParams(window.location.search);
- const initialSidebar = (shouldDisplaySidebarOpen || query.get('sidebar') === 'true') ? SIDEBARS.DISCUSSIONS.ID : null;
+ const { alwaysOpenAuxiliarySidebar } = useSelector(getCoursewareOutlineSidebarSettings);
+ const isInitiallySidebarOpen = shouldDisplaySidebarOpen || query.get('sidebar') === 'true';
+
+ let initialSidebar = null;
+ if (isInitiallySidebarOpen && alwaysOpenAuxiliarySidebar) {
+ initialSidebar = isUnitHasDiscussionTopics
+ ? SIDEBARS[discussionsSidebar.ID].ID
+ : verifiedMode && SIDEBARS[notificationsSidebar.ID].ID;
+ }
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`));
useEffect(() => {
- // if the user hasn't purchased the course, show the notifications sidebar
- setCurrentSidebar(verifiedMode ? SIDEBARS.NOTIFICATIONS.ID : SIDEBARS.DISCUSSIONS.ID);
- }, [unitId]);
+ if (initialSidebar && currentSidebar !== initialSidebar) {
+ setCurrentSidebar(initialSidebar);
+ }
+ }, [unitId, topic]);
+
+ useEffect(() => {
+ if (initialSidebar) {
+ setCurrentSidebar(initialSidebar);
+ }
+ }, [shouldDisplaySidebarOpen]);
const onNotificationSeen = useCallback(() => {
setNotificationStatus('inactive');
@@ -40,6 +61,7 @@ const SidebarProvider = ({
}, [currentSidebar]);
const contextValue = useMemo(() => ({
+ initialSidebar,
toggleSidebar,
onNotificationSeen,
setNotificationStatus,
diff --git a/src/courseware/course/sidebar/SidebarTriggers.jsx b/src/courseware/course/sidebar/SidebarTriggers.jsx
index 9ce24e0cea..d1a48a5d41 100644
--- a/src/courseware/course/sidebar/SidebarTriggers.jsx
+++ b/src/courseware/course/sidebar/SidebarTriggers.jsx
@@ -1,5 +1,5 @@
+import { useContext } from 'react';
import classNames from 'classnames';
-import React, { useContext } from 'react';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import SidebarContext from './SidebarContext';
import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
@@ -19,7 +19,7 @@ const SidebarTriggers = () => {
const isActive = sidebarId === currentSidebar;
return (
diff --git a/src/courseware/course/sidebar/common/SidebarBase.jsx b/src/courseware/course/sidebar/common/SidebarBase.jsx
index fbbe426415..23f5df2941 100644
--- a/src/courseware/course/sidebar/common/SidebarBase.jsx
+++ b/src/courseware/course/sidebar/common/SidebarBase.jsx
@@ -3,8 +3,8 @@ import { Icon, IconButton } from '@openedx/paragon';
import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
-import React, { useCallback, useContext } from 'react';
-import { useEventListener } from '../../../../generic/hooks';
+import { useCallback, useContext } from 'react';
+import { useEventListener } from '@src/generic/hooks';
import messages from '../../messages';
import SidebarContext from '../SidebarContext';
@@ -36,14 +36,15 @@ const SidebarBase = ({
return (