Skip to content

Commit 3cbbb02

Browse files
authored
fix: update outline sidebar hooks for plugins (#1586)
* fix: update outline sidebar hooks for plugins * docs: explain UnitLinkWrapper
1 parent 911c765 commit 3cbbb02

File tree

9 files changed

+172
-101
lines changed

9 files changed

+172
-101
lines changed

src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx

+6-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState } from 'react';
22
import classNames from 'classnames';
3-
import { useDispatch, useSelector } from 'react-redux';
43
import { Button, useToggle, IconButton } from '@openedx/paragon';
54
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
65
import {
@@ -9,15 +8,8 @@ import {
98
} from '@openedx/paragon/icons';
109

1110
import { useModel } from '@src/generic/model-store';
12-
import { LOADING, LOADED } from '@src/constants';
11+
import { LOADING } from '@src/constants';
1312
import PageLoading from '@src/generic/PageLoading';
14-
import {
15-
getSequenceId,
16-
getCourseOutline,
17-
getCourseOutlineStatus,
18-
getCourseOutlineShouldUpdate,
19-
} from '../../../../data/selectors';
20-
import { getCourseOutlineStructure } from '../../../../data/thunks';
2113
import SidebarSection from './components/SidebarSection';
2214
import SidebarSequence from './components/SidebarSequence';
2315
import { ID } from './constants';
@@ -28,12 +20,6 @@ const CourseOutlineTray = ({ intl }) => {
2820
const [selectedSection, setSelectedSection] = useState(null);
2921
const [isDisplaySequenceLevel, setDisplaySequenceLevel, setDisplaySectionLevel] = useToggle(true);
3022

31-
const dispatch = useDispatch();
32-
const activeSequenceId = useSelector(getSequenceId);
33-
const { sections = {}, sequences = {} } = useSelector(getCourseOutline);
34-
const courseOutlineStatus = useSelector(getCourseOutlineStatus);
35-
const courseOutlineShouldUpdate = useSelector(getCourseOutlineShouldUpdate);
36-
3723
const {
3824
courseId,
3925
unitId,
@@ -42,6 +28,10 @@ const CourseOutlineTray = ({ intl }) => {
4228
handleToggleCollapse,
4329
isActiveEntranceExam,
4430
shouldDisplayFullScreen,
31+
courseOutlineStatus,
32+
activeSequenceId,
33+
sections,
34+
sequences,
4535
} = useCourseOutlineSidebar();
4636

4737
const {
@@ -87,12 +77,6 @@ const CourseOutlineTray = ({ intl }) => {
8777
</div>
8878
);
8979

90-
useEffect(() => {
91-
if ((isEnabledSidebar && courseOutlineStatus !== LOADED) || courseOutlineShouldUpdate) {
92-
dispatch(getCourseOutlineStructure(courseId));
93-
}
94-
}, [courseId, isEnabledSidebar, courseOutlineShouldUpdate]);
95-
9680
if (!isEnabledSidebar || isActiveEntranceExam || currentSidebar !== ID) {
9781
return null;
9882
}

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import PropTypes from 'prop-types';
22
import classNames from 'classnames';
3-
import { useSelector } from 'react-redux';
43
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
54
import { Button, Icon } from '@openedx/paragon';
65
import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons';
76

87
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
9-
import { getSequenceId } from '@src/courseware/data/selectors';
108
import CompletionIcon from './CompletionIcon';
9+
import { useCourseOutlineSidebar } from '../hooks';
1110

1211
const SidebarSection = ({ intl, section, handleSelectSection }) => {
1312
const {
@@ -18,7 +17,7 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
1817
completionStat,
1918
} = section;
2019

21-
const activeSequenceId = useSelector(getSequenceId);
20+
const { activeSequenceId } = useCourseOutlineSidebar();
2221
const isActiveSection = sequenceIds.includes(activeSequenceId);
2322

2423
const sectionTitle = (

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.test.jsx

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { useMemo } from 'react';
12
import { render } from '@testing-library/react';
23
import userEvent from '@testing-library/user-event';
34
import { IntlProvider } from '@edx/frontend-platform/i18n';
45
import { AppProvider } from '@edx/frontend-platform/react';
56

67
import { initializeTestStore } from '@src/setupTest';
78
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
9+
import SidebarContext from '../../../SidebarContext';
810
import SidebarSection from './SidebarSection';
911

1012
describe('<SidebarSection />', () => {
@@ -19,17 +21,23 @@ describe('<SidebarSection />', () => {
1921
section = state.courseware.courseOutline.sections[activeSectionId];
2022
};
2123

22-
const RootWrapper = (props) => (
23-
<AppProvider store={store} wrapWithRouter={false}>
24-
<IntlProvider locale="en">
25-
<SidebarSection
26-
section={section}
27-
handleSelectSection={mockHandleSelectSection}
28-
{...props}
29-
/>,
30-
</IntlProvider>
31-
</AppProvider>
32-
);
24+
const RootWrapper = (props) => {
25+
const mockData = useMemo(() => ({ toggleSidebar: jest.fn() }), []);
26+
27+
return (
28+
<AppProvider store={store} wrapWithRouter={false}>
29+
<IntlProvider locale="en">
30+
<SidebarContext.Provider value={mockData}>
31+
<SidebarSection
32+
section={section}
33+
handleSelectSection={mockHandleSelectSection}
34+
{...props}
35+
/>
36+
</SidebarContext.Provider>
37+
</IntlProvider>
38+
</AppProvider>
39+
);
40+
};
3341

3442
beforeEach(() => {
3543
mockHandleSelectSection = jest.fn();

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.jsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { useState } from 'react';
2-
import { useSelector } from 'react-redux';
32
import classNames from 'classnames';
43
import PropTypes from 'prop-types';
54
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
65
import { Collapsible } from '@openedx/paragon';
76

87
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
9-
import { getCourseOutline, getSequenceId } from '@src/courseware/data/selectors';
8+
import { useCourseOutlineSidebar } from '../hooks';
109
import CompletionIcon from './CompletionIcon';
1110
import SidebarUnit from './SidebarUnit';
1211
import { UNIT_ICON_TYPES } from './UnitIcon';
@@ -29,8 +28,7 @@ const SidebarSequence = ({
2928
} = sequence;
3029

3130
const [open, setOpen] = useState(defaultOpen);
32-
const { units = {} } = useSelector(getCourseOutline);
33-
const activeSequenceId = useSelector(getSequenceId);
31+
const { activeSequenceId, units } = useCourseOutlineSidebar();
3432
const isActiveSequence = id === activeSequenceId;
3533

3634
const sectionTitle = (

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSequence.test.jsx

+17-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
66

77
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
88
import { initializeMockApp, initializeTestStore } from '@src/setupTest';
9+
import SidebarContext from '../../../SidebarContext';
910
import messages from '../messages';
1011
import SidebarSequence from './SidebarSequence';
1112

@@ -17,6 +18,7 @@ describe('<SidebarSequence />', () => {
1718
let sequence;
1819
let unit;
1920
const sequenceDescription = 'sequence test description';
21+
let mockData;
2022

2123
const initTestStore = async (options) => {
2224
store = await initializeTestStore(options);
@@ -27,21 +29,27 @@ describe('<SidebarSequence />', () => {
2729
sequence = state.courseware.courseOutline.sequences[activeSequenceId];
2830
const unitId = sequence.unitIds[0];
2931
unit = state.courseware.courseOutline.units[unitId];
32+
33+
mockData = {
34+
toggleSidebar: jest.fn(),
35+
};
3036
};
3137

3238
function renderWithProvider(props = {}) {
3339
const { container } = render(
3440
<AppProvider store={store} wrapWithRouter={false}>
3541
<IntlProvider locale="en">
36-
<MemoryRouter>
37-
<SidebarSequence
38-
courseId={courseId}
39-
defaultOpen={false}
40-
sequence={sequence}
41-
activeUnitId={sequence.unitIds[0]}
42-
{...props}
43-
/>
44-
</MemoryRouter>
42+
<SidebarContext.Provider value={{ ...mockData }}>
43+
<MemoryRouter>
44+
<SidebarSequence
45+
courseId={courseId}
46+
defaultOpen={false}
47+
sequence={sequence}
48+
activeUnitId={sequence.unitIds[0]}
49+
{...props}
50+
/>
51+
</MemoryRouter>
52+
</SidebarContext.Provider>
4553
</IntlProvider>
4654
</AppProvider>,
4755
);

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.jsx

+9-38
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import PropTypes from 'prop-types';
22
import classNames from 'classnames';
3-
import { Link } from 'react-router-dom';
4-
import { useDispatch, useSelector } from 'react-redux';
53
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
6-
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
74

8-
import { checkBlockCompletion } from '@src/courseware/data';
9-
import { getCourseOutline } from '@src/courseware/data/selectors';
105
import messages from '../messages';
116
import UnitIcon, { UNIT_ICON_TYPES } from './UnitIcon';
7+
import UnitLinkWrapper from './UnitLinkWrapper';
128

139
const SidebarUnit = ({
1410
id,
@@ -26,43 +22,18 @@ const SidebarUnit = ({
2622
title,
2723
icon = UNIT_ICON_TYPES.other,
2824
} = unit;
29-
const dispatch = useDispatch();
30-
const { sequences = {} } = useSelector(getCourseOutline);
31-
32-
const logEvent = (eventName, widgetPlacement) => {
33-
const findSequenceByUnitId = (unitId) => Object.values(sequences).find(seq => seq.unitIds.includes(unitId));
34-
const activeSequence = findSequenceByUnitId(activeUnitId);
35-
const targetSequence = findSequenceByUnitId(id);
36-
const payload = {
37-
id: activeUnitId,
38-
current_tab: activeSequence.unitIds.indexOf(activeUnitId) + 1,
39-
tab_count: activeSequence.unitIds.length,
40-
target_id: id,
41-
target_tab: targetSequence.unitIds.indexOf(id) + 1,
42-
widget_placement: widgetPlacement,
43-
};
44-
45-
if (activeSequence.id !== targetSequence.id) {
46-
payload.target_tab_count = targetSequence.unitIds.length;
47-
}
48-
49-
sendTrackEvent(eventName, payload);
50-
sendTrackingLogEvent(eventName, payload);
51-
};
52-
53-
const handleClick = () => {
54-
logEvent('edx.ui.lms.sequence.tab_selected', 'left');
55-
dispatch(checkBlockCompletion(courseId, sequenceId, activeUnitId));
56-
};
5725

5826
const iconType = isLocked ? UNIT_ICON_TYPES.lock : icon;
5927

6028
return (
6129
<li className={classNames({ 'bg-info-100': isActive, 'border-top border-light': !isFirst })}>
62-
<Link
63-
to={`/course/${courseId}/${sequenceId}/${id}`}
64-
className="row w-100 m-0 d-flex align-items-center text-gray-700"
65-
onClick={handleClick}
30+
<UnitLinkWrapper
31+
{...{
32+
sequenceId,
33+
activeUnitId,
34+
id,
35+
courseId,
36+
}}
6637
>
6738
<div className="col-auto p-0">
6839
<UnitIcon type={iconType} isCompleted={complete} />
@@ -75,7 +46,7 @@ const SidebarUnit = ({
7546
, {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
7647
</span>
7748
</div>
78-
</Link>
49+
</UnitLinkWrapper>
7950
</li>
8051
);
8152
};

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarUnit.test.jsx

+20-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
66
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
77

88
import { initializeMockApp, initializeTestStore } from '@src/setupTest';
9+
import SidebarContext from '../../../SidebarContext';
910
import SidebarUnit from './SidebarUnit';
1011

1112
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -19,31 +20,38 @@ describe('<SidebarUnit />', () => {
1920
let store = {};
2021
let unit;
2122
let sequenceId;
23+
let mockData;
2224

2325
const initTestStore = async (options) => {
2426
store = await initializeTestStore(options);
2527
const state = store.getState();
2628
[sequenceId] = Object.keys(state.courseware.courseOutline.sequences);
2729
const sequence = state.courseware.courseOutline.sequences[sequenceId];
2830
unit = state.courseware.courseOutline.units[sequence.unitIds[0]];
31+
32+
mockData = {
33+
toggleSidebar: jest.fn(),
34+
};
2935
};
3036

3137
function renderWithProvider(props = {}) {
3238
const { container } = render(
3339
<AppProvider store={store} wrapWithRouter={false}>
3440
<IntlProvider locale="en">
35-
<MemoryRouter>
36-
<SidebarUnit
37-
isFirst
38-
id={unit.id}
39-
courseId="course123"
40-
sequenceId={sequenceId}
41-
unit={{ ...unit, icon: 'video', isLocked: false }}
42-
isActive={false}
43-
activeUnitId={unit.id}
44-
{...props}
45-
/>
46-
</MemoryRouter>
41+
<SidebarContext.Provider value={{ ...mockData }}>
42+
<MemoryRouter>
43+
<SidebarUnit
44+
isFirst
45+
id={unit.id}
46+
courseId="course123"
47+
sequenceId={sequenceId}
48+
unit={{ ...unit, icon: 'video', isLocked: false }}
49+
isActive={false}
50+
activeUnitId={unit.id}
51+
{...props}
52+
/>
53+
</MemoryRouter>
54+
</SidebarContext.Provider>
4755
</IntlProvider>
4856
</AppProvider>,
4957
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom';
3+
4+
import { useCourseOutlineSidebar } from '../hooks';
5+
6+
interface Props {
7+
courseId: string;
8+
sequenceId: string;
9+
activeUnitId: string;
10+
id: string;
11+
children?: React.ReactNode;
12+
}
13+
14+
/*
15+
* UnitLinkWrapper is necessary for unit navigation within the OutlineTrayPlugin.
16+
* import { Link } from 'react-router-dom' throws errors inside the plugin
17+
* because the package tries to load two versions of 'react-router-dom' or a
18+
* route can not be found. This component abstracts the import into a wrapper
19+
* component that can be imported into plugins without a render error.
20+
*/
21+
22+
const UnitLinkWrapper: React.FC<Props> = ({
23+
sequenceId,
24+
activeUnitId,
25+
id,
26+
courseId,
27+
children,
28+
}) => {
29+
const { handleUnitClick } = useCourseOutlineSidebar();
30+
31+
return (
32+
<Link
33+
to={`/course/${courseId}/${sequenceId}/${id}`}
34+
className="row w-100 m-0 d-flex align-items-center text-gray-700"
35+
onClick={() => handleUnitClick({ sequenceId, activeUnitId, id })}
36+
>
37+
{children}
38+
</Link>
39+
);
40+
};
41+
42+
export default UnitLinkWrapper;

0 commit comments

Comments
 (0)