Skip to content
Open
15 changes: 6 additions & 9 deletions src/course-tabs/CourseTabsNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React from 'react';
import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';
import { CourseTabLinksSlot } from '../plugin-slots/CourseTabLinksSlot';
import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/courseware-search';
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';

import Tabs from '../generic/tabs/Tabs';
import { CoursewareSearch, CoursewareSearchToggle } from '@src/course-home/courseware-search';
import { useCoursewareSearchState } from '@src/course-home/courseware-search/hooks';
import { CourseTabLinksSlot } from '@src/plugin-slots/CourseTabLinksSlot';
import Tabs from '@src/generic/tabs/Tabs';
import messages from './messages';

interface CourseTabsNavigationProps {
export interface CourseTabsNavigationProps {
activeTabSlug?: string;
className?: string | null;
tabs: Array<{
title: string;
slug: string;
Expand All @@ -20,14 +18,13 @@ interface CourseTabsNavigationProps {

const CourseTabsNavigation = ({
activeTabSlug = undefined,
className = null,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in one place and it doesn't send this param, so it can be removed.

tabs,
}:CourseTabsNavigationProps) => {
const intl = useIntl();
const { show } = useCoursewareSearchState();

return (
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
<div id="courseTabsNavigation" className="mb-3 course-tabs-navigation">
<div className="container-xl">
<div className="nav-bar">
<div className="nav-menu">
Expand Down
2 changes: 1 addition & 1 deletion src/course-tabs/index.js → src/course-tabs/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as CourseTabsNavigation } from './CourseTabsNavigation';
export type { CourseTabsNavigationProps } from './CourseTabsNavigation';
2 changes: 1 addition & 1 deletion src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe('Course', () => {

await setupDiscussionSidebar();

const { rerender } = render(<Course {...testData} />, { store: testStore });
const { rerender } = render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
loadUnit();

const sidebar = await screen.findByTestId('sidebar-DISCUSSIONS');
Expand Down
18 changes: 17 additions & 1 deletion src/courseware/course/sequence/Sequence.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import SequenceBottomNavigationSlot from '@src/plugin-slots/SequenceBottomNavigationSlot';
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';

Expand Down Expand Up @@ -224,7 +225,22 @@ const Sequence = ({
isOriginalUserStaff={originalUserIsStaff}
renderUnitNavigation={renderUnitNavigation}
/>
{unitHasLoaded && renderUnitNavigation(false)}
{unitHasLoaded && (
<SequenceBottomNavigationSlot
courseId={courseId}
sequenceId={sequenceId}
unitId={unitId}
previousHandler={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
handlePrevious();
}}
nextHandler={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
handleNext();
}}
onNavigate={onNavigate}
/>
)}
</div>
</div>
<RightSidebarSlot courseId={courseId} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';

import { NextUnitTopNavTriggerSlot } from '@src/plugin-slots/NextUnitTopNavTriggerSlot';
import { GetCourseExitNavigation } from '../../course-exit';

import { useSequenceNavigationMetadata } from './hooks';
import messages from './messages';
import PreviousButton from './generic/PreviousButton';
import NextButton from './generic/NextButton';
import { NextUnitTopNavTriggerSlot } from '../../../../plugin-slots/NextUnitTopNavTriggerSlot';

const UnitNavigation = ({
sequenceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,85 +1,73 @@
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 SidebarSection from './components/SidebarSection';
import { CourseOutlineSidebarHeadingSlot } from '@src/plugin-slots/CourseOutlineSidebarHeadingSlot';
import classNames from 'classnames';
import { useState } from 'react';
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';

const CourseOutlineTray = () => {
interface CourseOutlineProps {
shouldDisplayFullScreen?: boolean;
onToggleCollapse?: () => void;
}

interface CoursePageParams extends Record<string, string> {
courseId: string;
unitId: string;
}

export const CourseOutline = ({
shouldDisplayFullScreen = false,
onToggleCollapse,
}: CourseOutlineProps) => {
const intl = useIntl();
const [selectedSection, setSelectedSection] = useState(null);
const [selectedSection, setSelectedSection] = useState<string | null>(null);
const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true);

const { unitId, courseId } = useParams<CoursePageParams>();
const {
courseId,
unitId,
currentSidebar,
handleToggleCollapse,
isActiveEntranceExam,
shouldDisplayFullScreen,
courseOutlineStatus,
activeSequenceId,
sections,
sequences,
} = useCourseOutlineSidebar();
isActiveEntranceExam,
} = 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 = (
<div className="outline-sidebar-heading-wrapper sticky d-flex justify-content-between align-self-start align-items-center bg-light-200 p-2.5 pl-4">
{isDisplaySequenceLevel && backButtonTitle ? (
<Button
variant="link"
iconBefore={ChevronLeftIcon}
className="outline-sidebar-heading p-0 mb-0 text-left text-dark-500"
onClick={handleBackToSectionLevel}
>
{backButtonTitle}
</Button>
) : (
<span className="outline-sidebar-heading mb-0 h4 text-dark-500">
{intl.formatMessage(messages.courseOutlineTitle)}
</span>
)}
<IconButton
alt={intl.formatMessage(messages.toggleCourseOutlineTrigger)}
className="outline-sidebar-toggle-btn flex-shrink-0 text-dark bg-light-200"
iconAs={MenuOpenIcon}
onClick={handleToggleCollapse}
/>
</div>
<CourseOutlineSidebarHeadingSlot
onToggleCollapse={onToggleCollapse}
isSequenceLevel={isDisplaySequenceLevel}
title={backButtonTitle}
onClickBack={handleBackToSectionLevel}
/>
);

if (isActiveEntranceExam || currentSidebar !== ID) {
if (isActiveEntranceExam) {
return null;
}

if (courseOutlineStatus === LOADING) {
return (
<div className={classNames('outline-sidebar-wrapper', {
Expand Down Expand Up @@ -107,19 +95,18 @@ const CourseOutlineTray = () => {
{sidebarHeading}
<ol id="outline-sidebar-outline" className="list-unstyled">
{isDisplaySequenceLevel
? sequenceIds.map((sequenceId) => (
? sequenceIds.map((sequenceId: string) => (
<SidebarSequence
key={sequenceId}
courseId={courseId}
courseId={courseId!}
sequence={sequences[sequenceId]}
defaultOpen={sequenceId === activeSequenceId}
activeUnitId={unitId}
activeUnitId={unitId!}
/>
))
: sectionsIds.map((sectionId) => (
<SidebarSection
key={sectionId}
courseId={courseId}
section={sections[sectionId]}
handleSelectSection={handleSelectSection}
/>
Expand All @@ -129,7 +116,3 @@ const CourseOutlineTray = () => {
</div>
);
};

CourseOutlineTray.ID = ID;

export default CourseOutlineTray;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brian-smith-tcril Here is the PR I mentioned during the discussion on the conference talk. There may still be changes based on how well it's working out.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
}

.outline-sidebar-heading-wrapper {
border: 1px solid #d7d3d1;
border: 1px solid var(--pgn-color-light-700);

&.sticky {
position: sticky;
Expand All @@ -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;
Expand Down Expand Up @@ -78,7 +78,7 @@
}

&:last-child .pgn_collapsible {
margin-bottom: 0px !important;
margin-bottom: 0 !important;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('<CourseOutlineTray />', () => {
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();
});

Expand Down Expand Up @@ -115,13 +115,13 @@ describe('<CourseOutlineTray />', () => {

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();
});
});
Original file line number Diff line number Diff line change
@@ -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 <CourseOutline shouldDisplayFullScreen={shouldDisplayFullScreen} onToggleCollapse={handleToggleCollapse} />;
};

CourseOutlineTray.ID = ID;

export default CourseOutlineTray;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import PropTypes from 'prop-types';
import {
CheckCircle as CheckCircleIcon,
LmsCompletionSolid as LmsCompletionSolidIcon,
} from '@openedx/paragon/icons';

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;

Expand All @@ -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;
Loading
Loading