diff --git a/package.json b/package.json
index b4351459..14a2e49e 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,9 @@
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
+ "start:with-theme": "paragon install-theme && npm start && npm install",
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
- "test": "fedx-scripts jest --coverage --passWithNoTests",
+ "test": "NODE_ENV=test fedx-scripts jest --coverage --passWithNoTests",
"test:watch": "fedx-scripts jest --watch --passWithNoTests",
"types": "tsc --noEmit"
},
@@ -97,4 +98,4 @@
],
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
}
-}
+}
\ No newline at end of file
diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/DiscussionsNotificationsSidebar.tsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/DiscussionsNotificationsSidebar.tsx
index 1912438a..d98ffcec 100644
--- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/DiscussionsNotificationsSidebar.tsx
+++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/DiscussionsNotificationsSidebar.tsx
@@ -21,9 +21,9 @@ const DiscussionsNotificationsSidebar = () => {
showTitleBar={false}
showBorder={false}
>
-
- {!hideNotificationbar &&
}
+ {!hideNotificationbar && }
+
);
};
diff --git a/src/courseware/course/sidebar/common/SidebarBase.scss b/src/courseware/course/sidebar/common/SidebarBase.scss
index d705c252..1ab5f41c 100644
--- a/src/courseware/course/sidebar/common/SidebarBase.scss
+++ b/src/courseware/course/sidebar/common/SidebarBase.scss
@@ -1,5 +1,5 @@
#course-sidebar {
- @media (max-width: -1 + map-get($grid-breakpoints, "lg")) {
+ @media (max-width: map-get($grid-breakpoints, "lg")) {
overflow-y: scroll;
padding: 0 .625rem !important;
}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss
index 5fd9a0b7..8376f4aa 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss
@@ -66,7 +66,7 @@
border-radius: 0;
padding: map-get($spacers, 3\.5) map-get($spacers, 4) map-get($spacers, 3\.5) map-get($spacers, 5);
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
padding-left: map-get($spacers, 4);
}
@@ -90,7 +90,7 @@
ol li > a {
padding: map-get($spacers, 3\.5) map-get($spacers, 4) map-get($spacers, 3\.5) map-get($spacers, 5\.5);
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
padding-left: map-get($spacers, 4\.5);
}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
index 5fda3616..1f826a69 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
@@ -102,6 +102,21 @@ describe('', () => {
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
});
+ it('collapses sidebar correctly when screen is resized', async () => {
+ const mockToggleSidebar = jest.fn();
+ await initTestStore();
+ renderWithProvider({ toggleSidebar: mockToggleSidebar });
+
+ const collapseBtn = screen.getByRole('button', { name: messages.toggleCourseOutlineTrigger.defaultMessage });
+ expect(collapseBtn).toBeInTheDocument();
+
+ // Simulate screen resize
+ window.innerWidth = 500;
+ window.dispatchEvent(new Event('resize'));
+
+ expect(mockToggleSidebar).toHaveBeenCalledWith(null);
+ });
+
it('navigates to section or sequence level correctly on click by back/section button', async () => {
const user = userEvent.setup();
await initTestStore();
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx
index 19ce686e..9b3a855d 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx
@@ -6,12 +6,12 @@ import {
import { DashedCircleIcon } from '../icons';
-const CompletionIcon = ({ completionStat: { completed = 0, total = 0 } }) => {
+const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled }) => {
const percentage = total !== 0 ? Math.min((completed / total) * 100, 100) : 0;
const remainder = 100 - percentage;
switch (true) {
- case !completed:
+ case !completed || !enabled:
return ;
case completed === total:
return ;
@@ -25,6 +25,7 @@ CompletionIcon.propTypes = {
completed: PropTypes.number,
total: PropTypes.number,
}).isRequired,
+ enabled: PropTypes.bool.isRequired,
};
export default CompletionIcon;
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx
index 3bbb3f8a..cb026fb4 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx
@@ -3,21 +3,33 @@ import { render, screen } from '@testing-library/react';
import CompletionIcon from './CompletionIcon';
describe('CompletionIcon', () => {
- it('renders check circle icon when completion is equal to total', () => {
+ it('renders check circle icon when completion is equal to total and completion tracking is enabled', () => {
const completionStat = { completed: 5, total: 5 };
- render();
+ render();
expect(screen.getByTestId('check-circle-icon')).toBeInTheDocument();
});
- it('renders dashed circle icon when completion is between 0 and total', () => {
+ it('renders dashed circle icon when completion is between 0 and total and completion tracking is enabled', () => {
const completionStat = { completed: 2, total: 5 };
- render();
+ render();
expect(screen.getByTestId('dashed-circle-icon')).toBeInTheDocument();
});
- it('renders completion solid icon when completion is 0', () => {
+ it('renders completion solid icon when completion is between 0 and total and completion tracking is not enabled', () => {
+ const completionStat = { completed: 2, total: 5 };
+ render();
+ expect(screen.getByTestId('completion-solid-icon')).toBeInTheDocument();
+ });
+
+ it('renders completion solid icon when completion is 0 and enabled', () => {
const completionStat = { completed: 0, total: 5 };
- render();
+ render();
+ expect(screen.getByTestId('completion-solid-icon')).toBeInTheDocument();
+ });
+
+ it('renders completion solid icon when completion is at any value and not enabled', () => {
+ const completionStat = { completed: 0, total: 5 };
+ render();
expect(screen.getByTestId('completion-solid-icon')).toBeInTheDocument();
});
});
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx
index 2fb02ab8..03457574 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx
@@ -18,21 +18,24 @@ const SidebarSection = ({ section, handleSelectSection }) => {
completionStat,
} = section;
- const { activeSequenceId } = useCourseOutlineSidebar();
+ const { activeSequenceId, isEnabledCompletionTracking } = useCourseOutlineSidebar();
const isActiveSection = sequenceIds.includes(activeSequenceId);
const sectionTitle = (
<>
-
+
{title}
-
- , {intl.formatMessage(complete
- ? courseOutlineMessages.completedSection
- : courseOutlineMessages.incompleteSection)}
-
+ {isEnabledCompletionTracking && (
+
+ , {intl.formatMessage(complete
+ ? courseOutlineMessages.completedSection
+ : courseOutlineMessages.incompleteSection)}
+
+ )}
+
>
);
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx
index 791b1581..5e1fb37a 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx
@@ -28,22 +28,24 @@ const SidebarSequence = ({
} = sequence;
const [open, setOpen] = useState(defaultOpen);
- const { activeSequenceId, units } = useCourseOutlineSidebar();
+ const { activeSequenceId, units, isEnabledCompletionTracking } = useCourseOutlineSidebar();
const isActiveSequence = id === activeSequenceId;
const sectionTitle = (
<>
-
+
{title}
{specialExamInfo && {specialExamInfo}}
-
- , {intl.formatMessage(complete
- ? courseOutlineMessages.completedAssignment
- : courseOutlineMessages.incompleteAssignment)}
-
+ {isEnabledCompletionTracking && (
+
+ , {intl.formatMessage(complete
+ ? courseOutlineMessages.completedAssignment
+ : courseOutlineMessages.incompleteAssignment)}
+
+ )}
>
);
@@ -69,6 +71,7 @@ const SidebarSequence = ({
activeUnitId={activeUnitId}
isFirst={index === 0}
isLocked={type === UNIT_ICON_TYPES.lock}
+ isCompletionTrackingEnabled={isEnabledCompletionTracking}
/>
))}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.test.jsx
index 25f15bb3..c88ad22c 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.test.jsx
@@ -66,7 +66,7 @@ describe('', () => {
expect(screen.queryByText(unit.title)).not.toBeInTheDocument();
});
- it('renders correctly when sequence is not collapsed and complete', async () => {
+ it('renders correctly when sequence is not collapsed and complete and completion tracking enabled', async () => {
const user = userEvent.setup();
await initTestStore();
renderWithProvider({
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx
index 70a8dcb4..b2f6bc72 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx
@@ -15,6 +15,7 @@ const SidebarUnit = ({
isActive,
isLocked,
activeUnitId,
+ isCompletionTrackingEnabled,
}) => {
const intl = useIntl();
const {
@@ -24,6 +25,7 @@ const SidebarUnit = ({
} = unit;
const iconType = isLocked ? UNIT_ICON_TYPES.lock : icon;
+ const completeAndEnabled = complete && isCompletionTrackingEnabled;
return (
@@ -36,15 +38,17 @@ const SidebarUnit = ({
}}
>
-
+
{title}
-
- , {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
-
+ {isCompletionTrackingEnabled && (
+
+ , {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
+
+ )}
@@ -66,6 +70,7 @@ SidebarUnit.propTypes = {
courseId: PropTypes.string.isRequired,
sequenceId: PropTypes.string.isRequired,
activeUnitId: PropTypes.string.isRequired,
+ isCompletionTrackingEnabled: PropTypes.bool.isRequired,
};
export default SidebarUnit;
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.test.jsx
index 64cf719f..496c4a9c 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.test.jsx
@@ -50,6 +50,7 @@ describe('', () => {
unit={{ ...unit, icon: 'video', isLocked: false }}
isActive={false}
activeUnitId={unit.id}
+ isCompletionTrackingEnabled
{...props}
/>
@@ -68,7 +69,7 @@ describe('', () => {
expect(container.querySelector('.text-success')).not.toBeInTheDocument();
});
- it('renders correctly when unit is complete', async () => {
+ it('renders correctly when unit is complete and tracking enabled', async () => {
await initTestStore();
const container = renderWithProvider({ unit: { ...unit, complete: true } });
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js
similarity index 80%
rename from src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx
rename to src/courseware/course/sidebar/sidebars/course-outline/hooks.js
index c685fe59..6d4ea8ca 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/hooks.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js
@@ -1,7 +1,10 @@
-import { useContext, useEffect, useState } from 'react';
+import {
+ useContext, useEffect, useLayoutEffect, useState,
+} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
+import { breakpoints } from '@openedx/paragon';
import { useModel } from '@src/generic/model-store';
import { LOADED } from '@src/constants';
@@ -22,7 +25,10 @@ import { ID } from './constants';
export const useCourseOutlineSidebar = () => {
const dispatch = useDispatch();
const isCollapsedOutlineSidebar = window.sessionStorage.getItem('hideCourseOutlineSidebar');
- const { enableNavigationSidebar: isEnabledSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
+ const {
+ enableNavigationSidebar: isEnabledSidebar,
+ enableCompletionTracking: isEnabledCompletionTracking,
+ } = useSelector(getCoursewareOutlineSidebarSettings);
const courseOutlineShouldUpdate = useSelector(getCourseOutlineShouldUpdate);
const courseOutlineStatus = useSelector(getCourseOutlineStatus);
const sequenceStatus = useSelector(getSequenceStatus);
@@ -51,10 +57,14 @@ export const useCourseOutlineSidebar = () => {
} = course.entranceExamData || {};
const isActiveEntranceExam = entranceExamEnabled && !entranceExamPassed;
+ const collapseSidebar = () => {
+ toggleSidebar(null);
+ window.sessionStorage.setItem('hideCourseOutlineSidebar', 'true');
+ };
+
const handleToggleCollapse = () => {
if (currentSidebar === ID) {
- toggleSidebar(null);
- window.sessionStorage.setItem('hideCourseOutlineSidebar', 'true');
+ collapseSidebar();
} else {
toggleSidebar(ID);
window.sessionStorage.removeItem('hideCourseOutlineSidebar');
@@ -104,12 +114,28 @@ export const useCourseOutlineSidebar = () => {
}
}, [courseId, isEnabledSidebar, courseOutlineShouldUpdate]);
+ // Collapse sidebar if screen resized to a width that displays the sidebar automatically
+ useLayoutEffect(() => {
+ const handleResize = () => {
+ // breakpoints.large.maxWidth is 1200px and currently the breakpoint for showing the sidebar
+ if (currentSidebar === ID && global.innerWidth < breakpoints.large.maxWidth) {
+ collapseSidebar();
+ }
+ };
+
+ global.addEventListener('resize', handleResize);
+ return () => {
+ global.removeEventListener('resize', handleResize);
+ };
+ }, [isOpen]);
+
return {
courseId,
unitId,
currentSidebar,
shouldDisplayFullScreen,
isEnabledSidebar,
+ isEnabledCompletionTracking,
isOpen,
setIsOpen,
handleToggleCollapse,
diff --git a/src/courseware/course/sidebar/sidebars/discussions/Discussions.scss b/src/courseware/course/sidebar/sidebars/discussions/Discussions.scss
index fe94248b..28d4fb89 100644
--- a/src/courseware/course/sidebar/sidebars/discussions/Discussions.scss
+++ b/src/courseware/course/sidebar/sidebars/discussions/Discussions.scss
@@ -1,5 +1,5 @@
.discussions-sidebar-frame {
- @media (max-width: -1 + map-get($grid-breakpoints, "xl")) {
+ @media (max-width: map-get($grid-breakpoints, "xl")) {
max-height: calc(100vh - 65px);
}
}
diff --git a/src/courseware/data/api.js b/src/courseware/data/api.js
index 199aa37c..b19dce4c 100644
--- a/src/courseware/data/api.js
+++ b/src/courseware/data/api.js
@@ -115,5 +115,6 @@ export async function getCoursewareOutlineSidebarToggles(courseId) {
return {
enable_navigation_sidebar: data.enable_navigation_sidebar || false,
always_open_auxiliary_sidebar: data.always_open_auxiliary_sidebar || false,
+ enable_completion_tracking: data.enable_completion_tracking || false,
};
}
diff --git a/src/courseware/data/redux.test.js b/src/courseware/data/redux.test.js
index 2c8f5469..91b6e210 100644
--- a/src/courseware/data/redux.test.js
+++ b/src/courseware/data/redux.test.js
@@ -113,6 +113,7 @@ describe('Data layer integration tests', () => {
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
enable_navigation_sidebar: true,
always_open_auxiliary_sidebar: true,
+ enable_completion_tracking: true,
});
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
@@ -126,6 +127,7 @@ describe('Data layer integration tests', () => {
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
enableNavigationSidebar: true,
alwaysOpenAuxiliarySidebar: true,
+ enableCompletionTracking: true,
});
// check that at least one key camel cased, thus course data normalized
@@ -154,6 +156,7 @@ describe('Data layer integration tests', () => {
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
enableNavigationSidebar: false,
alwaysOpenAuxiliarySidebar: false,
+ enableCompletionTracking: false,
});
// check that at least one key camel cased, thus course data normalized
diff --git a/src/courseware/data/thunks.js b/src/courseware/data/thunks.js
index 3f84fbf2..15d36f72 100644
--- a/src/courseware/data/thunks.js
+++ b/src/courseware/data/thunks.js
@@ -90,8 +90,11 @@ export function fetchCourse(courseId) {
const {
enable_navigation_sidebar: enableNavigationSidebar,
always_open_auxiliary_sidebar: alwaysOpenAuxiliarySidebar,
+ enable_completion_tracking: enableCompletionTracking,
} = coursewareOutlineSidebarTogglesResult.value;
- dispatch(setCoursewareOutlineSidebarToggles({ enableNavigationSidebar, alwaysOpenAuxiliarySidebar }));
+ dispatch(setCoursewareOutlineSidebarToggles(
+ { enableNavigationSidebar, alwaysOpenAuxiliarySidebar, enableCompletionTracking },
+ ));
}
// Log errors for each request if needed. Outline failures may occur
diff --git a/src/index.scss b/src/index.scss
index fe517831..29304d60 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -87,7 +87,7 @@
}
.notification-btn {
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
height: 3rem;
}
}
@@ -96,7 +96,7 @@
display: flex;
flex-grow: 1;
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
max-width: 100%;
}
@@ -104,7 +104,7 @@
margin: -1px -1px 0;
}
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
width: 100% !important;
}
@@ -249,7 +249,7 @@
justify-content: center;
align-items: center;
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
padding-top: 1rem;
padding-bottom: 1rem;
}
@@ -332,7 +332,7 @@
max-width: 640px;
margin: 0 auto;
- @media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
+ @media (max-width: map-get($grid-breakpoints, "sm")) {
flex-direction: column;
gap: $spacer;
}
diff --git a/src/setupTest.js b/src/setupTest.js
index 63da82ab..0e51c814 100755
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -179,6 +179,7 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
const provider = options?.provider || 'legacy';
const enableNavigationSidebar = options.enableNavigationSidebar || { enable_navigation_sidebar: true };
const alwaysOpenAuxiliarySidebar = options.alwaysOpenAuxiliarySidebar || { always_open_auxiliary_sidebar: true };
+ const enableCompletionTracking = options.enableCompletionTracking || { enable_completion_tracking: true };
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
@@ -187,6 +188,7 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
...enableNavigationSidebar,
...alwaysOpenAuxiliarySidebar,
+ ...enableCompletionTracking,
});
axiosMock.onGet(outlineSidebarUrl).reply(200, {