Skip to content

Commit

Permalink
feat: legacy course navigation
Browse files Browse the repository at this point in the history
Add an option to enable the legacy course navigation where clicking a
breadcrumb leads to the course index page highlighting the selected section.
  • Loading branch information
ArturGaspar committed Nov 23, 2023
1 parent 0137868 commit f0d4fcd
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL=''
DISCUSSIONS_MFE_BASE_URL=''
ECOMMERCE_BASE_URL=''
ENABLE_JUMPNAV='true'
ENABLE_LEGACY_NAV=''
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
EXAMS_BASE_URL=''
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_LEGACY_NAV=''
ENABLE_NOTICES=''
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
EXAMS_BASE_URL='http://localhost:18740'
Expand Down
13 changes: 12 additions & 1 deletion src/course-home/outline-tab/OutlineTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ const OutlineTab = ({ intl }) => {
}
}, [location.search]);

// Remove the initial # sign.
const hashValue = location.hash.substring(1);
const selectedSectionId = courses[rootCourseId].sectionIds.find((sectionId) => (
// Section is selected or contains selected subsection.
(hashValue === sectionId) || (sections[sectionId].sequenceIds.includes(hashValue))
));

return (
<>
<div data-learner-type={learnerType} className="row w-100 mx-0 my-3 justify-content-between">
Expand Down Expand Up @@ -171,7 +178,11 @@ const OutlineTab = ({ intl }) => {
<Section
key={sectionId}
courseId={courseId}
defaultOpen={sections[sectionId].resumeBlock}
defaultOpen={
(!selectedSectionId)
? sections[sectionId].resumeBlock
: sectionId === selectedSectionId
}
expand={expandAll}
section={sections[sectionId]}
/>
Expand Down
22 changes: 22 additions & 0 deletions src/course-home/outline-tab/OutlineTab.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ describe('Outline Tab', () => {
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
});

it('expands selected section', async () => {
const { courseBlocks, sectionBlocks } = await buildMinimalCourseBlocks(courseId, 'Title');
setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
window.location.hash = `#${sectionBlocks[0].id}`;
await fetchAndRender();
const expandedSectionNode = screen.getByRole('button', { name: /Title of Section/ });
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
});

it('expands section that contains selected subsection', async () => {
const { courseBlocks, sequenceBlocks } = await buildMinimalCourseBlocks(courseId, 'Title');
setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
window.location.hash = `#${sequenceBlocks[0].id}`;
await fetchAndRender();
const expandedSectionNode = screen.getByRole('button', { name: /Title of Section/ });
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
});

it('handles expand/collapse all button click', async () => {
await fetchAndRender();
// Button renders as "Expand All"
Expand Down
8 changes: 7 additions & 1 deletion src/course-home/outline-tab/Section.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useLocation } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, IconButton } from '@edx/paragon';
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
Expand Down Expand Up @@ -29,6 +31,9 @@ const Section = ({
sequences,
},
} = useModel('outline', courseId);
// Remove the initial # sign.
const hashValue = useLocation().hash.substring(1);
const selected = hashValue === section.id;

const [open, setOpen] = useState(defaultOpen);

Expand All @@ -42,7 +47,7 @@ const Section = ({
}, []);

const sectionTitle = (
<div className="row w-100 m-0">
<div className={classNames('row w-100 m-0', { 'bg-light-200': selected })}>
<div className="col-auto p-0">
{complete ? (
<FontAwesomeIcon
Expand Down Expand Up @@ -104,6 +109,7 @@ const Section = ({
courseId={courseId}
sequence={sequences[sequenceId]}
first={index === 0}
selected={hashValue === sequenceId}
/>
))}
</ol>
Expand Down
4 changes: 3 additions & 1 deletion src/course-home/outline-tab/SequenceLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const SequenceLink = ({
courseId,
first,
sequence,
selected,
}) => {
const {
complete,
Expand Down Expand Up @@ -86,7 +87,7 @@ const SequenceLink = ({
return (
<li>
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
<div className="row w-100 m-0">
<div className={classNames('row w-100 m-0', { 'bg-light-200': selected })}>
<div className="col-auto p-0">
{complete ? (
<FontAwesomeIcon
Expand Down Expand Up @@ -130,6 +131,7 @@ SequenceLink.propTypes = {
courseId: PropTypes.string.isRequired,
first: PropTypes.bool.isRequired,
sequence: PropTypes.shape().isRequired,
selected: PropTypes.bool.isRequired,
};

export default injectIntl(SequenceLink);
15 changes: 9 additions & 6 deletions src/courseware/course/CourseBreadcrumbs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const CourseBreadcrumb = ({
content, withSeparator, courseId, sequenceId, unitId, isStaff,
}) => {
const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '', sequences: [] };
let defaultTo;
if (getConfig().ENABLE_LEGACY_NAV === 'true') {
defaultTo = `/course/${courseId}/home#${defaultContent.id}`;
} else if (defaultContent.sequences.length) {
defaultTo = `/course/${courseId}/${defaultContent.sequences[0].id}`;
} else {
defaultTo = `/course/${courseId}/${defaultContent.id}`;
}
return (
<>
{withSeparator && (
Expand All @@ -23,12 +31,7 @@ const CourseBreadcrumb = ({
<li className="reactive-crumbs">
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff
? (
<Link
className="text-primary-500"
to={defaultContent.sequences.length
? `/course/${courseId}/${defaultContent.sequences[0].id}`
: `/course/${courseId}/${defaultContent.id}`}
>
<Link className="text-primary-500" to={defaultTo}>
{defaultContent.label}
</Link>
)
Expand Down
47 changes: 35 additions & 12 deletions src/courseware/course/CourseBreadcrumbs.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,23 +106,46 @@ describe('CourseBreadcrumbs', () => {
},
],
]);
render(
<IntlProvider>
<BrowserRouter>
<CourseBreadcrumbs
courseId="course-v1:edX+DemoX+Demo_Course"
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
isStaff
/>
</BrowserRouter>,
</IntlProvider>,
);
it('renders course breadcrumbs as expected', async () => {
await render(
<IntlProvider>
<BrowserRouter>
<CourseBreadcrumbs
courseId="course-v1:edX+DemoX+Demo_Course"
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
isStaff
/>
</BrowserRouter>,
</IntlProvider>,
);
expect(screen.queryAllByRole('link')).toHaveLength(1);
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByRole('button')).toHaveLength(2);
});
it('renders legacy navigation links as expected', async () => {
getConfig.mockImplementation(() => ({
ENABLE_JUMPNAV: 'false',
ENABLE_LEGACY_NAV: 'true',
}));
await render(
<IntlProvider>
<BrowserRouter>
<CourseBreadcrumbs
courseId="course-v1:edX+DemoX+Demo_Course"
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
/>
</BrowserRouter>,
</IntlProvider>,
);
expect(screen.queryAllByRole('link')).toHaveLength(3);
const sectionButtonDestination = screen.getAllByRole('link')[1].href;
expect(sectionButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home#block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations');
const sequenceButtonDestination = screen.getAllByRole('link')[2].href;
expect(sequenceButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home#block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions');
expect(screen.queryAllByRole('button')).toHaveLength(0);
});
});
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ initialize({
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
ENABLE_LEGACY_NAV: process.env.ENABLE_LEGACY_NAV || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
Expand Down

0 comments on commit f0d4fcd

Please sign in to comment.