Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
62 changes: 16 additions & 46 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,7 @@ describe('<CourseUnit />', () => {
});
});

it('handle creating Problem xblock and navigate to editor page', async () => {
const { courseKey, locator } = courseCreateXblockMock;
it('handle creating Problem xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
Expand Down Expand Up @@ -701,11 +700,16 @@ describe('<CourseUnit />', () => {
await waitFor(() => {
const problemButton = getByRole('button', {
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
hidden: true,
});

userEvent.click(problemButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
});

await waitFor(() => {
expect(getByRole('heading', {
name: new RegExp(`${addComponentMessages.blockEditorModalTitle.defaultMessage}`, 'i'),
})).toBeInTheDocument();
});

axiosMock
Expand Down Expand Up @@ -735,44 +739,6 @@ describe('<CourseUnit />', () => {
)).toBeInTheDocument();
});

it('handle creating Text xblock and saves scroll position in localStorage', async () => {
const { getByText, getByRole } = render(<RootWrapper />);
const xblockType = 'text';

axiosMock
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);

window.scrollTo(0, 250);
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });

await waitFor(() => {
const textButton = screen.getByRole('button', { name: /Text/i });

expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();

userEvent.click(textButton);

const addXBlockDialog = getByRole('dialog');
expect(addXBlockDialog).toBeInTheDocument();

expect(getByText(
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
)).toBeInTheDocument();

const textRadio = screen.getByRole('radio', { name: /Text/i });
userEvent.click(textRadio);
expect(textRadio).toBeChecked();

const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
expect(selectBtn).toBeInTheDocument();

userEvent.click(selectBtn);
});

expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
});

it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
const { getByRole, getAllByTestId } = render(<RootWrapper />);
let units = null;
Expand Down Expand Up @@ -863,8 +829,7 @@ describe('<CourseUnit />', () => {
});
});

it('handles creating Video xblock and navigates to editor page', async () => {
const { courseKey, locator } = courseCreateXblockMock;
it('handles creating Video xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
Expand Down Expand Up @@ -902,13 +867,18 @@ describe('<CourseUnit />', () => {

const videoButton = getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});

userEvent.click(videoButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
});

/** TODO -- fix this test.
await waitFor(() => {
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
});
*/

axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
Expand Down
49 changes: 41 additions & 8 deletions src/course-unit/add-component/AddComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, StandardModal, useToggle,
Expand All @@ -16,6 +16,7 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useEventListener } from '../../generic/hooks';
import EditorPage from '../../editors/EditorPage';

const AddComponent = ({
parentLocator,
Expand All @@ -24,14 +25,18 @@ const AddComponent = ({
addComponentTemplateData,
handleCreateNewCourseXBlock,
}) => {
const navigate = useNavigate();
const intl = useIntl();
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();

const [blockType, setBlockType] = useState(null);
const [courseId, setCourseId] = useState(null);
const [newBlockId, setNewBlockId] = useState(null);
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]);
const [usageId, setUsageId] = useState(null);
Expand All @@ -54,6 +59,11 @@ const AddComponent = ({
closeSelectLibraryContentModal();
}, [selectedComponents]);

const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, sendMessageToIframe]);

