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, {