diff --git a/.gitignore b/.gitignore
index 3fc643913a..fc47888426 100755
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,5 @@ module.config.js
src/i18n/messages/
env.config.jsx
+
+webpack.dev-tutor.config.js
diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx
index 1bf61f0eef..89d68dd0e3 100644
--- a/src/courseware/course/Course.test.jsx
+++ b/src/courseware/course/Course.test.jsx
@@ -34,6 +34,10 @@ jest.mock(
}),
);
+jest.mock('@src/data/sessionStorage', () => ({
+ getSessionStorage: jest.fn().mockReturnValue(null),
+}));
+
const recordFirstSectionCelebration = jest.fn();
// eslint-disable-next-line no-import-assign
celebrationUtils.recordFirstSectionCelebration = recordFirstSectionCelebration;
diff --git a/src/courseware/course/sidebar/SidebarContextProvider.jsx b/src/courseware/course/sidebar/SidebarContextProvider.jsx
index ed9567558b..aff889bc12 100644
--- a/src/courseware/course/sidebar/SidebarContextProvider.jsx
+++ b/src/courseware/course/sidebar/SidebarContextProvider.jsx
@@ -7,6 +7,8 @@ import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
import { useModel } from '@src/generic/model-store';
+import { getSessionStorage } from '@src/data/sessionStorage';
+import { WIDGETS } from '@src/constants';
import SidebarContext from './SidebarContext';
import {
@@ -68,14 +70,15 @@ const SidebarProvider = ({
}, [getAvailableWidgets]);
// Calculate initial sidebar with priority cascade
- const initialSidebar = useInitialSidebar({
+ const initialSidebarByPriority = useInitialSidebar({
courseId,
shouldDisplayFullScreen,
isInitiallySidebarOpen,
getFirstAvailablePanel,
getAvailableWidgets,
});
-
+ const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
+ const initialSidebar = isNotificationTrayOpen ? WIDGETS.NOTIFICATIONS : initialSidebarByPriority;
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
// Track if user has manually toggled sidebar within current unit
diff --git a/src/courseware/course/sidebar/SidebarContextProvider.test.jsx b/src/courseware/course/sidebar/SidebarContextProvider.test.jsx
index 59219ad031..ab58f53e1f 100644
--- a/src/courseware/course/sidebar/SidebarContextProvider.test.jsx
+++ b/src/courseware/course/sidebar/SidebarContextProvider.test.jsx
@@ -4,6 +4,7 @@ import {
render, screen, fireEvent, act,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
+import { getSessionStorage } from '@src/data/sessionStorage';
import SidebarContext from './SidebarContext';
import SidebarProvider from './SidebarContextProvider';
@@ -20,6 +21,10 @@ jest.mock('@src/generic/model-store', () => ({
}),
}));
+jest.mock('@src/data/sessionStorage', () => ({
+ getSessionStorage: jest.fn(() => null),
+}));
+
jest.mock('@openedx/paragon', () => {
const actual = jest.requireActual('@openedx/paragon');
return {
@@ -105,6 +110,7 @@ describe('SidebarContextProvider', () => {
const storage = jest.requireMock('./utils/storage');
storage.getSidebarId.mockReturnValue(null);
storage.isOutlineSidebarCollapsed.mockReturnValue(false);
+ getSessionStorage.mockReturnValue(null);
});
describe('context values provided', () => {
@@ -120,6 +126,14 @@ describe('SidebarContextProvider', () => {
expect(screen.getByTestId('available-ids').textContent).toBe('DISCUSSIONS,NOTES');
});
+ it('opens the legacy notification tray when session storage marks it open', () => {
+ getSessionStorage.mockReturnValue('open');
+
+ renderProvider();
+
+ expect(screen.getByTestId('current-sidebar').textContent).toBe('UPGRADE');
+ });
+
it('excludes a widget from availableSidebarIds when isAvailable returns false', () => {
const { getEnabledWidgets } = jest.requireMock('./defaultWidgets');
getEnabledWidgets.mockReturnValueOnce([
diff --git a/src/courseware/course/sidebar/common/SidebarBase.jsx b/src/courseware/course/sidebar/common/SidebarBase.jsx
index d2b6fb19d1..55ac25c353 100644
--- a/src/courseware/course/sidebar/common/SidebarBase.jsx
+++ b/src/courseware/course/sidebar/common/SidebarBase.jsx
@@ -4,7 +4,10 @@ import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useCallback, useContext } from 'react';
+
import { useEventListener } from '@src/generic/hooks';
+import { WIDGETS } from '@src/constants';
+import { setSessionStorage } from '@src/data/sessionStorage';
import messages from '../../messages';
import SidebarContext from '../SidebarContext';
@@ -19,18 +22,25 @@ const SidebarBase = ({
}) => {
const intl = useIntl();
const {
+ courseId,
toggleSidebar,
shouldDisplayFullScreen,
currentSidebar,
} = useContext(SidebarContext);
+ const handleCloseSidebar = useCallback(() => {
+ toggleSidebar(null);
+ if (sidebarId === WIDGETS.NOTIFICATIONS) {
+ setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
+ }
+ }, [courseId, sidebarId, toggleSidebar]);
+
const receiveMessage = useCallback(({ data }) => {
const { type } = data;
if (type === 'learning.events.sidebar.close') {
- toggleSidebar(null);
+ handleCloseSidebar();
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [sidebarId, toggleSidebar]);
+ }, [handleCloseSidebar]);
useEventListener('message', receiveMessage);
@@ -49,8 +59,8 @@ const SidebarBase = ({
{shouldDisplayFullScreen ? (
toggleSidebar(null)}
- onKeyDown={() => toggleSidebar(null)}
+ onClick={handleCloseSidebar}
+ onKeyDown={handleCloseSidebar}
role="button"
tabIndex="0"
>
@@ -71,7 +81,7 @@ const SidebarBase = ({
src={Close}
size="sm"
iconAs={Icon}
- onClick={() => toggleSidebar(null)}
+ onClick={handleCloseSidebar}
variant="primary"
alt={intl.formatMessage(messages.closeSidebarTrigger)}
/>
diff --git a/src/courseware/course/sidebar/common/SidebarBase.test.jsx b/src/courseware/course/sidebar/common/SidebarBase.test.jsx
index 7c3b4b777c..4d670d3ee7 100644
--- a/src/courseware/course/sidebar/common/SidebarBase.test.jsx
+++ b/src/courseware/course/sidebar/common/SidebarBase.test.jsx
@@ -1,12 +1,20 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen, fireEvent } from '@testing-library/react';
+import { WIDGETS } from '@src/constants';
+import { setSessionStorage } from '@src/data/sessionStorage';
import SidebarContext from '../SidebarContext';
import SidebarBase from './SidebarBase';
const mockToggleSidebar = jest.fn();
+const courseId = 'course-test-123';
+
+jest.mock('@src/data/sessionStorage', () => ({
+ setSessionStorage: jest.fn(),
+}));
const defaultContextValue = {
+ courseId,
toggleSidebar: mockToggleSidebar,
shouldDisplayFullScreen: false,
currentSidebar: 'TEST_SIDEBAR',
@@ -82,6 +90,14 @@ describe('SidebarBase', () => {
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
});
+ it('marks the notification tray as closed when closing the upgrade sidebar', () => {
+ renderSidebarBase({ sidebarId: WIDGETS.NOTIFICATIONS }, { currentSidebar: WIDGETS.NOTIFICATIONS });
+
+ fireEvent.click(screen.getByRole('button', { name: /close sidebar/i }));
+
+ expect(setSessionStorage).toHaveBeenCalledWith(`notificationTrayStatus.${courseId}`, 'closed');
+ });
+
it('does not render the back-to-course button', () => {
renderSidebarBase();
diff --git a/src/widgets/upgrade/src/UpgradeTrigger.jsx b/src/widgets/upgrade/src/UpgradeTrigger.jsx
index 029b1de723..3db419ea5d 100644
--- a/src/widgets/upgrade/src/UpgradeTrigger.jsx
+++ b/src/widgets/upgrade/src/UpgradeTrigger.jsx
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import SidebarContext from '@src/courseware/course/sidebar/SidebarContext';
import SidebarTriggerBase from '@src/courseware/course/sidebar/common/TriggerBase';
import { getLocalStorage, setLocalStorage } from '@src/data/localStorage';
+import { getSessionStorage, setSessionStorage } from '@src/data/sessionStorage';
import { useUpgradeWidgetContext } from './UpgradeWidgetContext';
import UpgradeIcon from './UpgradeIcon';
import messages from './messages';
@@ -12,13 +13,21 @@ export const ID = 'UPGRADE';
const UpgradeTrigger = ({ onClick }) => {
const intl = useIntl();
- const { courseId } = useContext(SidebarContext);
+ const { courseId, currentSidebar, toggleSidebar } = useContext(SidebarContext);
const {
upgradeWidgetStatus,
setUpgradeWidgetStatus,
upgradeCurrentState,
} = useUpgradeWidgetContext();
+ useEffect(() => {
+ const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
+
+ if (isNotificationTrayOpen && !currentSidebar && toggleSidebar) {
+ toggleSidebar(ID);
+ }
+ }, [courseId, currentSidebar, toggleSidebar]);
+
useEffect(() => {
if (!upgradeCurrentState) {
return;
@@ -30,8 +39,15 @@ const UpgradeTrigger = ({ onClick }) => {
}
}, [upgradeCurrentState, courseId, setUpgradeWidgetStatus]);
+ const handleClick = () => {
+ const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
+
+ setSessionStorage(`notificationTrayStatus.${courseId}`, isNotificationTrayOpen ? 'closed' : 'open');
+ onClick();
+ };
+
return (
-
+
);
diff --git a/src/widgets/upgrade/src/UpgradeTrigger.test.jsx b/src/widgets/upgrade/src/UpgradeTrigger.test.jsx
index 489093a7fc..ee5736fda8 100644
--- a/src/widgets/upgrade/src/UpgradeTrigger.test.jsx
+++ b/src/widgets/upgrade/src/UpgradeTrigger.test.jsx
@@ -3,6 +3,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen, fireEvent } from '@testing-library/react';
import SidebarContext from '@src/courseware/course/sidebar/SidebarContext';
import * as localStorageModule from '@src/data/localStorage';
+import * as sessionStorageModule from '@src/data/sessionStorage';
import { UpgradeWidgetProvider } from './UpgradeWidgetContext';
import UpgradeTrigger from './UpgradeTrigger';
@@ -11,6 +12,11 @@ jest.mock('@src/data/localStorage', () => ({
setLocalStorage: jest.fn(),
}));
+jest.mock('@src/data/sessionStorage', () => ({
+ getSessionStorage: jest.fn(() => null),
+ setSessionStorage: jest.fn(),
+}));
+
const courseId = 'course-test-123';
function renderTrigger(sidebarContextOverrides = {}, localStorageOverride = null) {
@@ -35,6 +41,7 @@ function renderTrigger(sidebarContextOverrides = {}, localStorageOverride = null
describe('UpgradeTrigger', () => {
beforeEach(() => {
jest.clearAllMocks();
+ sessionStorageModule.getSessionStorage.mockReturnValue(null);
});
it('renders a button with the correct aria-label', () => {
@@ -66,6 +73,48 @@ describe('UpgradeTrigger', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
+ it('marks the notification tray as open when the trigger opens the panel', () => {
+ const onClick = jest.fn();
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /show upgrade panel/i }));
+
+ expect(sessionStorageModule.setSessionStorage).toHaveBeenCalledWith(
+ `notificationTrayStatus.${courseId}`,
+ 'open',
+ );
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('marks the notification tray as closed when the trigger closes the panel', () => {
+ sessionStorageModule.getSessionStorage.mockReturnValue('open');
+ renderTrigger({ currentSidebar: 'UPGRADE' });
+
+ fireEvent.click(screen.getByRole('button', { name: /show upgrade panel/i }));
+
+ expect(sessionStorageModule.setSessionStorage).toHaveBeenCalledWith(
+ `notificationTrayStatus.${courseId}`,
+ 'closed',
+ );
+ });
+
+ it('reopens the upgrade panel when notification tray status is open', () => {
+ const toggleSidebar = jest.fn();
+ sessionStorageModule.getSessionStorage.mockReturnValue('open');
+
+ renderTrigger({ currentSidebar: null, toggleSidebar });
+
+ expect(toggleSidebar).toHaveBeenCalledWith('UPGRADE');
+ });
+
it('shows status dot when upgradeWidgetStatus is "active"', () => {
// Default status from localStorage is null, so provider defaults to "active"
renderTrigger();