Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show problem bank component picker on window msg [FC-0062] #1522

Merged
merged 6 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
ChrisChV marked this conversation as resolved.
Show resolved Hide resolved
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',
};

export const IFRAME_FEATURE_POLICY = (
Expand Down