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.
This commit is contained in:
@@ -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 }) => {
|
||||
})}
|
||||
</ul>
|
||||
<StandardModal
|
||||
title="Select component"
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
title={
|
||||
isAddLibraryContentModalOpen
|
||||
? intl.formatMessage(messages.singleComponentPickerModalTitle)
|
||||
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
|
||||
}
|
||||
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
|
||||
onClose={() => {
|
||||
closeAddLibraryContentModal();
|
||||
closeSelectLibraryContentModal();
|
||||
}}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
footerNode={
|
||||
isSelectLibraryContentModalOpen && (
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onComponentSelectionSubmit}>
|
||||
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
|
||||
onComponentSelected={handleLibraryV2Selection}
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<button type="submit" onClick={onClick}>
|
||||
Dummy button
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSendMessageToIframe = jest.fn();
|
||||
jest.mock('../context/hooks', () => ({
|
||||
useIframe: () => ({
|
||||
sendMessageToIframe: mockSendMessageToIframe,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
<IframeProvider>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
</IframeProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
@@ -413,18 +445,64 @@ describe('<AddComponent />', () => {
|
||||
});
|
||||
|
||||
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,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -52,6 +52,8 @@ export const messageTypes = {
|
||||
videoFullScreen: 'plugin.videoFullScreen',
|
||||
refreshXBlock: 'refreshXBlock',
|
||||
showMoveXBlockModal: 'showMoveXBlockModal',
|
||||
showMultipleComponentPicker: 'showMultipleComponentPicker',
|
||||
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
|
||||
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user