const handleLibraryV2Selection = useCallback((selection) => {
handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2,
Expand All @@ -70,11 +80,13 @@ const AddComponent = ({
case COMPONENT_TYPES.dragAndDrop:
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break;
case COMPONENT_TYPES.problem:
case COMPONENT_TYPES.video:
case COMPONENT_TYPES.problem:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
showXBlockEditorModal();
});
break;
// TODO: The library functional will be a bit different of current legacy (CMS)
Expand All @@ -99,9 +111,11 @@ const AddComponent = ({
type,
boilerplate: moduleName,
parentLocator: blockId,
}, ({ courseKey, locator }) => {
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
navigate(`/course/${courseKey}/editor/html/${locator}`);
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
showXBlockEditorModal();
});
break;
default:
Expand Down Expand Up @@ -201,6 +215,25 @@ const AddComponent = ({
onChangeComponentSelection={setSelectedComponents}
/>
</StandardModal>
<StandardModal
title={intl.formatMessage(messages.blockEditorModalTitle)}
isOpen={isXBlockEditorModalOpen}
onClose={closeXBlockEditorModal}
isOverflowVisible={false}
size="xl"
>
<div className="editor-page">
<EditorPage
courseId={courseId}
blockType={blockType}
blockId={newBlockId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={closeXBlockEditorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
</div>
);
}
Expand Down
10 changes: 10 additions & 0 deletions src/course-unit/add-component/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ const messages = defineMessages({
defaultMessage: 'Add selected components',
description: 'Problem bank component add button text.',
},
videoPickerModalTitle: {
id: 'course-authoring.course-unit.modal.video-title.text',
defaultMessage: 'Select video',
description: 'Video picker modal title.',
},
blockEditorModalTitle: {
id: 'course-authoring.course-unit.modal.block-editor-title.text',
defaultMessage: 'Edit component',
description: 'Block editor modal title.',
},
modalContainerTitle: {
id: 'course-authoring.course-unit.modal.container.title',
defaultMessage: 'Add {componentTitle} component',
Expand Down
3 changes: 1 addition & 2 deletions src/course-unit/xblock-container-iframe/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export type UseMessageHandlersTypes = {
courseId: string;
navigate: (path: string) => void;
dispatch: (action: any) => void;
setIframeOffset: (height: number) => void;
handleDeleteXBlock: (usageId: string) => void;
handleScrollToXBlock: (scrollOffset: number) => void;
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
handleEditXBlock: (blockType: string, usageId: string) => void;
handleManageXBlockAccess: (usageId: string) => void;
handleShowLegacyEditXBlockModal: (id: string) => void;
handleCloseLegacyEditorXBlockModal: () => void;
Expand All @@ -14,7 +14,6 @@ export type UseMessageHandlersTypes = {
handleOpenManageTagsModal: (id: string) => void;
handleShowProcessingNotification: (variant: string) => void;
handleHideProcessingNotification: () => void;
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
};

export type MessageHandlersTypes = Record<string, (payload: any) => void>;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
*/
export const useMessageHandlers = ({
courseId,
navigate,
dispatch,
setIframeOffset,
handleDeleteXBlock,
Expand All @@ -30,14 +29,14 @@ export const useMessageHandlers = ({
handleOpenManageTagsModal,
handleShowProcessingNotification,
handleHideProcessingNotification,
handleRedirectToXBlockEditPage,
handleEditXBlock,
}: UseMessageHandlersTypes): MessageHandlersTypes => {
const { copyToClipboard } = useClipboard();

return useMemo(() => ({
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
Expand All @@ -52,9 +51,14 @@ export const useMessageHandlers = ({
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
[messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
[messageTypes.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification(
NOTIFICATION_MESSAGES.copying,
),
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock(
payload.type,
payload.locator,
),
}), [
courseId,
handleDeleteXBlock,
Expand Down
51 changes: 41 additions & 10 deletions src/course-unit/xblock-container-iframe/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getConfig } from '@edx/frontend-platform';
import {
FC, useEffect, useState, useMemo, useCallback,
} from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle, Sheet } from '@openedx/paragon';
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';

Expand Down Expand Up @@ -35,6 +36,7 @@ import messages from './messages';
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
import { useIframeContent } from '../../generic/hooks/useIframeContent';
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
import EditorPage from '../../editors/EditorPage';

const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
Expand All @@ -45,6 +47,9 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({

const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState<string>('');
const [newBlockId, setNewBlockId] = useState<string>('');
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
const [iframeOffset, setIframeOffset] = useState(0);
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
Expand All @@ -64,11 +69,23 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
setIframeRef(iframeRef);
}, [setIframeRef]);

const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, sendMessageToIframe]);

const handleEditXBlock = useCallback((type: string, id: string) => {
setBlockType(type);
setNewBlockId(id);
showXBlockEditorModal();
}, [showXBlockEditorModal]);

const handleDuplicateXBlock = useCallback(
(blockType: string, usageId: string) => {
(type: string, usageId: string) => {
unitXBlockActions.handleDuplicate(usageId);
if (supportedEditors[blockType]) {
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
if (supportedEditors[type]) {
// istanbul ignore next
handleEditXBlock(type, usageId);
}
},
[unitXBlockActions, courseId, navigate],
Expand Down Expand Up @@ -147,13 +164,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
dispatch(hideProcessingNotification());
};

const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
};

const messageHandlers = useMessageHandlers({
courseId,
navigate,
dispatch,
setIframeOffset,
handleDeleteXBlock,
Expand All @@ -167,7 +179,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
handleOpenManageTagsModal,
handleShowProcessingNotification,
handleHideProcessingNotification,
handleRedirectToXBlockEditPage,
handleEditXBlock,
});

useIframeMessages(messageHandlers);
Expand All @@ -186,6 +198,25 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
close={closeDeleteModal}
onDeleteSubmit={onDeleteSubmit}
/>
<StandardModal
title={intl.formatMessage(messages.blockEditorModalTitle)}
isOpen={isXBlockEditorModalOpen}
onClose={closeXBlockEditorModal}
isOverflowVisible={false}
size="xl"
>
<div className="editor-page">
<EditorPage
courseId={courseId}
blockType={blockType}
blockId={newBlockId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={closeXBlockEditorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
{Object.keys(accessManagedXBlockData).length ? (
<ConfigureModal
isXBlockComponent
Expand Down
Loading