From 9654f53f8ba05b4b8a7757ec2ed132aa65d5327f Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Tue, 28 Apr 2026 22:53:53 +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 and styles to reflect this change. This change allows the usage of the course outline outside of a sidebar context. --- .../course-outline/CourseOutlineTray.jsx | 39 ++++-- .../course-outline/CourseOutlineTray.scss | 4 +- .../course-outline/CourseOutlineTrigger.jsx | 4 +- .../components/SidebarSection.jsx | 4 +- .../components/SidebarSequence.jsx | 4 +- .../course-outline/components/SidebarUnit.jsx | 2 +- .../components/UnitLinkWrapper.tsx | 14 +- .../sidebar/sidebars/course-outline/hooks.js | 131 +++++++++++------- 8 files changed, 123 insertions(+), 79 deletions(-) diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx index 8eaf5e8a0b..2609f48f3e 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx @@ -9,29 +9,25 @@ import { import { LOADING } from '@src/constants'; import PageLoading from '@src/generic/PageLoading'; +import { useParams } from 'react-router-dom'; import SidebarSection from './components/SidebarSection'; import SidebarSequence from './components/SidebarSequence'; import { ID } from './constants'; -import { useCourseOutlineSidebar } from './hooks'; +import { useCourseOutlineData, useCourseOutlineSidebar } from './hooks'; import messages from './messages'; -const CourseOutlineTray = () => { +export const CourseOutline = ({ shouldDisplayFullScreen = false, onToggleCollapse = null }) => { const intl = useIntl(); const [selectedSection, setSelectedSection] = useState(null); const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true); - + const { unitId, courseId } = useParams(); const { - courseId, - unitId, - currentSidebar, - handleToggleCollapse, - isActiveEntranceExam, - shouldDisplayFullScreen, courseOutlineStatus, activeSequenceId, sections, sequences, - } = useCourseOutlineSidebar(); + isActiveEntranceExam, + } = useCourseOutlineData(); const resolvedSectionId = selectedSection || Object.keys(sections).find( @@ -50,7 +46,6 @@ const CourseOutlineTray = () => { setDisplaySequenceLevel(); setSelectedSection(id); }; - const sidebarHeading = (
{isDisplaySequenceLevel && backButtonTitle ? ( @@ -67,19 +62,20 @@ const CourseOutlineTray = () => { {intl.formatMessage(messages.courseOutlineTitle)} )} + {onToggleCollapse + && ( + )}
); - - if (isActiveEntranceExam || currentSidebar !== ID) { + if (isActiveEntranceExam) { return null; } - if (courseOutlineStatus === LOADING) { return (
{ ); }; +const CourseOutlineTray = () => { + const { + currentSidebar, + shouldDisplayFullScreen, + handleToggleCollapse, + } = useCourseOutlineSidebar(); + + if (currentSidebar !== ID) { + return null; + } + return ; +}; + CourseOutlineTray.ID = ID; export default CourseOutlineTray; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss index a574f02736..82c68f4452 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/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 034575743b..246201ff37 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx @@ -6,7 +6,7 @@ import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons'; import courseOutlineMessages from '@src/course-home/outline-tab/messages'; import CompletionIcon from './CompletionIcon'; -import { useCourseOutlineSidebar } from '../hooks'; +import { useCourseOutlineData } from '../hooks'; const SidebarSection = ({ section, handleSelectSection }) => { const intl = useIntl(); @@ -18,7 +18,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 5e1fb37a4f..e612a93c2d 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx @@ -5,7 +5,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 CompletionIcon from './CompletionIcon'; import SidebarUnit from './SidebarUnit'; import { UNIT_ICON_TYPES } from './UnitIcon'; @@ -28,7 +28,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/SidebarUnit.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx index b2f6bc7249..e88408319b 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx @@ -28,7 +28,7 @@ const SidebarUnit = ({ const completeAndEnabled = complete && isCompletionTrackingEnabled; return ( -
  • +
  • = ({ 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 1e180fa279..8b0a2360da 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/hooks.js +++ b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js @@ -1,28 +1,27 @@ -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 { setOutlineSidebarCollapsed } from '../../utils/storage'; import { ID } from './constants'; -// eslint-disable-next-line import/prefer-default-export -export const useCourseOutlineSidebar = () => { +export const useCourseOutlineData = () => { const dispatch = useDispatch(); const { enableCompletionTracking: isEnabledCompletionTracking, @@ -31,43 +30,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); - setOutlineSidebarCollapsed(true); - }; - - const handleToggleCollapse = () => { - if (currentSidebar === ID) { - collapseSidebar(); - } else { - toggleSidebar(ID); - setOutlineSidebarCollapsed(false); - } - }; - - 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); @@ -91,10 +73,64 @@ export const useCourseOutlineSidebar = () => { logEvent('edx.ui.lms.sequence.tab_selected', 'left'); dispatch(checkBlockCompletion(courseId, sequenceId, activeUnitId)); + }; + + // Load course outline structure when needed + useEffect(() => { + if (courseOutlineStatus !== LOADED || courseOutlineShouldUpdate) { + dispatch(getCourseOutlineStructure(courseId)); + } + }, [courseId, courseOutlineShouldUpdate]); + + return { + isEnabledCompletionTracking, + isActiveEntranceExam, + courseOutlineStatus, + activeSequenceId, + sections, + sequences, + units, + handleUnitClick, + sequenceStatus, + }; +}; - // Hide the sidebar after selecting a unit on a mobile device. - if (shouldDisplayFullScreen) { - handleToggleCollapse(); +export const useCourseOutlineSidebar = () => { + const dispatch = useDispatch(); + const courseOutlineShouldUpdate = useSelector(getCourseOutlineShouldUpdate); + const courseOutlineStatus = useSelector(getCourseOutlineStatus); + + 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); + setOutlineSidebarCollapsed(true); + }; + + const handleToggleCollapse = () => { + if (currentSidebar === ID) { + collapseSidebar(); + } else { + toggleSidebar(ID); + setOutlineSidebarCollapsed(false); } }; @@ -121,21 +157,12 @@ export const useCourseOutlineSidebar = () => { }, [currentSidebar]); return { - courseId, + isActiveEntranceExam, unitId, currentSidebar, shouldDisplayFullScreen, - isEnabledCompletionTracking, isOpen, setIsOpen, handleToggleCollapse, - isActiveEntranceExam, - courseOutlineStatus, - activeSequenceId, - sections, - sequences, - units, - handleUnitClick, - sequenceStatus, }; }; From cd853d0f8cb4fdbe339bcd612b25e7f17f3bb141 Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Wed, 29 Apr 2026 01:05:47 +0530 Subject: [PATCH 2/2] feat: Add new slots for customizing course outline section and sequence completion icons Introduces `CourseOutlineSidebarSectionCompletionIconSlot` and `CourseOutlineSidebarSequenceCompletionIconSlot` to enable customization, modification, or replacement of completion icons in the course outline. Updated components to integrate with these new slots. --- ...ourseOutlineTray.jsx => CourseOutline.tsx} | 106 +++++++----------- .../course-outline/CourseOutlineTray.scss | 2 +- .../course-outline/CourseOutlineTray.test.jsx | 6 +- .../course-outline/CourseOutlineTray.tsx | 20 ++++ ...nIcon.test.jsx => CompletionIcon.test.tsx} | 3 +- ...{CompletionIcon.jsx => CompletionIcon.tsx} | 19 ++-- .../components/CourseOutlineHeading.tsx | 51 +++++++++ .../components/SidebarSection.jsx | 17 +-- .../components/SidebarSequence.jsx | 10 +- .../components/UnitIcon.test.jsx | 1 + .../CourseOutlineSidebarHeadingSlot/README.md | 46 ++++++++ .../course-outline-heading-with-outline.png | Bin 0 -> 40059 bytes .../CourseOutlineSidebarHeadingSlot/index.tsx | 25 +++++ .../README.md | 51 +++++++++ ...course-outline-section-completion-icon.png | Bin 0 -> 72078 bytes .../course-outline-section-original.png | Bin 0 -> 66473 bytes .../index.tsx | 20 ++++ .../README.md | 48 ++++++++ ...ourse-outline-sequence-completion-icon.png | Bin 0 -> 55662 bytes .../index.tsx | 19 ++++ 20 files changed, 354 insertions(+), 90 deletions(-) rename src/courseware/course/sidebar/sidebars/course-outline/{CourseOutlineTray.jsx => CourseOutline.tsx} (51%) create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx rename src/courseware/course/sidebar/sidebars/course-outline/components/{CompletionIcon.test.jsx => CompletionIcon.test.tsx} (95%) rename src/courseware/course/sidebar/sidebars/course-outline/components/{CompletionIcon.jsx => CompletionIcon.tsx} (70%) create mode 100644 src/courseware/course/sidebar/sidebars/course-outline/components/CourseOutlineHeading.tsx create mode 100644 src/plugin-slots/CourseOutlineSidebarHeadingSlot/README.md create mode 100644 src/plugin-slots/CourseOutlineSidebarHeadingSlot/course-outline-heading-with-outline.png create mode 100644 src/plugin-slots/CourseOutlineSidebarHeadingSlot/index.tsx create mode 100644 src/plugin-slots/CourseOutlineSidebarSectionCompletionIconSlot/README.md create mode 100644 src/plugin-slots/CourseOutlineSidebarSectionCompletionIconSlot/course-outline-section-completion-icon.png create mode 100644 src/plugin-slots/CourseOutlineSidebarSectionCompletionIconSlot/course-outline-section-original.png create mode 100644 src/plugin-slots/CourseOutlineSidebarSectionCompletionIconSlot/index.tsx create mode 100644 src/plugin-slots/CourseOutlineSidebarSequenceCompletionIconSlot/README.md create mode 100644 src/plugin-slots/CourseOutlineSidebarSequenceCompletionIconSlot/course-outline-sequence-completion-icon.png create mode 100644 src/plugin-slots/CourseOutlineSidebarSequenceCompletionIconSlot/index.tsx diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx similarity index 51% rename from src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx index 2609f48f3e..425058277e 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutline.tsx @@ -1,26 +1,37 @@ -import { useState } from 'react'; -import classNames from 'classnames'; -import { Button, useToggle, IconButton } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { - MenuOpen as MenuOpenIcon, - ChevronLeft as ChevronLeftIcon, -} from '@openedx/paragon/icons'; - +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 { useParams } from 'react-router-dom'; -import SidebarSection from './components/SidebarSection'; import SidebarSequence from './components/SidebarSequence'; -import { ID } from './constants'; -import { useCourseOutlineData, useCourseOutlineSidebar } from './hooks'; +import SidebarSection from './components/SidebarSection'; import messages from './messages'; -export const CourseOutline = ({ shouldDisplayFullScreen = false, onToggleCollapse = null }) => { +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 [selectedSection, setSelectedSection] = useState(null); const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true); - const { unitId, courseId } = useParams(); + const { unitId, courseId } = useParams(); const { courseOutlineStatus, activeSequenceId, @@ -30,48 +41,29 @@ export const CourseOutline = ({ shouldDisplayFullScreen = false, onToggleCollaps } = useCourseOutlineData(); const resolvedSectionId = selectedSection - || Object.keys(sections).find( - (sectionId) => sections[sectionId].sequenceIds.includes(activeSequenceId), - ); + || Object.keys(sections).find( + (sectionId):boolean => sections[sectionId].sequenceIds.includes(activeSequenceId), + )!; const sectionsIds = Object.keys(sections); - const sequenceIds = sections[resolvedSectionId]?.sequenceIds || []; - const backButtonTitle = sections[resolvedSectionId]?.title; + const sequenceIds: string[] = sections[resolvedSectionId]?.sequenceIds || []; + const backButtonTitle: string | undefined = sections[resolvedSectionId]?.title; const handleBackToSectionLevel = () => { setDisplaySectionLevel(); setSelectedSection(null); }; - const handleSelectSection = (id) => { + const handleSelectSection = (id:string) => { setDisplaySequenceLevel(); setSelectedSection(id); }; const sidebarHeading = ( -
    - {isDisplaySequenceLevel && backButtonTitle ? ( - - ) : ( - - {intl.formatMessage(messages.courseOutlineTitle)} - - )} - {onToggleCollapse - && ( - - )} -
    + ); if (isActiveEntranceExam) { return null; @@ -103,19 +95,18 @@ export const CourseOutline = ({ shouldDisplayFullScreen = false, onToggleCollaps {sidebarHeading}
      {isDisplaySequenceLevel - ? sequenceIds.map((sequenceId) => ( + ? sequenceIds.map((sequenceId: string) => ( )) : sectionsIds.map((sectionId) => ( @@ -125,20 +116,3 @@ export const CourseOutline = ({ shouldDisplayFullScreen = false, onToggleCollaps
  • ); }; - -const CourseOutlineTray = () => { - const { - currentSidebar, - shouldDisplayFullScreen, - handleToggleCollapse, - } = useCourseOutlineSidebar(); - - if (currentSidebar !== ID) { - return null; - } - return ; -}; - -CourseOutlineTray.ID = ID; - -export default CourseOutlineTray; diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss index 82c68f4452..ec8bb50230 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.scss @@ -78,7 +78,7 @@ } &:last-child .pgn_collapsible { - margin-bottom: 0px !important; + margin-bottom: 0 !important; } } 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 aba41c8291..653a9bc8d5 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx @@ -74,7 +74,7 @@ describe('', () => { await expect(screen.queryByText(messages.loading.defaultMessage)).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: section.title })).toBeInTheDocument(); expect(screen.getByRole('button', { name: messages.toggleCourseOutlineTrigger.defaultMessage })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: `${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}` })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: new RegExp(`${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}`) })).toBeInTheDocument(); expect(screen.getByText(unit.title)).toBeInTheDocument(); }); @@ -115,13 +115,13 @@ describe('', () => { const sidebarBackBtn = screen.queryByRole('button', { name: section.title }); expect(sidebarBackBtn).toBeInTheDocument(); - expect(screen.getByRole('button', { name: `${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}` })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: new RegExp(`${sequence.title} , ${courseOutlineMessages.incompleteAssignment.defaultMessage}`) })).toBeInTheDocument(); await user.click(sidebarBackBtn); expect(sidebarBackBtn).not.toBeInTheDocument(); expect(screen.queryByText(messages.courseOutlineTitle.defaultMessage)).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: `${section.title} , ${courseOutlineMessages.incompleteSection.defaultMessage}` })); + await user.click(screen.getByRole('button', { name: new RegExp(`${section.title} , ${courseOutlineMessages.incompleteSection.defaultMessage}`) })); expect(screen.queryByRole('button', { name: section.title })).toBeInTheDocument(); }); }); diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx new file mode 100644 index 0000000000..88f30fcfbe --- /dev/null +++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.tsx @@ -0,0 +1,20 @@ +import { CourseOutline } from './CourseOutline'; +import { ID } from './constants'; +import { useCourseOutlineSidebar } from './hooks'; + +const CourseOutlineTray = () => { + const { + currentSidebar, + shouldDisplayFullScreen, + handleToggleCollapse, + } = useCourseOutlineSidebar(); + + if (currentSidebar !== ID) { + return null; + } + return ; +}; + +CourseOutlineTray.ID = ID; + +export default CourseOutlineTray; 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.tsx similarity index 95% rename from src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.tsx index cb026fb46b..8e51445cb1 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.test.tsx @@ -1,6 +1,7 @@ +import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; -import CompletionIcon from './CompletionIcon'; +import { CompletionIcon } from './CompletionIcon'; describe('CompletionIcon', () => { it('renders check circle icon when completion is equal to total and completion tracking is enabled', () => { diff --git a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.tsx similarity index 70% rename from src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx rename to src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.tsx index 9b3a855db8..a3fa67f5d7 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CompletionIcon.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { CheckCircle as CheckCircleIcon, LmsCompletionSolid as LmsCompletionSolidIcon, @@ -6,7 +5,15 @@ import { import { DashedCircleIcon } from '../icons'; -const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled }) => { +export interface CompletionIconProps { + completionStat: { + completed: number; + total: number; + }; + enabled: boolean; +} + +export const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled }: CompletionIconProps) => { const percentage = total !== 0 ? Math.min((completed / total) * 100, 100) : 0; const remainder = 100 - percentage; @@ -20,12 +27,4 @@ const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled } }; -CompletionIcon.propTypes = { - completionStat: PropTypes.shape({ - 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/CourseOutlineHeading.tsx b/src/courseware/course/sidebar/sidebars/course-outline/components/CourseOutlineHeading.tsx new file mode 100644 index 0000000000..e14036353d --- /dev/null +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/CourseOutlineHeading.tsx @@ -0,0 +1,51 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, IconButton } from '@openedx/paragon'; +import { + MenuOpen as MenuOpenIcon, + ChevronLeft as ChevronLeftIcon, +} from '@openedx/paragon/icons'; +import messages from '../messages'; + +export interface CourseOutlineHeadingProps { + isSequenceLevel: boolean; + title?: string; + onClickBack: () => void; + onToggleCollapse?: () => void; +} + +export const CourseOutlineHeading = ({ + isSequenceLevel, + title, + onClickBack, + onToggleCollapse, +}:CourseOutlineHeadingProps) => { + const intl = useIntl(); + return ( +
    + {isSequenceLevel && title ? ( + + ) : ( + + {intl.formatMessage(messages.courseOutlineTitle)} + + )} + {onToggleCollapse && ( + + )} +
    + ); +}; 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 246201ff37..92881ffcc2 100644 --- a/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx +++ b/src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx @@ -1,3 +1,6 @@ +import { + CourseOverviewSectionCompletionIconSlot, +} from '@src/plugin-slots/CourseOutlineSidebarSectionCompletionIconSlot'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -5,7 +8,6 @@ import { Button, Icon } from '@openedx/paragon'; import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons'; import courseOutlineMessages from '@src/course-home/outline-tab/messages'; -import CompletionIcon from './CompletionIcon'; import { useCourseOutlineData } from '../hooks'; const SidebarSection = ({ section, handleSelectSection }) => { @@ -24,7 +26,11 @@ const SidebarSection = ({ section, handleSelectSection }) => { const sectionTitle = ( <>
    - +
    {title} @@ -41,13 +47,10 @@ const SidebarSection = ({ section, handleSelectSection }) => { ); return ( -
  • +