Merge branch 'openedx:master' into master

This commit is contained in:
Nathan Sprenkle
2025-06-13 10:34:40 -04:00
committed by GitHub
11 changed files with 350 additions and 298 deletions

14
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-lib-learning-assistant": "^2.20.0",
"@edx/frontend-lib-special-exams": "^3.5.0",
"@edx/frontend-lib-special-exams": "^4.0.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^4.0.0",
@@ -2287,9 +2287,9 @@
}
},
"node_modules/@edx/frontend-lib-special-exams": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-3.5.0.tgz",
"integrity": "sha512-lRKD3K+XAuoKAaxbZxb7QLTWkSlV9yIy08XflYoHh/weClesVTETU3+NtJ5YRsC/kYHZrzSYIpMZnBnkKTGTww==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-4.0.0.tgz",
"integrity": "sha512-mJdrxebdKO9NxDFkQZ1vyWVUvWCk393pIVHJyz9vH42Kvn08LC5db8/gYk37srCfsA4Dl78pMLa408CoT14JMA==",
"license": "AGPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.34",
@@ -7884,9 +7884,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001720",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz",
"integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==",
"version": "1.0.30001721",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
"funding": [
{
"type": "opencollective",

View File

@@ -38,7 +38,7 @@
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-lib-learning-assistant": "^2.20.0",
"@edx/frontend-lib-special-exams": "^3.5.0",
"@edx/frontend-lib-special-exams": "^4.0.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^4.0.0",
@@ -98,4 +98,4 @@
],
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
}
}
}

View File

