From 55fe87a3db7bd7d1db9bf6e8fbaf23ccf3ccefab Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Sat, 23 Nov 2024 01:59:18 +0530 Subject: [PATCH] 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. --- .../add-component/AddComponent.jsx | 54 ++++++++-- .../add-component/AddComponent.test.jsx | 102 +++++++++++++++--- src/course-unit/add-component/messages.js | 20 ++++ src/course-unit/constants.js | 2 + 4 files changed, 159 insertions(+), 19 deletions(-) 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', };