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:
Navin Karkera
2024-11-23 01:59:18 +05:30
committed by GitHub
parent 7aa5accdbb
commit 55fe87a3db
4 changed files with 159 additions and 19 deletions

View File

@@ -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>

View File

@@ -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,
}],
});
});

View File

@@ -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',

View File

@@ -52,6 +52,8 @@ export const messageTypes = {
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
showMultipleComponentPicker: 'showMultipleComponentPicker',
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
};