diff --git a/src/course-unit/add-component/AddComponent.jsx b/src/course-unit/add-component/AddComponent.jsx
index 1a0676239c..70962a7ac6 100644
--- a/src/course-unit/add-component/AddComponent.jsx
+++ b/src/course-unit/add-component/AddComponent.jsx
@@ -1,8 +1,11 @@
+import { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
-import { useIntl } from '@edx/frontend-platform/i18n';
-import { StandardModal, useToggle } from '@openedx/paragon';
+import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
+import {
+ ActionRow, Button, StandardModal, useToggle,
+} from '@openedx/paragon';
import { getCourseSectionVertical } from '../data/selectors';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
@@ -10,6 +13,9 @@ import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn';
import messages from './messages';
import { ComponentPicker } from '../../library-authoring/component-picker';
+import { messageTypes } from '../constants';
+import { useIframe } from '../context/hooks';
+import { useEventListener } from '../../generic/hooks';
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
const navigate = useNavigate();
@@ -19,8 +25,24 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates } = useSelector(getCourseSectionVertical);
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
+ const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
+ const [selectedComponents, setSelectedComponents] = useState([]);
+ const { sendMessageToIframe } = useIframe();
- const handleLibraryV2Selection = (selection) => {
+ const receiveMessage = useCallback(({ data: { type } }) => {
+ if (type === messageTypes.showMultipleComponentPicker) {
+ showSelectLibraryContentModal();
+ }
+ }, [showSelectLibraryContentModal]);
+
+ useEventListener('message', receiveMessage);
+
+ const onComponentSelectionSubmit = useCallback(() => {
+ sendMessageToIframe(messageTypes.addSelectedComponentsToBank, { selectedComponents });
+ closeSelectLibraryContentModal();
+ }, [selectedComponents]);
+
+ const handleLibraryV2Selection = useCallback((selection) => {
handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2,
category: selection.blockType,
@@ -28,7 +50,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
libraryContentKey: selection.usageKey,
});
closeAddLibraryContentModal();
- };
+ }, []);
const handleCreateNewXBlock = (type, moduleName) => {
switch (type) {
@@ -138,15 +160,33 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
})}
{
+ closeAddLibraryContentModal();
+ closeSelectLibraryContentModal();
+ }}
isOverflowVisible={false}
size="xl"
+ footerNode={
+ isSelectLibraryContentModalOpen && (
+
+
+
+ )
+ }
>
diff --git a/src/course-unit/add-component/AddComponent.test.jsx b/src/course-unit/add-component/AddComponent.test.jsx
index 9cc27acf55..0971c20088 100644
--- a/src/course-unit/add-component/AddComponent.test.jsx
+++ b/src/course-unit/add-component/AddComponent.test.jsx
@@ -1,6 +1,7 @@
+/* eslint-disable react/prop-types */
import MockAdapter from 'axios-mock-adapter';
import {
- render, waitFor, within,
+ act, render, screen, waitFor, within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -17,25 +18,56 @@ import { courseSectionVerticalMock } from '../__mocks__';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import AddComponent from './AddComponent';
import messages from './messages';
+import { IframeProvider } from '../context/iFrameContext';
+import { messageTypes } from '../constants';
let store;
let axiosMock;
const blockId = '123';
const handleCreateNewCourseXBlockMock = jest.fn();
+const usageKey = 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fddest-usage-key';
-// Mock ComponentPicker to call onComponentSelected on load
+// Mock ComponentPicker to call onComponentSelected on click
jest.mock('../../library-authoring/component-picker', () => ({
- ComponentPicker: (props) => props.onComponentSelected({ usageKey: 'test-usage-key', blockType: 'html' }),
+ ComponentPicker: (props) => {
+ const onClick = () => {
+ if (props.componentPickerMode === 'single') {
+ props.onComponentSelected({
+ usageKey,
+ blockType: 'html',
+ });
+ } else {
+ props.onChangeComponentSelection([{
+ usageKey,
+ blockType: 'html',
+ }]);
+ }
+ };
+ return (
+
+ );
+ },
+}));
+
+const mockSendMessageToIframe = jest.fn();
+jest.mock('../context/hooks', () => ({
+ useIframe: () => ({
+ sendMessageToIframe: mockSendMessageToIframe,
+ }),
}));
const renderComponent = (props) => render(
-
+
+
+
,
);
@@ -413,18 +445,64 @@ describe('', () => {
});
it('shows library picker on clicking v2 library content btn', async () => {
- const { findByRole } = renderComponent();
- const libBtn = await findByRole('button', {
+ renderComponent();
+ const libBtn = await screen.findByRole('button', {
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
});
-
userEvent.click(libBtn);
+
+ // click dummy button to execute onComponentSelected prop.
+ const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
+ userEvent.click(dummyBtn);
+
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
type: COMPONENT_TYPES.libraryV2,
parentLocator: '123',
category: 'html',
- libraryContentKey: 'test-usage-key',
+ libraryContentKey: usageKey,
+ });
+ });
+
+ it('closes library component picker on close', async () => {
+ renderComponent();
+ const libBtn = await screen.findByRole('button', {
+ name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
+ });
+ userEvent.click(libBtn);
+
+ expect(screen.queryByRole('button', { name: 'Dummy button' })).toBeInTheDocument();
+ // click dummy button to execute onComponentSelected prop.
+ const closeBtn = await screen.findByRole('button', { name: 'Close' });
+ userEvent.click(closeBtn);
+
+ expect(screen.queryByRole('button', { name: 'Dummy button' })).not.toBeInTheDocument();
+ });
+
+ it('shows component picker on window message', async () => {
+ renderComponent();
+ const message = {
+ data: {
+ type: messageTypes.showMultipleComponentPicker,
+ },
+ };
+ // Dispatch showMultipleComponentPicker message event to open the picker modal.
+ act(() => {
+ window.dispatchEvent(new MessageEvent('message', message));
+ });
+
+ // click dummy button to execute onChangeComponentSelection prop.
+ const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
+ userEvent.click(dummyBtn);
+
+ const submitBtn = await screen.findByRole('button', { name: 'Add selected components' });
+ userEvent.click(submitBtn);
+
+ expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.addSelectedComponentsToBank, {
+ selectedComponents: [{
+ blockType: 'html',
+ usageKey,
+ }],
});
});
diff --git a/src/course-unit/add-component/messages.js b/src/course-unit/add-component/messages.js
index bc6c309833..71fdae52a1 100644
--- a/src/course-unit/add-component/messages.js
+++ b/src/course-unit/add-component/messages.js
@@ -4,22 +4,42 @@ const messages = defineMessages({
title: {
id: 'course-authoring.course-unit.add.component.title',
defaultMessage: 'Add a new component',
+ description: 'Title text for add component section in course unit.',
},
buttonText: {
id: 'course-authoring.course-unit.add.component.button.text',
defaultMessage: 'Add Component:',
+ description: 'Information text for screen-readers about each add component button',
},
modalBtnText: {
id: 'course-authoring.course-unit.modal.button.text',
defaultMessage: 'Select',
+ description: 'Information text for screen-readers about each add component button',
+ },
+ singleComponentPickerModalTitle: {
+ id: 'course-authoring.course-unit.modal.single-title.text',
+ defaultMessage: 'Select component',
+ description: 'Library content picker modal title.',
+ },
+ multipleComponentPickerModalTitle: {
+ id: 'course-authoring.course-unit.modal.multiple-title.text',
+ defaultMessage: 'Select components',
+ description: 'Problem bank component picker modal title.',
+ },
+ multipleComponentPickerModalBtn: {
+ id: 'course-authoring.course-unit.modal.multiple-btn.text',
+ defaultMessage: 'Add selected components',
+ description: 'Problem bank component add button text.',
},
modalContainerTitle: {
id: 'course-authoring.course-unit.modal.container.title',
defaultMessage: 'Add {componentTitle} component',
+ description: 'Modal title for adding components',
},
modalContainerCancelBtnText: {
id: 'course-authoring.course-unit.modal.container.cancel.button.text',
defaultMessage: 'Cancel',
+ description: 'Modal cancel button text.',
},
modalComponentSupportLabelFullySupported: {
id: 'course-authoring.course-unit.modal.component.support.label.fully-supported',
diff --git a/src/course-unit/constants.js b/src/course-unit/constants.js
index f63490530b..129fa55d9b 100644
--- a/src/course-unit/constants.js
+++ b/src/course-unit/constants.js
@@ -52,6 +52,8 @@ export const messageTypes = {
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
+ showMultipleComponentPicker: 'showMultipleComponentPicker',
+ addSelectedComponentsToBank: 'addSelectedComponentsToBank',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
};