Skip to content

Commit

Permalink
feat: show problem bank component picker on window msg [FC-0062] (#1522)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
navinkarkera authored Nov 22, 2024
1 parent 7aa5acc commit 55fe87a
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 19 deletions.
54 changes: 47 additions & 7 deletions src/course-unit/add-component/AddComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
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';
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();
Expand All @@ -19,16 +25,32 @@ 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,
parentLocator: blockId,
libraryContentKey: selection.usageKey,
});
closeAddLibraryContentModal();
};
}, []);

const handleCreateNewXBlock = (type, moduleName) => {
switch (type) {
Expand Down Expand Up @@ -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>
Expand Down
102 changes: 90 additions & 12 deletions src/course-unit/add-component/AddComponent.test.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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>,
);
Expand Down Expand Up @@ -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,
}],
});
});

Expand Down
20 changes: 20 additions & 0 deletions src/course-unit/add-component/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/course-unit/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export const messageTypes = {
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
showMultipleComponentPicker: 'showMultipleComponentPicker',
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
};

Expand Down

0 comments on commit 55fe87a

Please sign in to comment.