Skip to content

Commit 2f0eabb

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

13 files changed

Lines changed: 103 additions & 185 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: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as React from 'react';
2-
import { render } from '@testing-library/react';
2+
import { render, screen } from '@testing-library/react';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4-
import { CourseOutlineSidebar } from './CourseOutlineSidebar';
4+
5+
// @ts-ignore
6+
import {
7+
useOutlineSidebarPagesContext,
8+
} from 'CourseAuthoring/course-outline/outline-sidebar/OutlineSidebarPagesContext';
9+
10+
import { CourseOutlineAspectsPage, CourseOutlineSidebarWrapper } from './CourseOutlineSidebar';
511
import { useCourseBlocks, useAspectsSidebarContext } from '../hooks';
612
import { AspectsSidebar } from './AspectsSidebar';
713
import { BlockTypes } from '../constants';
@@ -24,6 +30,20 @@ jest.mock('../hooks', () => ({
2430
useAspectsSidebarContext: jest.fn(),
2531
}));
2632

33+
jest.mock(
34+
'CourseAuthoring/course-outline/outline-sidebar/OutlineSidebarPagesContext',
35+
() => {
36+
const mockOutlineSidebarPagesContext = React.createContext({});
37+
const useOutlineSidebarPagesContextMocked = () => React.useContext(mockOutlineSidebarPagesContext) as Record<string, any>;
38+
39+
return {
40+
OutlineSidebarPagesContext: mockOutlineSidebarPagesContext,
41+
useOutlineSidebarPagesContext: useOutlineSidebarPagesContextMocked,
42+
};
43+
},
44+
{ virtual: true },
45+
);
46+
2747
// Test Data
2848
const mockCourseId = 'course-v1:TestX+TST101+2025';
2949
const mockCourseName = 'Test Course 101';
@@ -103,7 +123,7 @@ const mockVideos: Block[] = [
103123
];
104124

105125
// Test Suite
106-
describe('CourseOutlineSidebar', () => {
126+
describe('CourseOutlineAspectsPage', () => {
107127
// Mock implementations setup
108128
const mockFormatMessage = jest.fn((message) => message.defaultMessage || message.id);
109129
const mockUseIntl = useIntl as jest.Mock;
@@ -122,13 +142,13 @@ describe('CourseOutlineSidebar', () => {
122142
MockAspectsSidebar.mockClear(); // Clear calls specifically for the component mock
123143
});
124144

125-
const renderComponent = (props: Partial<React.ComponentProps<typeof CourseOutlineSidebar>> = {}) => {
145+
const renderComponent = (props: Partial<React.ComponentProps<typeof CourseOutlineAspectsPage>> = {}) => {
126146
const defaultProps = {
127147
courseId: mockCourseId,
128148
courseName: mockCourseName,
129149
sections: mockSections,
130150
};
131-
return render(<CourseOutlineSidebar {...defaultProps} {...props} />);
151+
return render(<CourseOutlineAspectsPage {...defaultProps} {...props} />);
132152
};
133153

134154
// --- Test Cases ---
@@ -310,3 +330,30 @@ describe('CourseOutlineSidebar', () => {
310330
expect(mockFormatMessage).not.toHaveBeenCalledWith(messages.gradedSubsectionAnalytics);
311331
});
312332
});
333+
334+
function MockComponent() {
335+
const sidebarPages = useOutlineSidebarPagesContext();
336+
337+
// Return a div with the title of the analytics page, reading from the context
338+
return (
339+
<div>
340+
{sidebarPages.analytics.title.defaultMessage}
341+
</div>
342+
);
343+
}
344+
345+
describe('CourseOutlineSidebarWrapper', () => {
346+
const renderComponent = () => render(<CourseOutlineSidebarWrapper
347+
component={<MockComponent />}
348+
pluginProps={{
349+
courseId: mockCourseId,
350+
courseName: mockCourseName,
351+
sections: mockSections,
352+
}}
353+
/>);
354+
355+
it('adds analytics page to the sidebar', () => {
356+
renderComponent();
357+
expect(screen.getByText('Analytics')).toBeInTheDocument();
358+
});
359+
});

src/components/CourseOutlineSidebar.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
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+
// @ts-ignore
9+
} from 'CourseAuthoring/course-outline/outline-sidebar/OutlineSidebarPagesContext';
10+
311
import { useCourseBlocks, useAspectsSidebarContext } from '../hooks';
412
import { BlockTypes } from '../constants';
513
import { AspectsSidebar } from './AspectsSidebar';
614
import messages from '../messages';
715
import { Section, Block, castToBlock } from '../types';
816

9-
interface Props {
17+
interface CourseOutlineAspectsPageProps {
1018
courseId: string;
1119
courseName: string;
1220
sections: Section[];
@@ -22,7 +30,7 @@ function* getGradedSubsections(sections: Section[]) {
2230
}
2331
}
2432

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

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

0 commit comments

Comments
 (0)