Skip to content

Commit 55fe87a

Browse files
authored
feat: show problem bank component picker on window msg [FC-0062] (#1522)
Fix for: If you have a unit with many components and a problem bank on the NEW MFE unit page (with an iframe), clicking "Add Components" will open a modal that's way too tall.
1 parent 7aa5acc commit 55fe87a

File tree

4 files changed

+159
-19
lines changed

4 files changed

+159
-19
lines changed

src/course-unit/add-component/AddComponent.jsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
import { useCallback, useState } from 'react';
12
import PropTypes from 'prop-types';
23
import { useSelector } from 'react-redux';
34
import { useNavigate } from 'react-router-dom';
4-
import { useIntl } from '@edx/frontend-platform/i18n';
5-
import { StandardModal, useToggle } from '@openedx/paragon';
5+
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
6+
import {
7+
ActionRow, Button, StandardModal, useToggle,
8+
} from '@openedx/paragon';
69

710
import { getCourseSectionVertical } from '../data/selectors';
811
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
912
import ComponentModalView from './add-component-modals/ComponentModalView';
1013
import AddComponentButton from './add-component-btn';
1114
import messages from './messages';
1215
import { ComponentPicker } from '../../library-authoring/component-picker';
16+
import { messageTypes } from '../constants';
17+
import { useIframe } from '../context/hooks';
18+
import { useEventListener } from '../../generic/hooks';
1319

1420
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
1521
const navigate = useNavigate();
@@ -19,16 +25,32 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
1925
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
2026
const { componentTemplates } = useSelector(getCourseSectionVertical);
2127
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
28+
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
29+
const [selectedComponents, setSelectedComponents] = useState([]);
30+
const { sendMessageToIframe } = useIframe();
2231

23-
const handleLibraryV2Selection = (selection) => {
32+
const receiveMessage = useCallback(({ data: { type } }) => {
33+
if (type === messageTypes.showMultipleComponentPicker) {
34+
showSelectLibraryContentModal();
35+
}
36+
}, [showSelectLibraryContentModal]);
37+
38+
useEventListener('message', receiveMessage);
39+
40+
const onComponentSelectionSubmit = useCallback(() => {
41+
sendMessageToIframe(messageTypes.addSelectedComponentsToBank, { selectedComponents });
42+
closeSelectLibraryContentModal();
43+
}, [selectedComponents]);
44+
45+
const handleLibraryV2Selection = useCallback((selection) => {
2446
handleCreateNewCourseXBlock({
2547
type: COMPONENT_TYPES.libraryV2,
2648
category: selection.blockType,
2749
parentLocator: blockId,
2850
libraryContentKey: selection.usageKey,
2951
});
3052
closeAddLibraryContentModal();
31-
};
53+
}, []);
3254

3355
const handleCreateNewXBlock = (type, moduleName) => {
3456
switch (type) {
@@ -138,15 +160,33 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
138160
})}
139161
</ul>
140162
<StandardModal
141-
title="Select component"
142-
isOpen={isAddLibraryContentModalOpen}
143-
onClose={closeAddLibraryContentModal}
163+
title={
164+
isAddLibraryContentModalOpen
165+
? intl.formatMessage(messages.singleComponentPickerModalTitle)
166+
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
167+
}
168+
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
169+
onClose={() => {
170+
closeAddLibraryContentModal();
171+
closeSelectLibraryContentModal();
172+
}}
144173
isOverflowVisible={false}
145174
size="xl"
175+
footerNode={
176+
isSelectLibraryContentModalOpen && (
177+
<ActionRow>
178+
<Button variant="primary" onClick={onComponentSelectionSubmit}>
179+
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
180+
</Button>
181+
</ActionRow>
182+
)
183+
}
146184
>
147185
<ComponentPicker
148186
showOnlyPublished
187+
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
149188
onComponentSelected={handleLibraryV2Selection}
189+
onChangeComponentSelection={setSelectedComponents}
150190
/>
151191
</StandardModal>
152192
</div>

src/course-unit/add-component/AddComponent.test.jsx

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
/* eslint-disable react/prop-types */
12
import MockAdapter from 'axios-mock-adapter';
23
import {
3-
render, waitFor, within,
4+
act, render, screen, waitFor, within,
45
} from '@testing-library/react';
56
import userEvent from '@testing-library/user-event';
67

@@ -17,25 +18,56 @@ import { courseSectionVerticalMock } from '../__mocks__';
1718
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
1819
import AddComponent from './AddComponent';
1920
import messages from './messages';
21+
import { IframeProvider } from '../context/iFrameContext';
22+
import { messageTypes } from '../constants';
2023

2124
let store;
2225
let axiosMock;
2326
const blockId = '123';
2427
const handleCreateNewCourseXBlockMock = jest.fn();
28+
const usageKey = 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fddest-usage-key';
2529

26-
// Mock ComponentPicker to call onComponentSelected on load
30+
// Mock ComponentPicker to call onComponentSelected on click
2731
jest.mock('../../library-authoring/component-picker', () => ({
28-
ComponentPicker: (props) => props.onComponentSelected({ usageKey: 'test-usage-key', blockType: 'html' }),
32+
ComponentPicker: (props) => {
33+
const onClick = () => {
34+
if (props.componentPickerMode === 'single') {
35+
props.onComponentSelected({
36+
usageKey,
37+
blockType: 'html',
38+
});
39+
} else {
40+
props.onChangeComponentSelection([{
41+
usageKey,
42+
blockType: 'html',
43+
}]);
44+
}
45+
};
46+
return (
47+
<button type="submit" onClick={onClick}>
48+
Dummy button
49+
</button>
50+
);
51+
},
52+
}));
53+
54+
const mockSendMessageToIframe = jest.fn();
55+
jest.mock('../context/hooks', () => ({
56+
useIframe: () => ({
57+
sendMessageToIframe: mockSendMessageToIframe,
58+
}),
2959
}));
3060