@@ -34,188 +34,192 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<PathFixesProvider>
<NoticesProvider>
<UserMessagesProvider>
<Routes>
<Route
element={
<PageWrap>
<Page Not Found />
</PageWrap>
}
path="*"
/>
<Route
element={
<PageWrap>
<Goal Unsubscribe />
</PageWrap>
}
path="/goal-unsubscribe/:token"
/>
<Route
element={
<PageWrap>
<Courseware Redirect Landing Page />
</PageWrap>
}
path="/redirect/*"
/>
<Route
element={
<PageWrap>
<Preferences Unsubscribe />
</PageWrap>
}
path="/preferences-unsubscribe/:userToken/:updatePatch"
/>
<Route
element={
<DecodePageRoute>
<Course Access Error Page />
</DecodePageRoute>
}
path="/course/:courseId/access-denied"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="outline"
>
<Outline Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/home"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="lti_live"
>
<Live Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/live"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="dates"
>
<Dates Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/dates"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="discussion"
>
<Discussion Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/discussion/:path/*"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress/:targetUserId/"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseware"
tab="courseware"
>
<Course Exit />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/course-end"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId"
/>
</Routes>
<div
className="app-container"
>
<Routes>
<Route
element={
<PageWrap>
<Page Not Found />
</PageWrap>
}
path="*"
/>
<Route
element={
<PageWrap>
<Goal Unsubscribe />
</PageWrap>
}
path="/goal-unsubscribe/:token"
/>
<Route
element={
<PageWrap>
<Courseware Redirect Landing Page />
</PageWrap>
}
path="/redirect/*"
/>
<Route
element={
<PageWrap>
<Preferences Unsubscribe />
</PageWrap>
}
path="/preferences-unsubscribe/:userToken/:updatePatch"
/>
<Route
element={
<DecodePageRoute>
<Course Access Error Page />
</DecodePageRoute>
}
path="/course/:courseId/access-denied"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="outline"
>
<Outline Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/home"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="lti_live"
>
<Live Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/live"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="dates"
>
<Dates Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/dates"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="discussion"
>
<Discussion Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/discussion/:path/*"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress/:targetUserId/"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseware"
tab="courseware"
>
<Course Exit />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/course-end"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId"
/>
</Routes>
</div>
</UserMessagesProvider>
</NoticesProvider>
</PathFixesProvider>

View File

@@ -54,6 +54,8 @@ const SidebarProvider: React.FC<Props> = ({
}, [courseId]);
useEffect(() => {
window.sessionStorage.setItem('hideCourseOutlineSidebar', 'true');
window.sessionStorage.setItem(`notificationTrayStatus.${courseId}`, 'open');
setHideDiscussionbar(!isDiscussionbarAvailable);
setHideNotificationbar(!isNotificationbarAvailable);
if (initialSidebar && currentSidebar !== initialSidebar) {

View File

@@ -1,11 +1,13 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getSessionStorage, setSessionStorage } from '../../../../../../data/sessionStorage';
import {
initializeMockApp, initializeTestStore, render, screen,
} from '../../../../../../setupTest';
@@ -14,11 +16,19 @@ import { buildTopicsFromUnits } from '../../../../../data/__factories__/discussi
import { getCourseDiscussionTopics } from '../../../../../data/thunks';
import SidebarContext from '../../../SidebarContext';
import DiscussionsNotificationsSidebar from '../DiscussionsNotificationsSidebar';
import DiscussionsNotificationsTrigger from '../DiscussionsNotificationsTrigger';
import DiscussionsWidget from './DiscussionsWidget';
initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
jest.mock('../../../../../../data/sessionStorage', () => ({
getSessionStorage: jest.fn(),
setSessionStorage: jest.fn(),
}));
const onClickMock = jest.fn();
describe('DiscussionsWidget', () => {
let axiosMock;
let mockData;
@@ -81,4 +91,34 @@ describe('DiscussionsWidget', () => {
expect(screen.queryByText('Back to course')).toBeInTheDocument();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
});
it('should open notification tray if closed', () => {
(getSessionStorage as jest.Mock).mockReturnValue('closed');
renderWithProvider(() => <DiscussionsNotificationsTrigger onClick={onClickMock} />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(setSessionStorage).toHaveBeenCalledWith(
`notificationTrayStatus.${courseId}`,
'open',
);
expect(onClickMock).toHaveBeenCalled();
});
it('should close notification tray if open', () => {
(getSessionStorage as jest.Mock).mockReturnValue('open');
renderWithProvider(() => <DiscussionsNotificationsTrigger onClick={onClickMock} />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(setSessionStorage).toHaveBeenCalledWith(
`notificationTrayStatus.${courseId}`,
'open',
);
expect(onClickMock).toHaveBeenCalled();
});
});

View File

@@ -218,18 +218,20 @@ const Sequence = ({
if (sequenceStatus === 'loaded') {
return (
<div>
<SequenceExamWrapper
sequence={sequence}
courseId={courseId}
isStaff={isStaff}
originalUserIsStaff={originalUserIsStaff}
canAccessProctoredExams={canAccessProctoredExams}
>
{defaultContent}
</SequenceExamWrapper>
<>
<div className="d-flex flex-column flex-grow-1 justify-content-center">
<SequenceExamWrapper
sequence={sequence}
courseId={courseId}
isStaff={isStaff}
originalUserIsStaff={originalUserIsStaff}
canAccessProctoredExams={canAccessProctoredExams}
>
{defaultContent}
</SequenceExamWrapper>
</div>
<CourseLicense license={license || undefined} />
</div>
</>
);
}

View File

@@ -1,14 +1,12 @@
.outline-sidebar-wrapper {
width: 32.125rem;
max-width: 100%;
overflow: auto;
position: relative;
flex-shrink: 0;
}
.outline-sidebar {
@media (min-width: map-get($grid-breakpoints, "xl")) {
position: absolute;
left: 0;
top: 0;
}

View File

@@ -68,6 +68,7 @@ export const useCourseOutlineSidebar = () => {
} else {
toggleSidebar(ID);
window.sessionStorage.removeItem('hideCourseOutlineSidebar');
window.sessionStorage.setItem(`notificationTrayStatus.${courseId}`, 'closed');
}
};

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { getNotices } from './api';
@@ -25,11 +25,7 @@ const NoticesProvider = ({ children }) => {
getData();
}, []);
return (
<div>
{isRedirected === true ? null : children}
</div>
);
return isRedirected === true ? null : children;
};
NoticesProvider.propTypes = {

View File

@@ -49,100 +49,102 @@ subscribe(APP_READY, () => {
<PathFixesProvider>
<NoticesProvider>
<UserMessagesProvider>
<Routes>
<Route path="*" element={<PageWrap><PageNotFound /></PageWrap>} />
<Route path={ROUTES.UNSUBSCRIBE} element={<PageWrap><GoalUnsubscribe /></PageWrap>} />
<Route path={ROUTES.REDIRECT} element={<PageWrap><CoursewareRedirectLandingPage /></PageWrap>} />
<Route
path={ROUTES.PREFERENCES_UNSUBSCRIBE}
element={
<PageWrap><PreferencesUnsubscribe /></PageWrap>
}
/>
<Route
path={DECODE_ROUTES.ACCESS_DENIED}
element={<DecodePageRoute><CourseAccessErrorPage /></DecodePageRoute>}
/>
<Route
path={DECODE_ROUTES.HOME}
element={(
<DecodePageRoute>
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</DecodePageRoute>
)}
/>
<Route
path={DECODE_ROUTES.LIVE}
element={(
<DecodePageRoute>
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
<LiveTab />
</TabContainer>
</DecodePageRoute>
)}
/>
<Route
path={DECODE_ROUTES.DATES}
element={(
<DecodePageRoute>
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</DecodePageRoute>
)}
/>
<Route
path={DECODE_ROUTES.DISCUSSION}
element={(
<DecodePageRoute>
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
</DecodePageRoute>
)}
/>
{DECODE_ROUTES.PROGRESS.map((route) => (
<div className="app-container">
<Routes>
<Route path="*" element={<PageWrap><PageNotFound /></PageWrap>} />
<Route path={ROUTES.UNSUBSCRIBE} element={<PageWrap><GoalUnsubscribe /></PageWrap>} />
<Route path={ROUTES.REDIRECT} element={<PageWrap><CoursewareRedirectLandingPage /></PageWrap>} />
<Route
key={route}
path={route}
path={ROUTES.PREFERENCES_UNSUBSCRIBE}
element={
<PageWrap><PreferencesUnsubscribe /></PageWrap>
}
/>
<Route
path={DECODE_ROUTES.ACCESS_DENIED}
element={<DecodePageRoute><CourseAccessErrorPage /></DecodePageRoute>}
/>
<Route
path={DECODE_ROUTES.HOME}
element={(
<DecodePageRoute>
<TabContainer
tab="progress"
fetch={fetchProgressTab}
slice="courseHome"
isProgressTab
>
<ProgressTab />
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
<OutlineTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
))}
<Route
path={DECODE_ROUTES.COURSE_END}
element={(
<DecodePageRoute>
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</DecodePageRoute>
)}
/>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
key={route}
path={route}
path={DECODE_ROUTES.LIVE}
element={(
<DecodePageRoute>
<CoursewareContainer />
<TabContainer tab="lti_live" fetch={fetchLiveTab} slice="courseHome">
<LiveTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
))}
</Routes>
<Route
path={DECODE_ROUTES.DATES}
element={(
<DecodePageRoute>
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
<DatesTab />
</TabContainer>
</DecodePageRoute>
)}
/>
<Route
path={DECODE_ROUTES.DISCUSSION}
element={(
<DecodePageRoute>
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
<DiscussionTab />
</TabContainer>
</DecodePageRoute>
)}
/>
{DECODE_ROUTES.PROGRESS.map((route) => (
<Route
key={route}
path={route}
element={(
<DecodePageRoute>
<TabContainer
tab="progress"
fetch={fetchProgressTab}
slice="courseHome"
isProgressTab
>
<ProgressTab />
</TabContainer>
</DecodePageRoute>
)}
/>
))}
<Route
path={DECODE_ROUTES.COURSE_END}
element={(
<DecodePageRoute>
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />
</TabContainer>
</DecodePageRoute>
)}
/>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
key={route}
path={route}
element={(
<DecodePageRoute>
<CoursewareContainer />
</DecodePageRoute>
)}
/>
))}
</Routes>
</div>
</UserMessagesProvider>
</NoticesProvider>
</PathFixesProvider>

View File

@@ -8,13 +8,20 @@
#root {
display: flex;
flex-direction: column;
min-height: 100vh;
.app-container {
display: flex;
flex-direction: column;
min-height: 100svh;
}
main {
flex-grow: 1;
}
#main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
}
header {
flex: 0 0 auto;