From 17f7c60f4456118282904ce5d18524cc98321825 Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Fri, 17 Jul 2026 23:41:55 +0530 Subject: [PATCH 1/2] refactor: Separate course outline data and sidebar context logic Splits useCourseOutlineSidebar into useCourseOutlineData and useCourseOutlineSidebar for improved separation of responsibilities. Updates components to reflect this change. This allows the course outline to be used outside of a sidebar context. --- .../sidebars/course-outline/CourseOutline.tsx | 43 ++++--- .../course-outline/CourseOutlineTray.tsx | 4 +- .../course-outline/CourseOutlineTrigger.jsx | 4 +- .../components/SidebarSection.jsx | 4 +- .../components/SidebarSequence.jsx | 4 +- .../components/UnitLinkWrapper.tsx | 14 ++- .../sidebar/sidebars/course-outline/hooks.js | 110 +++++++++--------- 7 files changed, 102 insertions(+), 81 deletions(-) diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx index c06966f524..48dbf71078 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx @@ -2,33 +2,43 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useToggle } from '@openedx/paragon'; import { LOADING } from '@src/constants'; +import { + useCourseOutlineData, +} from '@src/courseware/course/sidebar/sidebars/course-outline/hooks'; import PageLoading from '@src/generic/PageLoading'; import { CourseOutlineSidebarHeadingSlot } from '@src/plugin-slots/CourseOutlineSidebarHeadingSlot'; import classNames from 'classnames'; import { useState } from 'react'; -import SidebarSection from './components/SidebarSection'; +import { useParams } from 'react-router-dom'; import SidebarSequence from './components/SidebarSequence'; -import { ID } from './constants'; -import { useCourseOutlineSidebar } from './hooks'; +import SidebarSection from './components/SidebarSection'; import messages from './messages'; -export const CourseOutline = () => { +interface CourseOutlineProps { + shouldDisplayFullScreen?: boolean; + onToggleCollapse?: () => void; +} + +interface CoursePageParams extends Record { + courseId: string; + unitId: string; +} + +export const CourseOutline = ({ + shouldDisplayFullScreen = false, + onToggleCollapse, +}: CourseOutlineProps) => { const intl = useIntl(); const [selectedSection, setSelectedSection] = useState(null); const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true); - + const { unitId, courseId } = useParams(); const { - courseId, - unitId, - currentSidebar, - isActiveEntranceExam, courseOutlineStatus, activeSequenceId, sections, sequences, - shouldDisplayFullScreen, - handleToggleCollapse, - } = useCourseOutlineSidebar(); + isActiveEntranceExam, + } = useCourseOutlineData(); const resolvedSectionId = selectedSection || Object.keys(sections).find( @@ -47,19 +57,16 @@ export const CourseOutline = () => { setDisplaySequenceLevel(); setSelectedSection(id); }; - const sidebarHeading = ( ); - - if (isActiveEntranceExam || currentSidebar !== ID) { + if (isActiveEntranceExam) { return null; } - if (courseOutlineStatus === LOADING) { return (
{
); }; - -export default CourseOutline; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx index f35775a092..88f30fcfbe 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx @@ -5,12 +5,14 @@ import { useCourseOutlineSidebar } from './hooks'; const CourseOutlineTray = () => { const { currentSidebar, + shouldDisplayFullScreen, + handleToggleCollapse, } = useCourseOutlineSidebar(); if (currentSidebar !== ID) { return null; } - return ; + return ; }; CourseOutlineTray.ID = ID; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx index abccd14aed..3d6039598f 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx @@ -4,17 +4,17 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { IconButton } from '@openedx/paragon'; import { MenuOpen as MenuOpenIcon } from '@openedx/paragon/icons'; -import { useCourseOutlineSidebar } from './hooks'; +import { useCourseOutlineData, useCourseOutlineSidebar } from './hooks'; import { ID } from './constants'; import messages from './messages'; const CourseOutlineTrigger = ({ isMobileView }) => { const intl = useIntl(); + const { isActiveEntranceExam } = useCourseOutlineData(); const { currentSidebar, shouldDisplayFullScreen, handleToggleCollapse, - isActiveEntranceExam, } = useCourseOutlineSidebar(); const isDisplayForDesktopView = !isMobileView && !shouldDisplayFullScreen && currentSidebar !== ID; 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 5e7a30879a..dacbe6dfcb 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx @@ -8,7 +8,7 @@ import { Button, Icon } from '@openedx/paragon'; import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons'; import courseOutlineMessages from '@src/course-home/outline-tab/messages'; -import { useCourseOutlineSidebar } from '../hooks'; +import { useCourseOutlineData } from '../hooks'; const SidebarSection = ({ section, handleSelectSection }) => { const intl = useIntl(); @@ -20,7 +20,7 @@ const SidebarSection = ({ section, handleSelectSection }) => { completionStat, } = section; - const { activeSequenceId, isEnabledCompletionTracking } = useCourseOutlineSidebar(); + const { activeSequenceId, isEnabledCompletionTracking } = useCourseOutlineData(); const isActiveSection = sequenceIds.includes(activeSequenceId); const sectionTitle = ( 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 fa0f16435c..ddf9852759 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx @@ -8,7 +8,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Collapsible } from '@openedx/paragon'; import courseOutlineMessages from '@src/course-home/outline-tab/messages'; -import { useCourseOutlineSidebar } from '../hooks'; +import { useCourseOutlineData } from '../hooks'; import SidebarUnit from './SidebarUnit'; import { UNIT_ICON_TYPES } from './UnitIcon'; @@ -30,7 +30,7 @@ const SidebarSequence = ({ } = sequence; const [open, setOpen] = useState(defaultOpen); - const { activeSequenceId, units, isEnabledCompletionTracking } = useCourseOutlineSidebar(); + const { activeSequenceId, units, isEnabledCompletionTracking } = useCourseOutlineData(); const isActiveSequence = id === activeSequenceId; const sectionTitle = ( diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/UnitLinkWrapper.tsx b/src/courseware/course/sidebar/sidebars/course-outline/components/UnitLinkWrapper.tsx index 771eb5f7d5..0bb158c92d 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/UnitLinkWrapper.tsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/UnitLinkWrapper.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { useCourseOutlineSidebar } from '../hooks'; +import { useCourseOutlineData, useCourseOutlineSidebar } from '../hooks'; interface Props { courseId: string; @@ -26,17 +26,25 @@ const UnitLinkWrapper: React.FC = ({ courseId, children, }) => { - const { handleUnitClick } = useCourseOutlineSidebar(); + const { handleUnitClick } = useCourseOutlineData(); + const { shouldDisplayFullScreen, handleToggleCollapse } = useCourseOutlineSidebar(); const { pathname } = useLocation(); const isPreview = pathname.startsWith('/preview'); const baseUrl = `/course/${courseId}/${sequenceId}/${id}`; const link = isPreview ? `/preview${baseUrl}` : baseUrl; + const handleClick = React.useCallback(() => { + // Hide the sidebar after selecting a unit on a mobile device. + if (shouldDisplayFullScreen) { + handleToggleCollapse(); + } + handleUnitClick({ sequenceId, activeUnitId, id }); + }, [handleUnitClick, sequenceId, activeUnitId, id]); return ( handleUnitClick({ sequenceId, activeUnitId, id })} + onClick={handleClick} > {children} diff --git a/src/courseware/course/sidebar/sidebars/course-outline/hooks.js b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js index 6b3ca556fc..9cd2499389 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/hooks.js +++ b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js @@ -1,27 +1,26 @@ -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'; -import { checkBlockCompletion, getCourseOutlineStructure } from '@src/courseware/data/thunks'; import { - getCoursewareOutlineSidebarSettings, + getCourseOutline, getCourseOutlineShouldUpdate, getCourseOutlineStatus, + getCoursewareOutlineSidebarSettings, getSequenceId, - getCourseOutline, getSequenceStatus, } from '@src/courseware/data/selectors'; +import { checkBlockCompletion, getCourseOutlineStructure } from '@src/courseware/data/thunks'; + +import { useModel } from '@src/generic/model-store'; +import { + useContext, useEffect, useLayoutEffect, useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; import SidebarContext from '../../SidebarContext'; import { ID } from './constants'; -// eslint-disable-next-line import/prefer-default-export -export const useCourseOutlineSidebar = () => { +export const useCourseOutlineData = () => { const dispatch = useDispatch(); const { enableCompletionTracking: isEnabledCompletionTracking, @@ -30,41 +29,26 @@ export const useCourseOutlineSidebar = () => { const courseOutlineStatus = useSelector(getCourseOutlineStatus); const sequenceStatus = useSelector(getSequenceStatus); const activeSequenceId = useSelector(getSequenceId); - const { sections = {}, sequences = {}, units = {} } = useSelector(getCourseOutline); + const { + sections = {}, + sequences = {}, + units = {}, + } = useSelector(getCourseOutline); const { courseId } = useParams(); const course = useModel('coursewareMeta', courseId); - const { - unitId, - currentSidebar, - toggleSidebar, - shouldDisplayFullScreen, - } = useContext(SidebarContext); - - // Course outline state is now fully controlled by SidebarContextProvider - // This component only renders when currentSidebar === 'COURSE_OUTLINE' - const [isOpen, setIsOpen] = useState(true); - const { entranceExamEnabled, entranceExamPassed, } = course.entranceExamData || {}; const isActiveEntranceExam = entranceExamEnabled && !entranceExamPassed; - const collapseSidebar = () => { - toggleSidebar(null); - }; - - const handleToggleCollapse = () => { - if (currentSidebar === ID) { - collapseSidebar(); - } else { - toggleSidebar(ID); - } - }; - - const handleUnitClick = ({ sequenceId, activeUnitId, id }) => { + const handleUnitClick = ({ + sequenceId, + activeUnitId, + id, + }) => { const logEvent = (eventName, widgetPlacement) => { const findSequenceByUnitId = () => Object.values(sequences).find(seq => seq.unitIds.includes(activeUnitId)); const activeSequence = findSequenceByUnitId(activeUnitId); @@ -88,11 +72,6 @@ export const useCourseOutlineSidebar = () => { logEvent('edx.ui.lms.sequence.tab_selected', 'left'); dispatch(checkBlockCompletion(courseId, sequenceId, activeUnitId)); - - // Hide the sidebar after selecting a unit on a mobile device. - if (shouldDisplayFullScreen) { - handleToggleCollapse(); - } }; // Load course outline structure when needed @@ -102,6 +81,43 @@ export const useCourseOutlineSidebar = () => { } }, [courseId, courseOutlineShouldUpdate]); + return { + isEnabledCompletionTracking, + isActiveEntranceExam, + courseOutlineStatus, + activeSequenceId, + sections, + sequences, + units, + handleUnitClick, + sequenceStatus, + }; +}; + +export const useCourseOutlineSidebar = () => { + const { + unitId, + currentSidebar, + toggleSidebar, + shouldDisplayFullScreen, + } = useContext(SidebarContext); + + // Course outline state is now fully controlled by SidebarContextProvider + // This component only renders when currentSidebar === 'COURSE_OUTLINE' + const [isOpen, setIsOpen] = useState(true); + + const collapseSidebar = () => { + toggleSidebar(null); + }; + + const handleToggleCollapse = () => { + if (currentSidebar === ID) { + collapseSidebar(); + } else { + toggleSidebar(ID); + } + }; + // Collapse sidebar if screen resized to a width that displays the sidebar automatically useLayoutEffect(() => { const handleResize = () => { @@ -118,21 +134,11 @@ export const useCourseOutlineSidebar = () => { }, [currentSidebar]); return { - courseId, unitId, currentSidebar, shouldDisplayFullScreen, - isEnabledCompletionTracking, isOpen, setIsOpen, handleToggleCollapse, - isActiveEntranceExam, - courseOutlineStatus, - activeSequenceId, - sections, - sequences, - units, - handleUnitClick, - sequenceStatus, }; }; From c3daaa04fec77754bc046698086b708684db45dc Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Fri, 17 Jul 2026 23:42:48 +0530 Subject: [PATCH 2/2] style: Update course outline sidebar styling and use theme tokens - Replace hardcoded border color (#d7d3d1) with var(--pgn-color-light-700) in CourseOutlineTray.scss for theme compatibility. - Move active-section highlight to a dedicated .active-section class on SidebarSection and add an .active-unit class on SidebarUnit. --- .../sidebar/sidebars/course-outline/CourseOutlineTray.scss | 4 ++-- .../sidebars/course-outline/components/SidebarSection.jsx | 7 ++----- .../sidebars/course-outline/components/SidebarUnit.jsx | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss index f051d248ea..ec8bb50230 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss @@ -13,7 +13,7 @@ } .outline-sidebar-heading-wrapper { - border: 1px solid #d7d3d1; + border: 1px solid var(--pgn-color-light-700); &.sticky { position: sticky; @@ -29,7 +29,7 @@ .course-sidebar-section { background: var(--pgn-color-white); - border: 1px solid #d7d3d1; + border: 1px solid var(--pgn-color-light-700); button { line-height: 1.75rem; 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 dacbe6dfcb..0562b38d77 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx @@ -48,13 +48,10 @@ const SidebarSection = ({ section, handleSelectSection }) => { ); return ( -
  • +