Skip to content

Commit 9c8e672

Browse files
authored
feat: remove ENABLE_COURSE_OUTLINE_NEW_DESIGN (#3005)
This PR removes the `ENABLE_COURSE_OUTLINE_NEW_DESIGN` flag, alongside the legacy code for the legacy page. It also includes the `Reindex` button in the Header, the `Enable email highlights` and the `Video sharing` settings in the course status bar.
1 parent 47d213b commit 9c8e672

27 files changed

Lines changed: 337 additions & 1279 deletions

src/course-outline/CourseOutline.test.tsx

Lines changed: 30 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getConfig, setConfig } from '@edx/frontend-platform';
1+
import { getConfig } from '@edx/frontend-platform';
22
import { cloneDeep } from 'lodash';
33
import { closestCorners } from '@dnd-kit/core';
44
import { logError } from '@edx/frontend-platform/logging';
@@ -9,8 +9,7 @@ import { executeThunk } from '@src/utils';
99
import configureModalMessages from '@src/generic/configure-modal/messages';
1010
import pasteButtonMessages from '@src/generic/clipboard/paste-component/messages';
1111
import { getApiBaseUrl, getClipboardUrl } from '@src/generic/data/api';
12-
import { postXBlockBaseApiUrl } from '@src/course-unit/data/api';
13-
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
12+
import { ContainerType } from '@src/generic/key-utils';
1413
import { getDownstreamApiUrl } from '@src/generic/unlink-modal/data/api';
1514
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
1615
import {
@@ -74,9 +73,8 @@ let axiosMock: import('axios-mock-adapter/types');
7473
let store;
7574
const mockPathname = '/foo-bar';
7675
const courseId = '123';
77-
const getContainerKey = jest.fn().mockReturnValue('lct:org:lib:unit:1');
78-
const getContainerType = jest.fn().mockReturnValue('unit');
7976
const clearSelection = jest.fn();
77+
const startCurrentFlow = jest.fn();
8078
let selectedContainerId: string | undefined;
8179
let courseOutlineIndexMock = cloneDeep(originalCourseOutlineIndexMock);
8280

@@ -85,6 +83,7 @@ jest.mock('@src/course-outline/outline-sidebar/OutlineSidebarContext', () => ({
8583
...jest.requireActual('@src/course-outline/outline-sidebar/OutlineSidebarContext'),
8684
useOutlineSidebarContext: () => ({
8785
...jest.requireActual('@src/course-outline/outline-sidebar/OutlineSidebarContext').useOutlineSidebarContext(),
86+
startCurrentFlow,
8887
clearSelection,
8988
selectedContainerState: (() => (selectedContainerId ? { currentId: selectedContainerId } : undefined))(),
9089
}),
@@ -109,24 +108,6 @@ jest.mock('./data/api', () => ({
109108
getTagsCount: () => jest.fn().mockResolvedValue({}),
110109
}));
111110

112-
// Mock LibraryAndComponentPicker to call onComponentSelected on click
113-
jest.mock('@src/library-authoring/component-picker', () => ({
114-
LibraryAndComponentPicker: (props) => {
115-
const onClick = () => {
116-
// eslint-disable-next-line react/prop-types
117-
props.onComponentSelected({
118-
usageKey: getContainerKey(),
119-
blockType: getContainerType(),
120-
});
121-
};
122-
return (
123-
<button type="submit" onClick={onClick}>
124-
Dummy button
125-
</button>
126-
);
127-
},
128-
}));
129-
130111
jest.mock('@edx/frontend-platform/logging', () => ({
131112
logError: jest.fn(),
132113
}));
@@ -480,118 +461,66 @@ describe('<CourseOutline />', () => {
480461
});
481462

482463
it('adds a unit from library correctly', async () => {
483-
getContainerKey.mockReturnValue('lct:org:lib:unit:1');
484-
getContainerKey.mockReturnValue('unit');
464+
const user = userEvent.setup();
485465
renderComponent();
486466
const [sectionElement] = await screen.findAllByTestId('section-card');
487467
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
488468
const units = await within(subsectionElement).findAllByTestId('unit-card');
489469
expect(units.length).toBe(1);
490470

491-
axiosMock
492-
.onPost(postXBlockBaseApiUrl())
493-
.reply(200, {
494-
locator: 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@vertical1e842129',
495-
parent_locator: 'parent',
496-
});
497-
498471
const addUnitFromLibraryButton = within(subsectionElement).getByRole('button', {
499472
name: /use unit from library/i,
500473
});
501-
fireEvent.click(addUnitFromLibraryButton);
502-
503-
// click dummy button to execute onComponentSelected prop.
504-
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
505-
fireEvent.click(dummyBtn);
506-
507-
await waitFor(() => expect(axiosMock.history.post.length).toBe(3));
474+
await user.click(addUnitFromLibraryButton);
508475

476+
// Start the add existing unit flow
509477
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
510478
const [subsection] = section.childInfo.children;
511-
await waitFor(() => {
512-
expect(axiosMock.history.post[2].data).toBe(JSON.stringify({
513-
type: COMPONENT_TYPES.libraryV2,
514-
category: 'vertical',
515-
parent_locator: subsection.id,
516-
library_content_key: getContainerKey(),
517-
}));
479+
expect(startCurrentFlow).toHaveBeenCalledWith({
480+
flowType: ContainerType.Unit,
481+
parentLocator: subsection.id,
482+
grandParentLocator: section.id,
518483
});
519484
});
520485

521486
it('adds a subsection from library correctly', async () => {
522-
getContainerKey.mockReturnValue('lct:org:lib:subsection:1');
523-
getContainerKey.mockReturnValue('subsection');
487+
const user = userEvent.setup();
524488
renderComponent();
525489
const [sectionElement] = await screen.findAllByTestId('section-card');
526490
const subsections = await within(sectionElement).findAllByTestId('subsection-card');
527491
expect(subsections.length).toBe(2);
528492

529-
axiosMock
530-
.onPost(postXBlockBaseApiUrl())
531-
.reply(200, {
532-
locator: 'block-v1:UNIX+UX1+2025_T3+type@sequential+block@sequential45d4d95a',
533-
parent_locator: 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@chaptersda1',
534-
});
535-
536493
const addSubsectionFromLibraryButton = within(sectionElement).getByRole('button', {
537494
name: /use subsection from library/i,
538495
});
539-
fireEvent.click(addSubsectionFromLibraryButton);
540-
541-
// click dummy button to execute onComponentSelected prop.
542-
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
543-
fireEvent.click(dummyBtn);
544-
545-
await waitFor(() => expect(axiosMock.history.post.length).toBe(3));
496+
await user.click(addSubsectionFromLibraryButton);
546497

498+
// Start the add existing subsection flow
547499
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
548-
await waitFor(() => {
549-
expect(axiosMock.history.post[2].data).toBe(JSON.stringify({
550-
type: COMPONENT_TYPES.libraryV2,
551-
category: 'sequential',
552-
parent_locator: section.id,
553-
library_content_key: getContainerKey(),
554-
}));
500+
expect(startCurrentFlow).toHaveBeenCalledWith({
501+
flowType: ContainerType.Subsection,
502+
parentLocator: section.id,
503+
grandParentLocator: undefined,
555504
});
556505
});
557506

558507
it('adds a section from library correctly', async () => {
559508
const user = userEvent.setup();
560-
getContainerKey.mockReturnValue('lct:org:lib:section:1');
561-
getContainerKey.mockReturnValue('section');
562509
renderComponent();
563510
const sections = await screen.findAllByTestId('section-card');
564511
expect(sections.length).toBe(4);
565512

566-
axiosMock
567-
.onPost(postXBlockBaseApiUrl())
568-
.reply(200, {
569-
locator: 'block-v1:UNIX+UX1+2025_T3+type@chapter+block@chaptersdafdd',
570-
courseKey: 'course-v1:UNIX+UX1+2025_T3',
571-
});
572-
axiosMock
573-
.onGet(getXBlockApiUrl('block-v1:UNIX+UX1+2025_T3+type@chapter+block@chaptersdafdd'))
574-
.reply(200, courseSectionMock);
575-
576513
const addSectionFromLibraryButton = await screen.findByRole('button', {
577514
name: /use section from library/i,
578515
});
579516
await user.click(addSectionFromLibraryButton);
580517

581-
// click dummy button to execute onComponentSelected prop.
582-
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
583-
fireEvent.click(dummyBtn);
584-
585-
await waitFor(() => expect(axiosMock.history.post.length).toBe(3));
586-
587-
const courseUsageKey = courseOutlineIndexMock.courseStructure.id;
588-
await waitFor(() => {
589-
expect(axiosMock.history.post[2].data).toBe(JSON.stringify({
590-
type: COMPONENT_TYPES.libraryV2,
591-
category: 'chapter',
592-
parent_locator: courseUsageKey,
593-
library_content_key: getContainerKey(),
594-
}));
518+
// Start the add existing section flow
519+
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
520+
expect(startCurrentFlow).toHaveBeenCalledWith({
521+
flowType: ContainerType.Section,
522+
parentLocator: courseBlockId,
523+
grandParentLocator: undefined,
595524
});
596525
});
597526

@@ -820,6 +749,7 @@ describe('<CourseOutline />', () => {
820749
});
821750

822751
it('check whether section, subsection and unit is deleted when corresponding delete button is clicked', async () => {
752+
const user = userEvent.setup();
823753
renderComponent();
824754
// get section, subsection and unit
825755
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
@@ -831,22 +761,18 @@ describe('<CourseOutline />', () => {
831761
selectedContainerId = section.id;
832762

833763
const checkDeleteBtn = async (item, element, elementName) => {
834-
await waitFor(() => {
835-
expect(screen.queryByText(item.displayName)).toBeInTheDocument();
836-
});
764+
expect(within(element).getByText(item.displayName)).toBeInTheDocument();
837765

838766
axiosMock.onDelete(getCourseItemApiUrl(item.id)).reply(200);
839767

840768
const menu = await within(element).findByTestId(`${elementName}-card-header__menu-button`);
841-
fireEvent.click(menu);
769+
await user.click(menu);
842770
const deleteButton = await within(element).findByTestId(`${elementName}-card-header__menu-delete-button`);
843-
fireEvent.click(deleteButton);
771+
await user.click(deleteButton);
844772
const confirmButton = await screen.findByRole('button', { name: 'Delete' });
845-
fireEvent.click(confirmButton);
773+
await user.click(confirmButton);
846774

847-
await waitFor(() => {
848-
expect(screen.queryByText(item.displayName)).not.toBeInTheDocument();
849-
});
775+
expect(element).not.toBeInTheDocument();
850776
};
851777

852778
// delete unit, subsection and then section in order.
@@ -2614,10 +2540,6 @@ describe('<CourseOutline />', () => {
26142540
});
26152541

26162542
it('check that the new status bar and expand bar is shown when flag is set', async () => {
2617-
setConfig({
2618-
...getConfig(),
2619-
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
2620-
});
26212543
renderComponent();
26222544
const btn = await screen.findByRole('button', { name: 'Collapse all' });
26232545
expect(btn).toBeInTheDocument();

src/course-outline/CourseOutline.tsx

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ import headerMessages from './header-navigations/messages';
5858
import { getTagsExportFile } from './data/api';
5959
import OutlineAddChildButtons from './OutlineAddChildButtons';
6060
import { StatusBar } from './status-bar/StatusBar';
61-
import { LegacyStatusBar } from './status-bar/LegacyStatusBar';
62-
import { isOutlineNewDesignEnabled } from './utils';
6361

6462
const CourseOutline = () => {
6563
const intl = useIntl();
@@ -131,9 +129,6 @@ const CourseOutline = () => {
131129
handleUnlinkItemSubmit,
132130
} = useCourseOutline({ courseId });
133131

134-
// Show the new actions bar if it is enabled in the configuration.
135-
// This is a temporary flag until the new design feature is fully implemented.
136-
const showNewActionsBar = isOutlineNewDesignEnabled();
137132
// Use `setToastMessage` to show the toast.
138133
const [toastMessage, setToastMessage] = useState<string | null>(null);
139134

@@ -244,45 +239,33 @@ const CourseOutline = () => {
244239
/>
245240
}
246241
/>
247-
{showNewActionsBar
248-
? (
249-
<StatusBar
250-
courseId={courseId}
251-
isLoading={isLoading}
252-
statusBarData={statusBarData}
253-
/>
254-
) :
255-
(
256-
<LegacyStatusBar
257-
courseId={courseId}
258-
isLoading={isLoading}
259-
statusBarData={statusBarData}
260-
openEnableHighlightsModal={openEnableHighlightsModal}
261-
handleVideoSharingOptionChange={handleVideoSharingOptionChange}
262-
/>
263-
)}
242+
<StatusBar
243+
courseId={courseId}
244+
isLoading={isLoading}
245+
statusBarData={statusBarData}
246+
openEnableHighlightsModal={openEnableHighlightsModal}
247+
handleVideoSharingOptionChange={handleVideoSharingOptionChange}
248+
/>
264249
<hr className="mt-4 mb-0 w-100 text-light-400" />
265250
<div className="d-flex align-items-baseline">
266251
<div className="flex-fill">
267252
<article>
268253
<div>
269-
{showNewActionsBar && (
270-
<ActionRow className="mt-3">
271-
{Boolean(sections.length) && (
272-
<Button
273-
variant="outline-primary"
274-
id="expand-collapse-all-button"
275-
data-testid="expand-collapse-all-button"
276-
iconBefore={isSectionsExpanded ? CloseFullscreen : OpenInFull}
277-
onClick={headerNavigationsActions.handleExpandAll}
278-
>
279-
{isSectionsExpanded
280-
? intl.formatMessage(headerMessages.collapseAllButton)
281-
: intl.formatMessage(headerMessages.expandAllButton)}
282-
</Button>
283-
)}
284-
</ActionRow>
285-
)}
254+
<ActionRow className="mt-3">
255+
{Boolean(sections.length) && (
256+
<Button
257+
variant="outline-primary"
258+
id="expand-collapse-all-button"
259+
data-testid="expand-collapse-all-button"
260+
iconBefore={isSectionsExpanded ? CloseFullscreen : OpenInFull}
261+
onClick={headerNavigationsActions.handleExpandAll}
262+
>
263+
{isSectionsExpanded
264+
? intl.formatMessage(headerMessages.collapseAllButton)
265+
: intl.formatMessage(headerMessages.expandAllButton)}
266+
</Button>
267+
)}
268+
</ActionRow>
286269
<section>
287270
{!errors?.outlineIndexApi && (
288271
<div className="pt-4">

src/course-outline/OutlineAddChildButtons.test.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import userEvent from '@testing-library/user-event';
2-
import { getConfig, setConfig } from '@edx/frontend-platform';
32
import { ContainerType } from '@src/generic/key-utils';
43
import {
54
initializeMocks,
@@ -59,19 +58,13 @@ jest.mock('@src/course-outline/outline-sidebar/OutlineSidebarContext', () => ({
5958
describe(`<OutlineAddChildButtons> for ${containerType}`, () => {
6059
beforeEach(() => {
6160
initializeMocks();
62-
setConfig({
63-
...getConfig(),
64-
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
65-
});
6661
});
6762

6863
it('renders and behaves correctly', async () => {
6964
const newClickHandler = jest.fn();
70-
const useFromLibClickHandler = jest.fn();
7165
render(
7266
<OutlineAddChildButtons
7367
handleNewButtonClick={newClickHandler}
74-
handleUseFromLibraryClick={useFromLibClickHandler}
7568
childType={containerType}
7669
parentLocator=""
7770
/>,
@@ -85,7 +78,11 @@ jest.mock('@src/course-outline/outline-sidebar/OutlineSidebarContext', () => ({
8578
await userEvent.click(newBtn);
8679
await waitFor(() => expect(newClickHandler).toHaveBeenCalled());
8780
await userEvent.click(useBtn);
88-
await waitFor(() => expect(useFromLibClickHandler).toHaveBeenCalled());
81+
82+
expect(startCurrentFlow).toHaveBeenCalledWith({
83+
flowType: containerType,
84+
parentLocator: '',
85+
});
8986
});
9087

9188
it('calls appropriate new handlers', async () => {

0 commit comments

Comments
 (0)