Revert "Feat lazy load jump nav destinations (#663)" (#670)

This reverts commit dd6a499cfc.
This commit is contained in:
connorhaugh
2021-10-05 16:55:58 -04:00
committed by GitHub
parent 20390d1e33
commit f57f39a787
6 changed files with 64 additions and 247 deletions

View File

@@ -90,7 +90,6 @@ function Course({
courseId={courseId}
sectionId={section ? section.id : null}
sequenceId={sequenceId}
unitId={unitId}
//* * [MM-P2P] Experiment */
mmp2p={MMP2P}
/>

View File

@@ -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: {},
};

View File

@@ -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);
});
});

View File

@@ -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);

View File

@@ -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.
});
});

View File

@@ -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 {};
};
}