3161
const renderComponent = (props) => render(
3262
<AppProvider store={store}>
3363
<IntlProvider locale="en">
34-
<AddComponent
35-
blockId={blockId}
36-
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
37-
{...props}
38-
/>
64+
<IframeProvider>
65+
<AddComponent
66+
blockId={blockId}
67+
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
68+
{...props}
69+
/>
70+
</IframeProvider>
3971
</IntlProvider>
4072
</AppProvider>,
4173
);
@@ -413,18 +445,64 @@ describe('<AddComponent />', () => {
413445
});
414446

415447
it('shows library picker on clicking v2 library content btn', async () => {
416-
const { findByRole } = renderComponent();
417-
const libBtn = await findByRole('button', {
448+
renderComponent();
449+
const libBtn = await screen.findByRole('button', {
418450
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
419451
});
420-
421452
userEvent.click(libBtn);
453+
454+
// click dummy button to execute onComponentSelected prop.
455+
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
456+
userEvent.click(dummyBtn);
457+
422458
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
423459
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
424460
type: COMPONENT_TYPES.libraryV2,
425461
parentLocator: '123',
426462
category: 'html',
427-
libraryContentKey: 'test-usage-key',
463+
libraryContentKey: usageKey,
464+
});
465+
});
466+
467+
it('closes library component picker on close', async () => {
468+
renderComponent();
469+
const libBtn = await screen.findByRole('button', {
470+
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
471+
});
472+
userEvent.click(libBtn);
473+
474+
expect(screen.queryByRole('button', { name: 'Dummy button' })).toBeInTheDocument();
475+
// click dummy button to execute onComponentSelected prop.
476+
const closeBtn = await screen.findByRole('button', { name: 'Close' });
477+
userEvent.click(closeBtn);
478+
479+
expect(screen.queryByRole('button', { name: 'Dummy button' })).not.toBeInTheDocument();
480+
});
481+
482+
it('shows component picker on window message', async () => {
483+
renderComponent();
484+
const message = {
485+
data: {
486+
type: messageTypes.showMultipleComponentPicker,
487+
},
488+
};
489+
// Dispatch showMultipleComponentPicker message event to open the picker modal.
490+
act(() => {
491+
window.dispatchEvent(new MessageEvent('message', message));
492+
});
493+
494+
// click dummy button to execute onChangeComponentSelection prop.
495+
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
496+
userEvent.click(dummyBtn);
497+
498+
const submitBtn = await screen.findByRole('button', { name: 'Add selected components' });
499+
userEvent.click(submitBtn);
500+
501+
expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.addSelectedComponentsToBank, {
502+
selectedComponents: [{
503+
blockType: 'html',
504+
usageKey,
505+
}],
428506
});
429507
});
430508

src/course-unit/add-component/messages.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,42 @@ const messages = defineMessages({
44
title: {
55
id: 'course-authoring.course-unit.add.component.title',
66
defaultMessage: 'Add a new component',
7+
description: 'Title text for add component section in course unit.',
78
},
89
buttonText: {
910
id: 'course-authoring.course-unit.add.component.button.text',
1011
defaultMessage: 'Add Component:',
12+
description: 'Information text for screen-readers about each add component button',
1113
},
1214
modalBtnText: {
1315
id: 'course-authoring.course-unit.modal.button.text',
1416
defaultMessage: 'Select',
17+
description: 'Information text for screen-readers about each add component button',
18+
},
19+
singleComponentPickerModalTitle: {
20+
id: 'course-authoring.course-unit.modal.single-title.text',
21+
defaultMessage: 'Select component',
22+
description: 'Library content picker modal title.',
23+
},
24+
multipleComponentPickerModalTitle: {
25+
id: 'course-authoring.course-unit.modal.multiple-title.text',
26+
defaultMessage: 'Select components',
27+
description: 'Problem bank component picker modal title.',
28+
},
29+
multipleComponentPickerModalBtn: {
30+
id: 'course-authoring.course-unit.modal.multiple-btn.text',
31+
defaultMessage: 'Add selected components',
32+
description: 'Problem bank component add button text.',
1533
},
1634
modalContainerTitle: {
1735
id: 'course-authoring.course-unit.modal.container.title',
1836
defaultMessage: 'Add {componentTitle} component',
37+
description: 'Modal title for adding components',
1938
},
2039
modalContainerCancelBtnText: {
2140
id: 'course-authoring.course-unit.modal.container.cancel.button.text',
2241
defaultMessage: 'Cancel',
42+
description: 'Modal cancel button text.',
2343
},
2444
modalComponentSupportLabelFullySupported: {
2545
id: 'course-authoring.course-unit.modal.component.support.label.fully-supported',

src/course-unit/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export const messageTypes = {
5252
videoFullScreen: 'plugin.videoFullScreen',
5353
refreshXBlock: 'refreshXBlock',
5454
showMoveXBlockModal: 'showMoveXBlockModal',
55+
showMultipleComponentPicker: 'showMultipleComponentPicker',
56+
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
5557
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
5658
};
5759

0 commit comments

Comments
 (0)