Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FC-0056] Course outline sidebar #1342

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a4bc421
feat: [AXIMST-572] create course-outline sidebar (#16)
ihor-romaniuk Mar 11, 2024
f8017ef
feat: [AXIMST-617] display section and sequence sidebar level (#17)
ihor-romaniuk Mar 13, 2024
d6295db
feat: [AXIMST-590] display units on sidebar, two tier layout (#18)
ihor-romaniuk Mar 14, 2024
1181db0
feat: [AXIMST-578] display outline sidebar depends on feature flag (#19)
ihor-romaniuk Mar 15, 2024
8071f6f
feat: [AXIMST-635] display discussions sidebar by waffle flag (#20)
ihor-romaniuk Mar 20, 2024
f2948ac
feat: [AXIMST-675] update outline sidebar completion on route change …
ihor-romaniuk Mar 21, 2024
bbdd0aa
feat: [AXIMST-641] display special exam label and unit lock icon (#24)
ihor-romaniuk Mar 22, 2024
4dc3ece
feat: [AXIMST-694] make sidebar fixed on scroll and add tests (#21)
ihor-romaniuk Mar 25, 2024
22199c7
fix: [AXIMST-727] hide outline sidebar with not passed entrance exam …
ihor-romaniuk Mar 29, 2024
7ca0919
fix: [AXIMST-728] check complete unit on prev and next click (#26)
ihor-romaniuk Mar 29, 2024
9e46bf1
fix: after upstream master rebase
ihor-romaniuk Apr 1, 2024
96c0e0f
fix: [AXIMST-748] update outline sidebar for locked subsection on com…
ihor-romaniuk Apr 2, 2024
32382c5
feat: [AXIMST-770] add indent for Units in the NavBar (#30)
ihor-romaniuk Apr 11, 2024
a4886c2
feat: [AXIMST-783] disable discussion sidebar by the waffle flag
ihor-romaniuk Apr 10, 2024
817d633
fix: [AXIMST-783] update sidebar logic
ihor-romaniuk Apr 11, 2024
4a4c337
fix: change endpoint for course navigation
GlugovGrGlib Apr 12, 2024
7bdcb57
fix: [AXIMST-789] update tests due to api endpoint changed (#32)
ihor-romaniuk Apr 15, 2024
52f939d
feat: [AXIMST-702] display completions for sections and subsections (…
ihor-romaniuk Apr 19, 2024
110964d
feat: [AXIMST-708] hide horizontal navigation in units (#33)
monteri Apr 22, 2024
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
15 changes: 15 additions & 0 deletions src/course-home/data/__snapshots__/redux.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ Object {
},
"courseware": Object {
"courseId": null,
"courseOutline": Object {},
"courseOutlineShouldUpdate": false,
"courseOutlineSidebarSettings": Object {},
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"rightSidebarSettings": Object {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
Expand Down Expand Up @@ -401,7 +406,12 @@ Object {
},
"courseware": Object {
"courseId": null,
"courseOutline": Object {},
"courseOutlineShouldUpdate": false,
"courseOutlineSidebarSettings": Object {},
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"rightSidebarSettings": Object {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
Expand Down Expand Up @@ -669,7 +679,12 @@ Object {
},
"courseware": Object {
"courseId": null,
"courseOutline": Object {},
"courseOutlineShouldUpdate": false,
"courseOutlineSidebarSettings": Object {},
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"rightSidebarSettings": Object {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
Expand Down
19 changes: 9 additions & 10 deletions src/course-home/outline-tab/SequenceLink.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
Expand Down Expand Up @@ -96,15 +95,15 @@ const SequenceLink = ({
icon={fasCheckCircle}
fixedWidth
className="float-left text-success mt-1"
aria-hidden="true"
aria-hidden={complete}
title={intl.formatMessage(messages.completedAssignment)}
/>
) : (
<FontAwesomeIcon
icon={farCheckCircle}
fixedWidth
className="float-left text-gray-400 mt-1"
aria-hidden="true"
aria-hidden={complete}
title={intl.formatMessage(messages.incompleteAssignment)}
/>
)}
Expand All @@ -118,14 +117,14 @@ const SequenceLink = ({
</div>
</div>
{hideFromTOC && (
<div className="row w-100 my-2 mx-4 pl-3">
<span className="small d-flex">
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
<span data-testid="hide-from-toc-sequence-link-text">
{intl.formatMessage(messages.hiddenSequenceLink)}
<div className="row w-100 my-2 mx-4 pl-3">
<span className="small d-flex">
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
<span data-testid="hide-from-toc-sequence-link-text">
{intl.formatMessage(messages.hiddenSequenceLink)}
</span>
</span>
</span>
</div>
</div>
)}
<div className="row w-100 m-0 ml-3 pl-3">
<small className="text-body pl-2">
Expand Down
14 changes: 14 additions & 0 deletions src/courseware/CoursewareContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
getSequenceForUnitDeprecated,
saveSequencePosition,
} from './data';
import {
fetchOutlineTab,
} from '../course-home/data';
import { TabPage } from '../tab-page';

import Course from './course';
Expand Down Expand Up @@ -139,6 +142,12 @@ class CoursewareContainer extends Component {
this.props.fetchCourse(courseId);
});

checkFetchOutlineData = memoize((courseId) => {
if (courseId) {
this.props.fetchOutlineTab(courseId);
}
});

checkFetchSequence = memoize((sequenceId) => {
if (sequenceId) {
this.props.fetchSequence(sequenceId);
Expand All @@ -147,12 +156,14 @@ class CoursewareContainer extends Component {

componentDidMount() {
const {
courseId,
routeCourseId,
routeSequenceId,
} = this.props;
// Load data whenever the course or sequence ID changes.
this.checkFetchCourse(routeCourseId);
this.checkFetchSequence(routeSequenceId);
this.checkFetchOutlineData(courseId);
}

componentDidUpdate() {
Expand All @@ -174,6 +185,7 @@ class CoursewareContainer extends Component {
// Load data whenever the course or sequence ID changes.
this.checkFetchCourse(routeCourseId);
this.checkFetchSequence(routeSequenceId);
this.checkFetchOutlineData(courseId);

// Check if we should save our sequence position. Only do this when the route unit ID changes.
this.checkSaveSequencePosition(routeUnitId);
Expand Down Expand Up @@ -333,6 +345,7 @@ CoursewareContainer.propTypes = {
checkBlockCompletion: PropTypes.func.isRequired,
fetchCourse: PropTypes.func.isRequired,
fetchSequence: PropTypes.func.isRequired,
fetchOutlineTab: PropTypes.func.isRequired,
navigate: PropTypes.func.isRequired,
};

Expand Down Expand Up @@ -455,4 +468,5 @@ export default connect(mapStateToProps, {
saveSequencePosition,
fetchCourse,
fetchSequence,
fetchOutlineTab,
})(withParamsAndNavigation(CoursewareContainer));
34 changes: 16 additions & 18 deletions src/courseware/course/Course.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { breakpoints, useWindowSize } from '@openedx/paragon';

import { AlertList } from '../../generic/user-messages';

import Sequence from './sequence';

import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration';
import { useModel } from '../../generic/model-store';
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
import { Trigger as CourseOutlineTrigger } from './sidebar/sidebars/course-outline';
import Chat from './chat/Chat';
import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarProvider from './sidebar/SidebarContextProvider';
import SidebarTriggers from './sidebar/SidebarTriggers';
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
import NewSidebarTriggers from './new-sidebar/SidebarTriggers';

import { useModel } from '../../generic/model-store';
import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import ContentTools from './content-tools';
import Sequence from './sequence';

const Course = ({
courseId,
Expand All @@ -37,7 +36,8 @@ const Course = ({
const sequence = useModel('sequences', sequenceId);
const section = useModel('sections', sequence ? sequence.sectionId : null);
const enableNewSidebar = getConfig().ENABLE_NEW_SIDEBAR;
const navigationDisabled = sequence?.navigationDisabled ?? false;
const { enabled: isEnabledOutlineSidebar = false } = useSelector(getCoursewareOutlineSidebarSettings);
const navigationDisabled = isEnabledOutlineSidebar || (sequence?.navigationDisabled ?? false);

const pageTitleBreadCrumbs = [
sequence,
Expand All @@ -54,7 +54,6 @@ const Course = ({
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal,
);
const shouldDisplayTriggers = windowWidth >= breakpoints.small.minWidth;
const shouldDisplayChat = windowWidth >= breakpoints.medium.minWidth;
const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek;

Expand All @@ -76,7 +75,7 @@ const Course = ({
<Helmet>
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
<div className="position-relative d-flex align-items-center mb-4 mt-1">
<div className="position-relative d-flex align-items-xl-center mb-4 mt-1 flex-column flex-xl-row">
{navigationDisabled || (
<>
<CourseBreadcrumbs
Expand All @@ -100,11 +99,10 @@ const Course = ({
/>
</>
)}
{shouldDisplayTriggers && (
<>
{enableNewSidebar === 'true' ? <NewSidebarTriggers /> : <SidebarTriggers /> }
</>
)}
<div className="w-100 d-flex align-items-center">
<CourseOutlineTrigger isMobileView />
{enableNewSidebar === 'true' ? <NewSidebarTriggers /> : <SidebarTriggers /> }
</div>
</div>

<AlertList topic="sequence" />
Expand Down
38 changes: 23 additions & 15 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Factory } from 'rosie';
import { breakpoints } from '@openedx/paragon';

import {
act, fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
fireEvent, getByRole, initializeTestStore, loadUnit, render, screen, waitFor,
} from '../../setupTest';
import * as celebrationUtils from './celebration/utils';
import { handleNextSectionCelebration } from './celebration';
Expand Down Expand Up @@ -59,7 +59,7 @@ describe('Course', () => {

it('loads learning sequence', async () => {
render(<Course {...mockData} />, { wrapWithRouter: true });
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();

expect(screen.queryByRole('alert')).not.toBeInTheDocument();
Expand Down Expand Up @@ -142,27 +142,32 @@ describe('Course', () => {

const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
expect(notificationTrigger).toBeInTheDocument();
expect(notificationTrigger.parentNode).not.toHaveClass('mt-3', { exact: true });
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
fireEvent.click(notificationTrigger);
expect(notificationTrigger.parentNode).toHaveClass('mt-3');
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
});

it('handles click to open/close discussions sidebar', async () => {
await setupDiscussionSidebar();
const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i });
const discussionsSideBar = await waitFor(() => screen.findByTestId('sidebar-DISCUSSIONS'));

expect(discussionsSideBar).not.toHaveClass('d-none');
await waitFor(() => {
expect(screen.getByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
expect(screen.getByTestId('sidebar-DISCUSSIONS')).not.toHaveClass('d-none');
});

const discussionsTrigger = await screen.getByRole('button', { name: /Show discussions tray/i });
expect(discussionsTrigger).toBeInTheDocument();
fireEvent.click(discussionsTrigger);

await act(async () => {
fireEvent.click(discussionsTrigger);
await waitFor(() => {
expect(screen.queryByTestId('sidebar-DISCUSSIONS')).not.toBeInTheDocument();
});
await expect(discussionsSideBar).toHaveClass('d-none');

await act(async () => {
fireEvent.click(discussionsTrigger);
fireEvent.click(discussionsTrigger);

await waitFor(() => {
expect(screen.queryByTestId('sidebar-DISCUSSIONS')).toBeInTheDocument();
});
await expect(discussionsSideBar).not.toHaveClass('d-none');
});

it('displays discussions sidebar when unit changes', async () => {
Expand Down Expand Up @@ -192,8 +197,9 @@ describe('Course', () => {
it('handles click to open/close notification tray', async () => {
await setupDiscussionSidebar();
const notificationShowButton = await screen.findByRole('button', { name: /Show notification tray/i });
expect(screen.queryByRole('region', { name: /notification tray/i })).toHaveClass('d-none');
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
fireEvent.click(notificationShowButton);
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toHaveClass('d-none');
});

Expand All @@ -204,7 +210,9 @@ describe('Course', () => {
{ type: 'vertical' },
{ courseId: courseMetadata.id },
));
const testStore = await initializeTestStore({ courseMetadata, unitBlocks }, false);
const testStore = await initializeTestStore({
courseMetadata, unitBlocks, outlineSidebarSettings: { enabled: false },
}, false);
const { courseware, models } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
Expand Down
2 changes: 1 addition & 1 deletion src/courseware/course/CourseBreadcrumbs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const CourseBreadcrumbs = ({
}, [courseStatus, sequenceStatus, allSequencesInSections]);

return (
<nav aria-label="breadcrumb" className="d-inline-block col-sm-10">
<nav aria-label="breadcrumb" className="d-inline-block col-sm-10 mb-3">
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
<li className="list-unstyled col-auto m-0 p-0">
<Link
Expand Down
Loading