Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';

import messages from './messages';
import Tabs from '../generic/tabs/Tabs';
import { useIntl } from '@edx/frontend-platform/i18n';
import { CourseTabLinksSlot } from '../plugin-slots/CourseTabLinksSlot';

Check failure on line 4 in src/course-tabs/CourseTabsNavigation.tsx

View workflow job for this annotation

GitHub Actions / tests

Missing file extension for "../plugin-slots/CourseTabLinksSlot"

Check failure on line 4 in src/course-tabs/CourseTabsNavigation.tsx

View workflow job for this annotation

GitHub Actions / tests

Unable to resolve path to module '../plugin-slots/CourseTabLinksSlot'

Check failure on line 4 in src/course-tabs/CourseTabsNavigation.tsx

View workflow job for this annotation

GitHub Actions / tests

'CourseTabLinksSlot' is defined but never used
import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/courseware-search';
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';

import Tabs from '../generic/tabs/Tabs';
import messages from './messages';

export interface CourseTabsNavigationProps {
activeTabSlug?: string;
tabs: Array<{
title: string;
slug: string;
url: string;
}>;
}

const CourseTabsNavigation = ({
activeTabSlug, className, tabs,
}) => {
activeTabSlug = undefined,
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 Expand Up @@ -44,19 +54,4 @@
);
};

CourseTabsNavigation.propTypes = {
activeTabSlug: PropTypes.string,
className: PropTypes.string,
tabs: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
})).isRequired,
};

CourseTabsNavigation.defaultProps = {
activeTabSlug: undefined,
className: null,
};

export default CourseTabsNavigation;
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';
Original file line number Diff line number Diff line change
@@ -1,85 +1,93 @@
import { useState } from 'react';
import classNames from 'classnames';
import { Button, useToggle, IconButton } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, IconButton, useToggle } from '@openedx/paragon';
import { LOADING } from '@src/constants';
import {
MenuOpen as MenuOpenIcon,
ChevronLeft as ChevronLeftIcon,
} from '@openedx/paragon/icons';

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 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">
<div className="outline-sidebar-heading-wrapper sticky d-flex justify-content-between align-self-start align-items-center 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"
className="outline-sidebar-heading p-0 mb-0 text-left"
onClick={handleBackToSectionLevel}
>
{backButtonTitle}
</Button>
) : (
<span className="outline-sidebar-heading mb-0 h4 text-dark-500">
<span className="outline-sidebar-heading mb-0 h4">
{intl.formatMessage(messages.courseOutlineTitle)}
</span>
)}
{onToggleCollapse && (
<IconButton
alt={intl.formatMessage(messages.toggleCourseOutlineTrigger)}
className="outline-sidebar-toggle-btn flex-shrink-0 text-dark bg-light-200"
className="outline-sidebar-toggle-btn flex-shrink-0 text-dark"
iconAs={MenuOpenIcon}
onClick={handleToggleCollapse}
onClick={onToggleCollapse}
/>
)}
</div>
);

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

if (courseOutlineStatus === LOADING) {
return (
<div className={classNames('outline-sidebar-wrapper', {
Expand Down Expand Up @@ -107,19 +115,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 +136,3 @@ const CourseOutlineTray = () => {
</div>
);
};

CourseOutlineTray.ID = ID;

export default CourseOutlineTray;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
}

.outline-sidebar-heading-wrapper {
border: 1px solid #d7d3d1;
border-width: var(--learning-sidebar-outline-heading-wrapper-border-width, 1px);
border-style: solid;
border-color: var(--learning-sidebar-outline-heading-wrapper-border-color, var(--pgn-color-light-700));
background-color: var(--learning-sidebar-outline-heading-wrapper-bg-color, var(--pgn-color-light-200));

&.sticky {
position: sticky;
Expand All @@ -24,12 +27,18 @@

.outline-sidebar-heading {
font-weight: var(--pgn-typography-font-weight-bold);
color: var(--learning-sidebar-outline-heading-text-color, var(--pgn-color-dark-500));
}
}

.course-sidebar-section {
background: var(--pgn-color-white);
border: 1px solid #d7d3d1;
background: var(--learning-sidebar-outline-section-bg-color, var(--pgn-color-white));

border-width: var(--learning-sidebar-outline-section-border-width, 1px);
border-style: solid;
border-color: var(--learning-sidebar-outline-section-border-color, var(--pgn-color-light-700));

margin-bottom: var(--learning-sidebar-outline-section-margin-bottom, var(--pgn-spacing-spacer-2));

button {
line-height: 1.75rem;
Expand All @@ -38,11 +47,17 @@
&:focus::before {
border-radius: 0;
}

padding: var(--learning-sidebar-outline-section-padding-y, var(--pgn-spacing-spacer-3-5)) var(--learning-sidebar-outline-section-padding-x, var(--pgn-spacing-spacer-4));
}
&.active-section button {
background-color: var(--learning-sidebar-outline-section-active-bg-color, var(--pgn-color-light-100));
}
}

.outline-sidebar-toggle-btn {
font-size: 1.5rem;
background-color: var(--learning-sidebar-outline-toggle-btn-bg-color, var(--pgn-color-light-200));

.collapsed & {
transform: scale(-1, 1);
Expand Down Expand Up @@ -78,7 +93,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,6 @@
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