This reverts commit dd6a499cfc.
This commit is contained in:
@@ -90,7 +90,6 @@ function Course({
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
//* * [MM-P2P] Experiment */
|
||||
mmp2p={MMP2P}
|
||||
/>
|
||||
|
||||
@@ -5,18 +5,32 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { SelectMenu } from '@edx/paragon';
|
||||
import { Hyperlink, MenuItem, SelectMenu } from '@edx/paragon';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
sendTrackingLogEvent,
|
||||
sendTrackEvent,
|
||||
} from '@edx/frontend-platform/analytics';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
/** [MM-P2P] Experiment */
|
||||
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
|
||||
import ConnectedJumpNavMenuItem from './JumpNavMenuItem';
|
||||
|
||||
function CourseBreadcrumb({
|
||||
content, withSeparator, courseId, unitId,
|
||||
content, withSeparator,
|
||||
}) {
|
||||
const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '' };
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
const defaultContent = content.filter(destination => destination.default)[0];
|
||||
const administrator = getAuthenticatedUser() ? getAuthenticatedUser().administrator : false;
|
||||
function logEvent(target) {
|
||||
const eventName = 'edx.ui.lms.jump_nav.selected';
|
||||
const payload = {
|
||||
target_name: target.label,
|
||||
id: target.id,
|
||||
current_id: defaultContent.id,
|
||||
widget_placement: 'breadcrumb',
|
||||
};
|
||||
sendTrackEvent(eventName, payload);
|
||||
sendTrackingLogEvent(eventName, payload);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -32,20 +46,20 @@ function CourseBreadcrumb({
|
||||
>
|
||||
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !administrator
|
||||
? (
|
||||
<a className="text-primary-500" href={`/course/${courseId}/${defaultContent.id}`}>
|
||||
{defaultContent.label}
|
||||
<a className="text-primary-500" href={defaultContent.url}>{defaultContent.label}
|
||||
</a>
|
||||
)
|
||||
: (
|
||||
<SelectMenu isLink defaultMessage={defaultContent.label}>
|
||||
{content.map(item => (
|
||||
<ConnectedJumpNavMenuItem
|
||||
isDefault={item.default}
|
||||
sequences={item.sequences}
|
||||
courseId={courseId}
|
||||
title={item.label}
|
||||
currentUnit={unitId}
|
||||
/>
|
||||
<MenuItem
|
||||
as={Hyperlink}
|
||||
defaultSelected={item.default}
|
||||
destination={item.url}
|
||||
onClick={logEvent(item)}
|
||||
>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SelectMenu>
|
||||
)}
|
||||
@@ -58,71 +72,58 @@ CourseBreadcrumb.propTypes = {
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
default: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
unitId: PropTypes.string,
|
||||
withSeparator: PropTypes.bool,
|
||||
courseId: PropTypes.string,
|
||||
};
|
||||
|
||||
CourseBreadcrumb.defaultProps = {
|
||||
withSeparator: false,
|
||||
unitId: null,
|
||||
courseId: null,
|
||||
};
|
||||
|
||||
export default function CourseBreadcrumbs({
|
||||
courseId,
|
||||
sectionId,
|
||||
sequenceId,
|
||||
unitId,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p,
|
||||
}) {
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
||||
const sections = course ? Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, section])) : null;
|
||||
const possibleSequences = sections && sectionId ? sections[sectionId].sequenceIds : [];
|
||||
const sequences = Object.fromEntries(useModels('sequences', possibleSequences).map(sequence => [sequence.id, sequence]));
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
|
||||
const allSequencesInSections = Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, {
|
||||
default: section.id === sectionId,
|
||||
title: section.title,
|
||||
sequences: useModels('sequences', section.sequenceIds),
|
||||
}]));
|
||||
|
||||
const links = useMemo(() => {
|
||||
const chapters = [];
|
||||
const sequentials = [];
|
||||
const temp = [];
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'loaded') {
|
||||
Object.entries(allSequencesInSections).forEach(([id, section]) => {
|
||||
chapters.push({
|
||||
id,
|
||||
label: section.title,
|
||||
default: section.default,
|
||||
sequences: section.sequences,
|
||||
});
|
||||
if (section.default) {
|
||||
section.sequences.forEach(sequence => {
|
||||
sequentials.push({
|
||||
id: sequence.id,
|
||||
label: sequence.title,
|
||||
default: sequence.id === sequenceId,
|
||||
sequences: [sequence],
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
temp.push(course.sectionIds.map(id => ({
|
||||
id,
|
||||
label: sections[id].title,
|
||||
default: (id === sectionId),
|
||||
// navigate to first sequence in section, (TODO: navigate to first incomplete sequence in section)
|
||||
url: `${getConfig().BASE_URL}/course/${courseId}/${sections[id].sequenceIds[0]}`,
|
||||
})));
|
||||
temp.push(sections[sectionId].sequenceIds.map(id => ({
|
||||
id,
|
||||
label: sequences[id].title,
|
||||
default: id === sequenceId,
|
||||
// first unit it section (TODO: navigate to first incomplete in sequence)
|
||||
url: `${getConfig().BASE_URL}/course/${courseId}/${sequences[id].id}/${sequences[id].unitIds[0]}`,
|
||||
})));
|
||||
}
|
||||
return [chapters, sequentials];
|
||||
}, [courseStatus, sequenceStatus, allSequencesInSections]);
|
||||
|
||||
return temp;
|
||||
}, [courseStatus, sections, sequences]);
|
||||
return (
|
||||
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
|
||||
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
|
||||
<li className="list-unstyled d-flex m-0">
|
||||
<ol className="list-unstyled d-flex align-items-center m-0">
|
||||
<li>
|
||||
<a
|
||||
href={`/courses/${courseId}/course/`}
|
||||
href={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course/`}
|
||||
className="flex-shrink-0 text-primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faHome} className="mr-2" />
|
||||
@@ -135,10 +136,7 @@ export default function CourseBreadcrumbs({
|
||||
</li>
|
||||
{links.map(content => (
|
||||
<CourseBreadcrumb
|
||||
courseId={courseId}
|
||||
sequenceId={sequenceId}
|
||||
content={content}
|
||||
unitId={unitId}
|
||||
withSeparator
|
||||
/>
|
||||
))}
|
||||
@@ -155,7 +153,6 @@ CourseBreadcrumbs.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
sectionId: PropTypes.string,
|
||||
sequenceId: PropTypes.string,
|
||||
unitId: PropTypes.string,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: PropTypes.shape({
|
||||
state: PropTypes.shape({
|
||||
@@ -167,7 +164,6 @@ CourseBreadcrumbs.propTypes = {
|
||||
CourseBreadcrumbs.defaultProps = {
|
||||
sectionId: null,
|
||||
sequenceId: null,
|
||||
unitId: null,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: {},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { screen, render, fireEvent } from '@testing-library/react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('react-redux');
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
// Remove When Fully rolled out>>>
|
||||
@@ -15,16 +17,7 @@ getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
|
||||
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
|
||||
// ^^^^Remove When Fully rolled out
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
ReactComponent,
|
||||
}),
|
||||
Provider: ({ children }) => children,
|
||||
useSelector: () => 'loaded',
|
||||
}));
|
||||
|
||||
useSelector.mockImplementation(() => 'loaded');
|
||||
useModels.mockImplementation((name) => {
|
||||
if (name === 'sections') {
|
||||
return [
|
||||
@@ -111,11 +104,14 @@ describe('CourseBreadcrumbs', () => {
|
||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||
/>,
|
||||
);
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/courses/course-v1:edX+DemoX+Demo_Course/course/');
|
||||
it('renders course breadcrumbs as expected, handles clicks', async () => {
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
expect(screen.queryAllByRole('button')).toHaveLength(2);
|
||||
const sectionButton = screen.getByText('Example Week 1: Getting Started');
|
||||
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||
fireEvent.click(sectionButton);
|
||||
expect(screen.queryAllByRole('link')).toHaveLength(2);
|
||||
const menuItem = screen.queryAllByRole('link')[0];
|
||||
fireEvent.click(menuItem);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/* eslint-disable consistent-return */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { Button, MenuItem } from '@edx/paragon';
|
||||
|
||||
import {
|
||||
sendTrackingLogEvent,
|
||||
sendTrackEvent,
|
||||
} from '@edx/frontend-platform/analytics';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { checkBlockCompletion } from '../data';
|
||||
|
||||
export function JumpNavMenuItem({
|
||||
title,
|
||||
courseId,
|
||||
currentUnit,
|
||||
sequences,
|
||||
isDefault,
|
||||
actions,
|
||||
|
||||
}) {
|
||||
function logEvent(targetUrl) {
|
||||
const eventName = 'edx.ui.lms.jump_nav.selected';
|
||||
const payload = {
|
||||
target_name: title,
|
||||
id: targetUrl,
|
||||
current_id: courseId,
|
||||
widget_placement: 'breadcrumb',
|
||||
};
|
||||
sendTrackEvent(eventName, payload);
|
||||
sendTrackingLogEvent(eventName, payload);
|
||||
}
|
||||
|
||||
function lazyloadUrl() {
|
||||
if (isDefault) {
|
||||
return `/course/${courseId}/${currentUnit}`;
|
||||
}
|
||||
const destinationString = sequences.forEach(sequence => sequence.unitIds.forEach(unitId => {
|
||||
const complete = actions.checkBlockCompletion(
|
||||
courseId,
|
||||
sequence.id, unitId,
|
||||
)
|
||||
.then(value => value);
|
||||
if (!complete) { return `/course/${courseId}/${unitId}`; }
|
||||
}));
|
||||
return destinationString || `/course/${courseId}/${sequences[0].unitIds[0]}`;
|
||||
}
|
||||
function handleClick() {
|
||||
const url = lazyloadUrl();
|
||||
logEvent(url);
|
||||
history.push(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
as={Button}
|
||||
defaultSelected={isDefault}
|
||||
onClick={() => handleClick()}
|
||||
>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
const sequenceShape = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
sectionId: PropTypes.string.isRequired,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
isProctored: PropTypes.bool,
|
||||
legacyWebUrl: PropTypes.string,
|
||||
});
|
||||
|
||||
JumpNavMenuItem.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
sequences: PropTypes.arrayOf(sequenceShape).isRequired,
|
||||
isDefault: PropTypes.bool.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
currentUnit: PropTypes.string.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
checkBlockCompletion: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({ checkBlockCompletion }, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null,
|
||||
mapDispatchToProps)(JumpNavMenuItem);
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { JumpNavMenuItem } from './JumpNavMenuItem';
|
||||
import { fireEvent } from '../../setupTest';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
ReactComponent,
|
||||
}),
|
||||
Provider: ({ children }) => children,
|
||||
useSelector: () => 'loaded',
|
||||
}));
|
||||
|
||||
const mockCheckBlock = jest.fn(() => Promise.resolve(true)); // check all units
|
||||
|
||||
const mockData = {
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
|
||||
title: 'Demo Menu Item',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
currentUnit: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
sequences: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
blockType: 'sequential',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
],
|
||||
legacyWebUrl: 'http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5?experience=legacy',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
title: 'Homework - Question Styles',
|
||||
legacyWebUrl: 'http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions?experience=legacy',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
|
||||
],
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
},
|
||||
],
|
||||
isDefault: false,
|
||||
actions: {
|
||||
checkBlockCompletion: mockCheckBlock,
|
||||
},
|
||||
};
|
||||
describe('JumpNavMenuItem', () => {
|
||||
render(
|
||||
<JumpNavMenuItem
|
||||
{...mockData}
|
||||
/>,
|
||||
);
|
||||
it('renders menu Item as expected with button and Text and handles clicks', () => {
|
||||
expect(screen.queryAllByRole('button')).toHaveLength(1);
|
||||
expect(screen.getByText('Demo Menu Item'));
|
||||
const navButton = screen.queryAllByRole('button')[0];
|
||||
fireEvent.click(navButton);
|
||||
expect(mockCheckBlock).toBeCalledTimes(14); // number of units to check on load.
|
||||
});
|
||||
});
|
||||
@@ -247,7 +247,7 @@ export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
||||
return async (dispatch, getState) => {
|
||||
const { models } = getState();
|
||||
if (models.units[unitId].complete) {
|
||||
return {}; // do nothing. Things don't get uncompleted after they are completed.
|
||||
return; // do nothing. Things don't get uncompleted after they are completed.
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -259,11 +259,9 @@ export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
||||
complete: isComplete,
|
||||
},
|
||||
}));
|
||||
return isComplete;
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user