Skip to content

Commit ae67be8

Browse files
authored
feat: new course outline header [FC-0114] (#2735)
Adds new header and subheader to course outline. Converts existing js code to ts.
1 parent 6f37118 commit ae67be8

34 files changed

Lines changed: 990 additions & 403 deletions

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
3737
ENABLE_TAGGING_TAXONOMY_PAGES=true
3838
ENABLE_CERTIFICATE_PAGE=true
3939
ENABLE_COURSE_IMPORT_IN_LIBRARY=false
40+
ENABLE_COURSE_OUTLINE_NEW_DESIGN=false
4041
BBB_LEARN_MORE_URL=''
4142
HOTJAR_APP_ID=''
4243
HOTJAR_VERSION=6

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ENABLE_ASSETS_PAGE=false
3838
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
3939
ENABLE_CERTIFICATE_PAGE=true
4040
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
41+
ENABLE_COURSE_OUTLINE_NEW_DESIGN=true
4142
ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
4243
ENABLE_TAGGING_TAXONOMY_PAGES=true
4344
BBB_LEARN_MORE_URL=''

.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ ENABLE_ASSETS_PAGE=false
3434
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
3535
ENABLE_CERTIFICATE_PAGE=true
3636
ENABLE_COURSE_IMPORT_IN_LIBRARY=true
37+
ENABLE_COURSE_OUTLINE_NEW_DESIGN=false
3738
ENABLE_TAGGING_TAXONOMY_PAGES=true
3839
BBB_LEARN_MORE_URL=''
3940
INVITE_STUDENTS_EMAIL_TO="[email protected]"

src/course-libraries/CourseLibraries.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ export const CourseLibraries = () => {
198198
<SubHeader
199199
title={intl.formatMessage(messages.headingTitle)}
200200
subtitle={intl.formatMessage(messages.headingSubtitle)}
201-
headerActions={!showReviewAlert && outOfSyncCount > 0 && tabKey === CourseLibraryTabs.all && (
201+
headerActions={(!showReviewAlert && outOfSyncCount > 0 && tabKey === CourseLibraryTabs.all) ? (
202202
<Button
203203
variant="primary"
204204
onClick={onAlertReview}
205205
iconBefore={Cached}
206206
>
207207
{intl.formatMessage(messages.reviewUpdatesBtn)}
208208
</Button>
209-
)}
209+
) : null}
210210
hideBorder
211211
/>
212212
<section className="mb-4">

src/course-outline/CourseOutline.test.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getConfig } from '@edx/frontend-platform';
1+
import { getConfig, setConfig } from '@edx/frontend-platform';
22
import { cloneDeep } from 'lodash';
33
import { closestCorners } from '@dnd-kit/core';
44
import { logError } from '@edx/frontend-platform/logging';
@@ -17,6 +17,7 @@ import {
1717
act, fireEvent, initializeMocks, render, screen, waitFor, within,
1818
} from '@src/testUtils';
1919
import { XBlock } from '@src/data/types';
20+
import { userEvent } from '@testing-library/user-event';
2021
import {
2122
getCourseBestPracticesApiUrl,
2223
getCourseLaunchApiUrl,
@@ -182,12 +183,10 @@ describe('<CourseOutline />', () => {
182183
});
183184

184185
it('render CourseOutline component correctly', async () => {
185-
const { getByText } = renderComponent();
186+
renderComponent();
186187

187-
await waitFor(() => {
188-
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
189-
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
190-
});
188+
expect(await screen.findByText('Demonstration Course')).toBeInTheDocument();
189+
expect(await screen.findByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
191190
});
192191

193192
it('logs an error when syncDiscussionsTopics encounters an API failure', async () => {
@@ -2486,4 +2485,20 @@ describe('<CourseOutline />', () => {
24862485
});
24872486
expect(axiosMock.history.delete[0].url).toBe(getDownstreamApiUrl(courseSectionMock.id));
24882487
});
2488+
2489+
it('check that the new status bar and expand bar is shown when flag is set', async () => {
2490+
setConfig({
2491+
...getConfig(),
2492+
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
2493+
});
2494+
renderComponent();
2495+
const btn = await screen.findByRole('button', { name: 'Collapse all' });
2496+
expect(btn).toBeInTheDocument();
2497+
expect(await screen.findByRole('link', { name: 'View live' })).toBeInTheDocument();
2498+
expect(await screen.findByRole('button', { name: 'Add' })).toBeInTheDocument();
2499+
expect(await screen.findByRole('button', { name: 'More actions' })).toBeInTheDocument();
2500+
const user = userEvent.setup();
2501+
await user.click(btn);
2502+
expect(await screen.findByRole('button', { name: 'Expand all' })).toBeInTheDocument();
2503+
});
24892504
});

src/course-outline/CourseOutline.tsx

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { getConfig } from '@edx/frontend-platform';
34
import {
45
Container,
56
Layout,
67
Row,
78
TransitionReplace,
89
Toast,
910
StandardModal,
11+
Button,
12+
ActionRow,
1013
} from '@openedx/paragon';
1114
import { Helmet } from 'react-helmet';
12-
import { CheckCircle as CheckCircleIcon } from '@openedx/paragon/icons';
15+
import { CheckCircle as CheckCircleIcon, CloseFullscreen, OpenInFull } from '@openedx/paragon/icons';
1316
import { useSelector } from 'react-redux';
1417
import {
1518
arrayMove,
@@ -44,7 +47,6 @@ import {
4447
getTimedExamsFlag,
4548
} from './data/selectors';
4649
import { COURSE_BLOCK_NAMES } from './constants';
47-
import StatusBar from './status-bar/StatusBar';
4850
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
4951
import SectionCard from './section-card/SectionCard';
5052
import SubsectionCard from './subsection-card/SubsectionCard';
@@ -61,8 +63,11 @@ import {
6163
} from './drag-helper/utils';
6264
import { useCourseOutline } from './hooks';
6365
import messages from './messages';
66+
import headerMessages from './header-navigations/messages';
6467
import { getTagsExportFile } from './data/api';
6568
import OutlineAddChildButtons from './OutlineAddChildButtons';
69+
import { StatusBar } from './status-bar/StatusBar';
70+
import { LegacyStatusBar } from './status-bar/LegacyStatusBar';
6671

6772
const CourseOutline = () => {
6873
const intl = useIntl();
@@ -141,6 +146,9 @@ const CourseOutline = () => {
141146
resetScrollState,
142147
} = useCourseOutline({ courseId });
143148

149+
// Show the new actions bar if it is enabled in the configuration.
150+
// This is a temporary flag until the new design feature is fully implemented.
151+
const showNewActionsBar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
144152
// Use `setToastMessage` to show the toast.
145153
const [toastMessage, setToastMessage] = useState<string | null>(null);
146154

@@ -314,8 +322,9 @@ const CourseOutline = () => {
314322
) : null}
315323
</TransitionReplace>
316324
<SubHeader
317-
title={intl.formatMessage(messages.headingTitle)}
325+
title={courseName}
318326
subtitle={intl.formatMessage(messages.headingSubtitle)}
327+
hideBorder
319328
headerActions={(
320329
<CourseOutlineHeaderActionsSlot
321330
isReIndexShow={isReIndexShow}
@@ -329,6 +338,23 @@ const CourseOutline = () => {
329338
/>
330339
)}
331340
/>
341+
{showNewActionsBar
342+
? (
343+
<StatusBar
344+
courseId={courseId}
345+
isLoading={isLoading}
346+
statusBarData={statusBarData}
347+
/>
348+
) : (
349+
<LegacyStatusBar
350+
courseId={courseId}
351+
isLoading={isLoading}
352+
statusBarData={statusBarData}
353+
openEnableHighlightsModal={openEnableHighlightsModal}
354+
handleVideoSharingOptionChange={handleVideoSharingOptionChange}
355+
/>
356+
)}
357+
<hr className="mt-4 mb-0 w-100 text-light-400" />
332358
<Layout
333359
lg={[{ span: 9 }, { span: 3 }]}
334360
md={[{ span: 9 }, { span: 3 }]}
@@ -339,14 +365,24 @@ const CourseOutline = () => {
339365
<Layout.Element>
340366
<article>
341367
<div>
368+
{showNewActionsBar && (
369+
<ActionRow className="mt-3">
370+
{Boolean(sectionsList.length) && (
371+
<Button
372+
variant="outline-primary"
373+
id="expand-collapse-all-button"
374+
data-testid="expand-collapse-all-button"
375+
iconBefore={isSectionsExpanded ? CloseFullscreen : OpenInFull}
376+
onClick={headerNavigationsActions.handleExpandAll}
377+
>
378+
{isSectionsExpanded
379+
? intl.formatMessage(headerMessages.collapseAllButton)
380+
: intl.formatMessage(headerMessages.expandAllButton)}
381+
</Button>
382+
)}
383+
</ActionRow>
384+
)}
342385
<section className="course-outline-section">
343-
<StatusBar
344-
courseId={courseId}
345-
isLoading={isLoading}
346-
statusBarData={statusBarData}
347-
openEnableHighlightsModal={openEnableHighlightsModal}
348-
handleVideoSharingOptionChange={handleVideoSharingOptionChange}
349-
/>
350386
{!errors?.outlineIndexApi && (
351387
<div className="pt-4">
352388
{sections.length ? (

src/course-outline/data/slice.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const initialState = {
2222
savingStatus: '',
2323
statusBarData: {
2424
courseReleaseDate: '',
25+
endDate: '',
2526
highlightsEnabledForMessaging: false,
2627
isSelfPaced: false,
2728
checklist: {

src/course-outline/data/thunk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export function fetchCourseOutlineIndexQuery(courseId: string): (dispatch: any)
7575
videoSharingEnabled,
7676
videoSharingOptions,
7777
actions,
78+
end,
7879
},
7980
} = outlineIndex;
8081
dispatch(fetchOutlineIndexSuccess(outlineIndex));
@@ -83,6 +84,7 @@ export function fetchCourseOutlineIndexQuery(courseId: string): (dispatch: any)
8384
highlightsEnabledForMessaging,
8485
videoSharingOptions,
8586
videoSharingEnabled,
87+
endDate: end,
8688
}));
8789
dispatch(updateCourseActions(actions));
8890

src/course-outline/data/types.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export interface CourseStructure {
44
highlightsEnabledForMessaging: boolean,
55
videoSharingEnabled: boolean,
66
videoSharingOptions: string,
7+
start: string,
8+
end: string,
79
actions: XBlockActions,
810
}
911

@@ -33,6 +35,21 @@ export interface CourseDetails {
3335
description?: string;
3436
}
3537

38+
export interface CourseOutlineStatusBar {
39+
courseReleaseDate: string;
40+
endDate: string;
41+
highlightsEnabledForMessaging: boolean;
42+
isSelfPaced: boolean;
43+
checklist: {
44+
totalCourseLaunchChecks: number;
45+
completedCourseLaunchChecks: number;
46+
totalCourseBestPracticesChecks: number;
47+
completedCourseBestPracticesChecks: number;
48+
};
49+
videoSharingEnabled: boolean;
50+
videoSharingOptions: string;
51+
}
52+
3653
export interface CourseOutlineState {
3754
loadingStatus: {
3855
outlineIndexLoadingStatus: string;
@@ -48,19 +65,7 @@ export interface CourseOutlineState {
4865
};
4966
outlineIndexData: object;
5067
savingStatus: string;
51-
statusBarData: {
52-
courseReleaseDate: string;
53-
highlightsEnabledForMessaging: boolean;
54-
isSelfPaced: boolean;
55-
checklist: {
56-
totalCourseLaunchChecks: number;
57-
completedCourseLaunchChecks: number;
58-
totalCourseBestPracticesChecks: number;
59-
completedCourseBestPracticesChecks: number;
60-
};
61-
videoSharingEnabled: boolean;
62-
videoSharingOptions: string;
63-
};
68+
statusBarData: CourseOutlineStatusBar;
6469
sectionsList: Array<XBlock>;
6570
isCustomRelativeDatesActive: boolean;
6671
currentSection: XBlock | {};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
fireEvent, initializeMocks, render, screen,
3+
} from '@src/testUtils';
4+
import messages from './messages';
5+
import HeaderActions, { HeaderActionsProps } from './HeaderActions';
6+
7+
const handleNewSectionMock = jest.fn();
8+
9+
const headerNavigationsActions = {
10+
handleNewSection: handleNewSectionMock,
11+
lmsLink: '',
12+
};
13+
14+
const courseActions = {
15+
draggable: true,
16+
childAddable: true,
17+
deletable: true,
18+
duplicable: true,
19+
};
20+
21+
const renderComponent = (props?: Partial<HeaderActionsProps>) => render(
22+
<HeaderActions
23+
actions={headerNavigationsActions}
24+
courseActions={courseActions}
25+
{...props}
26+
/>,
27+
);
28+
29+
describe('<HeaderActions />', () => {
30+
beforeEach(() => {
31+
initializeMocks();
32+
});
33+
34+
it('render HeaderActions component correctly', async () => {
35+
renderComponent();
36+
37+
expect(await screen.findByRole('button', { name: messages.addButton.defaultMessage })).toBeInTheDocument();
38+
expect(await screen.findByRole('button', { name: messages.viewLiveButton.defaultMessage })).toBeInTheDocument();
39+
expect(await screen.findByRole('button', { name: messages.moreActionsButtonAriaLabel.defaultMessage })).toBeInTheDocument();
40+
});
41+
42+
it('calls the correct handlers when clicking buttons', async () => {
43+
renderComponent();
44+
45+
const addButton = await screen.findByRole('button', { name: messages.addButton.defaultMessage });
46+
fireEvent.click(addButton);
47+
expect(handleNewSectionMock).toHaveBeenCalledTimes(1);
48+
});
49+
50+
it('disables new section button if course outline fetch fails', async () => {
51+
renderComponent({
52+
errors: { outlineIndexApi: { data: 'some error', type: 'serverError' } },
53+
});
54+
55+
expect(await screen.findByRole('button', { name: messages.addButton.defaultMessage })).toBeInTheDocument();
56+
expect(await screen.findByRole('button', { name: messages.addButton.defaultMessage })).toBeDisabled();
57+
});
58+
});

0 commit comments

Comments
 (0)