Skip to content

Commit 8e921e2

Browse files
committed
fix: update to new course outline structure and design
1 parent 9664fbd commit 8e921e2

13 files changed

Lines changed: 62 additions & 184 deletions

src/components/AspectsSidebar/index.test.tsx

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,19 @@ const defaultProps = {
7171
};
7272

7373
type InitialState = {
74-
sidebarOpen: boolean,
7574
activeBlock?: Block | null,
7675
filterUnit?: Block | null,
7776
filteredBlocks?: string[]
7877
};
7978

8079
// Helper component to set context values for tests
8180
function TestContextHelper({
82-
sidebarOpen = true, activeBlock = null, filterUnit = null, filteredBlocks = [],
81+
activeBlock = null, filterUnit = null, filteredBlocks = [],
8382
}: InitialState) {
8483
const {
85-
setSidebarOpen, setActiveBlock, setFilterUnit, setFilteredBlocks,
84+
setActiveBlock, setFilterUnit, setFilteredBlocks,
8685
} = useAspectsSidebarContext();
8786

88-
// This effect is run only one to set the initial state for the tests.
89-
React.useEffect(() => {
90-
setSidebarOpen(sidebarOpen);
91-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
92-
9387
// The sidebar will clear active/filter stuff to render cleanly on initalization
9488
// So, we can't set these values in the useEffect above. The only way to cleanly
9589
// do it is by userEvent.click - simulating real-world scenario
@@ -109,7 +103,7 @@ function TestContextHelper({
109103

110104
describe('AspectsSidebar', () => {
111105
const renderComponent = (
112-
initialState: InitialState = { sidebarOpen: true },
106+
initialState: InitialState = {},
113107
props?: Partial<React.ComponentProps<typeof AspectsSidebar>>,
114108
) => render(
115109

@@ -126,19 +120,6 @@ describe('AspectsSidebar', () => {
126120
jest.clearAllMocks();
127121
});
128122

129-
it('closes the sidebar when the close button is clicked', async () => {
130-
renderComponent();
131-
// Ensure the sidebar is initially open and visible
132-
expect(screen.getByTestId('sidebar')).toBeVisible();
133-
expect(screen.getByTestId('sidebar-title')).toHaveTextContent('Test Course');
134-
135-
const closeButton = screen.getByRole('button', { name: 'Close' });
136-
fireEvent.click(closeButton);
137-
138-
// Assert that the sidebar is no longer visible
139-
expect(screen.queryByTestId('sidebar')).not.toBeInTheDocument();
140-
});
141-
142123
it('shows the back button and changes the title when a block is clicked', () => {
143124
renderComponent();
144125

@@ -168,7 +149,7 @@ describe('AspectsSidebar', () => {
168149

169150
it('shows an Alert message when the content lists are empty on a Unit Page', () => {
170151
// NOTE: Currently there is not "Unit Page Dashboard", hence the alert
171-
renderComponent({ sidebarOpen: true }, { blockType: BlockTypes.vertical, contentLists: [] });
152+
renderComponent(undefined, { blockType: BlockTypes.vertical, contentLists: [] });
172153

173154
expect(mockDashboard).not.toHaveBeenCalled();
174155
expect(screen.getByRole('alert')).toHaveTextContent(messages.noAnalyticsForUnit.defaultMessage);
@@ -177,7 +158,6 @@ describe('AspectsSidebar', () => {
177158
it('shows filtered set of components in the Course Outline view when specific unit is selected', () => {
178159
// render the component as if the "UnitActionsButton" has been clicked
179160
renderComponent({
180-
sidebarOpen: true,
181161
activeBlock: mockUnit,
182162
filterUnit: mockUnit,
183163
filteredBlocks: ['block-v1:TEST+COURSE+SECTION1+prob2'],
@@ -194,7 +174,6 @@ describe('AspectsSidebar', () => {
194174

195175
it('navigate to component and back in filtered unit view', () => {
196176
renderComponent({
197-
sidebarOpen: true,
198177
activeBlock: mockUnit,
199178
filterUnit: mockUnit,
200179
filteredBlocks: ['block-v1:TEST+COURSE+SECTION1+prob2'],
@@ -220,7 +199,7 @@ describe('AspectsSidebar', () => {
220199

221200
it('posts a callback with the activatedBlock in Unit Page View', () => {
222201
const callback = jest.fn();
223-
renderComponent({ sidebarOpen: true }, {
202+
renderComponent(undefined, {
224203
title: 'Test Unit',
225204
blockType: BlockTypes.vertical,
226205
contentLists: mockContentLists.slice(0, 1),

src/components/AspectsSidebar/index.tsx

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
22
import * as React from 'react';
33
import {
4-
Alert, Icon, IconButton, IconButtonWithTooltip, Stack, Sticky,
4+
Alert, Icon, IconButton, Stack, Sticky,
55
} from '@openedx/paragon';
66
import {
7-
ArrowBack, AutoGraph, Close, Warning,
7+
ArrowBack, Warning,
88
} from '@openedx/paragon/icons';
99
import { BlockTypes, ICON_MAP } from '../../constants';
1010
import { useAspectsSidebarContext } from '../../hooks';
@@ -36,7 +36,7 @@ export function AspectsSidebar({
3636
}: AspectsSidebarProps) {
3737
const intl = useIntl();
3838
const {
39-
sidebarOpen, setSidebarOpen, setFilteredBlocks, activeBlock, setActiveBlock,
39+
setFilteredBlocks, activeBlock, setActiveBlock,
4040
filterUnit, setFilterUnit, filteredBlocks,
4141
} = useAspectsSidebarContext();
4242

@@ -52,10 +52,6 @@ export function AspectsSidebar({
5252
// eslint-disable-next-line react-hooks/exhaustive-deps
5353
}, []);
5454

55-
if (!sidebarOpen) {
56-
return null;
57-
}
58-
5955
const hideDashboard: boolean = (
6056
(!!activeBlock && (activeBlock.type === 'vertical'))
6157
|| (!activeBlock && (blockType === 'vertical'))
@@ -92,32 +88,7 @@ export function AspectsSidebar({
9288
<Sticky className="shadow rounded" offset={2}>
9389
<div className="bg-white rounded w-100">
9490
<Stack className="sidebar-header">
95-
<Stack className="course-unit-sidebar-header px-4 pt-4" direction="horizontal">
96-
<h5 className="course-unit-sidebar-header-title h5 flex-grow-1 text-gray">
97-
{intl.formatMessage(messages.analyticsLabel)}
98-
<Icon
99-
src={AutoGraph}
100-
size="xs"
101-
className="d-inline-block ml-1"
102-
aria-hidden
103-
style={{ verticalAlign: 'middle' }}
104-
/>
105-
</h5>
106-
<IconButtonWithTooltip
107-
className="ml-auto"
108-
tooltipContent={intl.formatMessage(messages.closeButtonLabel)}
109-
tooltipPlacement="top"
110-
alt={intl.formatMessage(messages.closeButtonLabel)}
111-
src={Close}
112-
iconAs={Icon}
113-
variant="black"
114-
onClick={() => {
115-
setSidebarOpen(false);
116-
}}
117-
size="sm"
118-
/>
119-
</Stack>
120-
<h3 className="h3 px-4 pb-4 mb-0 d-flex align-items-center" data-testid="sidebar-title">
91+
<h3 className="h3 p-4 mb-0 d-flex align-items-center" data-testid="sidebar-title">
12192
{(activeBlock) && (
12293
<IconButton
12394
className="mr-2"

src/components/CourseHeaderButton.tsx

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/components/CourseOutlineSidebar.test.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { render } from '@testing-library/react';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4-
import { CourseOutlineSidebar } from './CourseOutlineSidebar';
4+
import { CourseOutlineAspectsPage } from './CourseOutlineSidebar';
55
import { useCourseBlocks, useAspectsSidebarContext } from '../hooks';
66
import { AspectsSidebar } from './AspectsSidebar';
77
import { BlockTypes } from '../constants';
@@ -23,6 +23,14 @@ jest.mock('../hooks', () => ({
2323
useCourseBlocks: jest.fn(),
2424
useAspectsSidebarContext: jest.fn(),
2525
}));
26+
jest.mock(
27+
'CourseAuthoring/course-outline/outline-sidebar/OutlineSidebarPagesContext',
28+
() => ({
29+
// eslint-disable-next-line react/prop-types
30+
OutlineSidebarPagesContext: ({ children }) => children,
31+
useOutlineSidebarPagesContext: () => { },
32+
}),
33+
);
2634

2735
// Test Data
2836
const mockCourseId = 'course-v1:TestX+TST101+2025';
@@ -103,7 +111,7 @@ const mockVideos: Block[] = [
103111
];
104112

105113
// Test Suite
106-
describe('CourseOutlineSidebar', () => {
114+
describe('CourseOutlineAspectsPage', () => {
107115
// Mock implementations setup
108116
const mockFormatMessage = jest.fn((message) => message.defaultMessage || message.id);
109117
const mockUseIntl = useIntl as jest.Mock;
@@ -122,13 +130,13 @@ describe('CourseOutlineSidebar', () => {
122130
MockAspectsSidebar.mockClear(); // Clear calls specifically for the component mock
123131
});
124132

125-
const renderComponent = (props: Partial<React.ComponentProps<typeof CourseOutlineSidebar>> = {}) => {
133+
const renderComponent = (props: Partial<React.ComponentProps<typeof CourseOutlineAspectsPage>> = {}) => {
126134
const defaultProps = {
127135
courseId: mockCourseId,
128136
courseName: mockCourseName,
129137
sections: mockSections,
130138
};
131-
return render(<CourseOutlineSidebar {...defaultProps} {...props} />);
139+
return render(<CourseOutlineAspectsPage {...defaultProps} {...props} />);
132140
};
133141

134142
// --- Test Cases ---

src/components/CourseOutlineSidebar.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import * as React from 'react';
1+
import React, { useMemo } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { AutoGraph } from '@openedx/paragon/icons';
4+
5+
import {
6+
OutlineSidebarPagesContext,
7+
useOutlineSidebarPagesContext,
8+
} from 'CourseAuthoring/course-outline/outline-sidebar/OutlineSidebarPagesContext';
9+
310
import { useCourseBlocks, useAspectsSidebarContext } from '../hooks';
411
import { BlockTypes } from '../constants';
512
import { AspectsSidebar } from './AspectsSidebar';
613
import messages from '../messages';
714
import { Section, Block, castToBlock } from '../types';
815

9-
interface Props {
16+
interface CourseOutlineAspectsPageProps {
1017
courseId: string;
1118
courseName: string;
1219
sections: Section[];
@@ -22,7 +29,7 @@ function* getGradedSubsections(sections: Section[]) {
2229
}
2330
}
2431

25-
export function CourseOutlineSidebar({ courseId, courseName, sections }: Props) {
32+
export function CourseOutlineAspectsPage({ courseId, courseName, sections }: CourseOutlineAspectsPageProps) {
2633
const intl = useIntl();
2734
const { filteredBlocks } = useAspectsSidebarContext();
2835
const { data } = useCourseBlocks(courseId);
@@ -62,3 +69,26 @@ export function CourseOutlineSidebar({ courseId, courseName, sections }: Props)
6269
/>
6370
);
6471
}
72+
73+
export function CourseOutlineSidebarWrapper(
74+
{ component, pluginProps }: { component: React.ReactNode, pluginProps: CourseOutlineAspectsPageProps },
75+
) {
76+
const sidebarPages = useOutlineSidebarPagesContext();
77+
78+
const AnalyticsPage = React.useCallback(() => <CourseOutlineAspectsPage {...pluginProps} />, [pluginProps]);
79+
80+
const overridedPages = useMemo(() => ({
81+
...sidebarPages,
82+
analytics: {
83+
component: AnalyticsPage,
84+
icon: AutoGraph,
85+
title: messages.analyticsLabel,
86+
},
87+
}), [sidebarPages, AnalyticsPage]);
88+
89+
return (
90+
<OutlineSidebarPagesContext.Provider value={overridedPages}>
91+
{component}
92+
</OutlineSidebarPagesContext.Provider>
93+
);
94+
}

src/components/SidebarToggleWrapper.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/components/SubSectionAnalyticsButton.test.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@ const mockSubsections: SubSection[] = [
4747
const mockUseAspectsSidebarContext = useAspectsSidebarContext as jest.Mock;
4848

4949
describe('SubSectionAnalyticsButton', () => {
50-
const mockSetSidebarOpen = jest.fn();
5150
const mockSetActiveBlock = jest.fn();
5251
const mockSetFilterUnit = jest.fn();
5352

5453
const defaultContextValue = {
5554
activeBlock: null,
5655
sidebarOpen: false,
5756
setActiveBlock: mockSetActiveBlock,
58-
setSidebarOpen: mockSetSidebarOpen,
5957
setFilterUnit: mockSetFilterUnit,
6058
};
6159

@@ -79,7 +77,6 @@ describe('SubSectionAnalyticsButton', () => {
7977
const button = screen.getByRole('button', { name: /analytics/i });
8078
await user.click(button);
8179

82-
expect(mockSetSidebarOpen).toHaveBeenCalledWith(true);
8380
expect(mockSetActiveBlock).toHaveBeenCalledWith(expect.objectContaining({
8481
id: mockSubsection.id,
8582
}));
@@ -92,14 +89,12 @@ describe('SubSectionAnalyticsButton', () => {
9289
mockUseAspectsSidebarContext.mockReturnValue({
9390
...defaultContextValue,
9491
activeBlock: { id: mockSubsection.id } as any,
95-
sidebarOpen: true,
9692
});
9793

9894
render(<SubSectionAnalyticsButton subsection={mockSubsection} />);
9995
const button = screen.getByRole('button', { name: /analytics/i });
10096
await user.click(button);
10197

102-
expect(mockSetSidebarOpen).toHaveBeenCalledWith(true);
10398
expect(mockSetActiveBlock).toHaveBeenCalledWith(null);
10499
expect(mockSetFilterUnit).not.toHaveBeenCalled();
105100
});

src/components/SubSectionAnalyticsButton.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import * as React from 'react';
21
import { IconButton } from '@openedx/paragon';
32
import { AutoGraph } from '@openedx/paragon/icons';
43
import { useAspectsSidebarContext } from '../hooks';
54
import { Block, SubSection, castToBlock } from '../types';
65

76
export function SubSectionAnalyticsButton({ subsection }: { subsection: SubSection }) {
87
const {
9-
activeBlock, sidebarOpen, setActiveBlock, setSidebarOpen,
10-
setFilterUnit,
8+
activeBlock, setActiveBlock, setFilterUnit,
119
} = useAspectsSidebarContext();
1210
if (!subsection.graded) {
1311
return null;
@@ -16,9 +14,8 @@ export function SubSectionAnalyticsButton({ subsection }: { subsection: SubSecti
1614
<IconButton
1715
alt="Analytics"
1816
iconAs={AutoGraph}
19-
isActive={sidebarOpen && (activeBlock?.id === subsection.id)}
17+
isActive={activeBlock?.id === subsection.id}
2018
onClick={() => {
21-
setSidebarOpen(true);
2219
if (activeBlock?.id === subsection.id) {
2320
setActiveBlock(null);
2421
} else {

0 commit comments

Comments
 (0)