Skip to content

Commit dbb1a99

Browse files
authored
feat: display editors as modals (#1838)
1 parent b30a1c8 commit dbb1a99

File tree

19 files changed

+146
-152
lines changed

19 files changed

+146
-152
lines changed

src/course-unit/CourseUnit.test.jsx

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,7 @@ describe('<CourseUnit />', () => {
671671
});
672672
});
673673

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

706706
userEvent.click(problemButton);
707-
expect(mockedUsedNavigate).toHaveBeenCalled();
708-
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
707+
});
708+
709+
await waitFor(() => {
710+
expect(getByRole('heading', {
711+
name: new RegExp(`${addComponentMessages.blockEditorModalTitle.defaultMessage}`, 'i'),
712+
})).toBeInTheDocument();
709713
});
710714

711715
axiosMock
@@ -735,44 +739,6 @@ describe('<CourseUnit />', () => {
735739
)).toBeInTheDocument();
736740
});
737741

738-
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
739-
const { getByText, getByRole } = render(<RootWrapper />);
740-
const xblockType = 'text';
741-
742-
axiosMock
743-
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
744-
.reply(200, courseCreateXblockMock);
745-
746-
window.scrollTo(0, 250);
747-
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
748-
749-
await waitFor(() => {
750-
const textButton = screen.getByRole('button', { name: /Text/i });
751-
752-
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
753-
754-
userEvent.click(textButton);
755-
756-
const addXBlockDialog = getByRole('dialog');
757-
expect(addXBlockDialog).toBeInTheDocument();
758-
759-
expect(getByText(
760-
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
761-
)).toBeInTheDocument();
762-
763-
const textRadio = screen.getByRole('radio', { name: /Text/i });
764-
userEvent.click(textRadio);
765-
expect(textRadio).toBeChecked();
766-
767-
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
768-
expect(selectBtn).toBeInTheDocument();
769-
770-
userEvent.click(selectBtn);
771-
});
772-
773-
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
774-
});
775-
776742
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
777743
const { getByRole, getAllByTestId } = render(<RootWrapper />);
778744
let units = null;
@@ -863,8 +829,7 @@ describe('<CourseUnit />', () => {
863829
});
864830
});
865831

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

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

907873
userEvent.click(videoButton);
908-
expect(mockedUsedNavigate).toHaveBeenCalled();
909-
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
910874
});
911875

876+
/** TODO -- fix this test.
877+
await waitFor(() => {
878+
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
879+
});
880+
*/
881+
912882
axiosMock
913883
.onGet(getCourseUnitApiUrl(blockId))
914884
.reply(200, courseUnitIndexMock);

src/course-unit/add-component/AddComponent.jsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useState } from 'react';
22
import PropTypes from 'prop-types';
33
import { useSelector } from 'react-redux';
4-
import { useNavigate } from 'react-router-dom';
4+
import { getConfig } from '@edx/frontend-platform';
55
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
66
import {
77
ActionRow, Button, StandardModal, useToggle,
@@ -16,6 +16,7 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
1616
import { messageTypes } from '../constants';
1717
import { useIframe } from '../../generic/hooks/context/hooks';
1818
import { useEventListener } from '../../generic/hooks';
19+
import EditorPage from '../../editors/EditorPage';
1920

2021
const AddComponent = ({
2122
parentLocator,
@@ -24,14 +25,18 @@ const AddComponent = ({
2425
addComponentTemplateData,
2526
handleCreateNewCourseXBlock,
2627
}) => {
27-
const navigate = useNavigate();
2828
const intl = useIntl();
2929
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
3030
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
3131
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
3232
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
3333
const blockId = addComponentTemplateData.parentLocator || parentLocator;
3434
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
35+
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
36+
37+
const [blockType, setBlockType] = useState(null);
38+
const [courseId, setCourseId] = useState(null);
39+
const [newBlockId, setNewBlockId] = useState(null);
3540
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
3641
const [selectedComponents, setSelectedComponents] = useState([]);
3742
const [usageId, setUsageId] = useState(null);
@@ -54,6 +59,11 @@ const AddComponent = ({
5459
closeSelectLibraryContentModal();
5560
}, [selectedComponents]);
5661

62+
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
63+
closeXBlockEditorModal();
64+
sendMessageToIframe(messageTypes.refreshXBlock, null);
65+
}, [closeXBlockEditorModal, sendMessageToIframe]);
66+
5767
const handleLibraryV2Selection = useCallback((selection) => {
5868
handleCreateNewCourseXBlock({
5969
type: COMPONENT_TYPES.libraryV2,
@@ -70,11 +80,13 @@ const AddComponent = ({
7080
case COMPONENT_TYPES.dragAndDrop:
7181
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
7282
break;
73-
case COMPONENT_TYPES.problem:
7483
case COMPONENT_TYPES.video:
84+
case COMPONENT_TYPES.problem:
7585
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
76-
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
77-
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
86+
setCourseId(courseKey);
87+
setBlockType(type);
88+
setNewBlockId(locator);
89+
showXBlockEditorModal();
7890
});
7991
break;
8092
// TODO: The library functional will be a bit different of current legacy (CMS)
@@ -99,9 +111,11 @@ const AddComponent = ({
99111
type,
100112
boilerplate: moduleName,
101113
parentLocator: blockId,
102-
}, ({ courseKey, locator }) => {
103-
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
104-
navigate(`/course/${courseKey}/editor/html/${locator}`);
114+
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
115+
setCourseId(courseKey);
116+
setBlockType(type);
117+
setNewBlockId(locator);
118+
showXBlockEditorModal();
105119
});
106120
break;
107121
default:
@@ -201,6 +215,25 @@ const AddComponent = ({
201215
onChangeComponentSelection={setSelectedComponents}
202216
/>
203217
</StandardModal>
218+
<StandardModal
219+
title={intl.formatMessage(messages.blockEditorModalTitle)}
220+
isOpen={isXBlockEditorModalOpen}
221+
onClose={closeXBlockEditorModal}
222+
isOverflowVisible={false}
223+
size="xl"
224+
>
225+
<div className="editor-page">
226+
<EditorPage
227+
courseId={courseId}
228+
blockType={blockType}
229+
blockId={newBlockId}
230+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
231+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
232+
onClose={closeXBlockEditorModal}
233+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
234+
/>
235+
</div>
236+
</StandardModal>
204237
</div>
205238
);
206239
}

src/course-unit/add-component/messages.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ const messages = defineMessages({
3131
defaultMessage: 'Add selected components',
3232
description: 'Problem bank component add button text.',
3333
},
34+
videoPickerModalTitle: {
35+
id: 'course-authoring.course-unit.modal.video-title.text',
36+
defaultMessage: 'Select video',
37+
description: 'Video picker modal title.',
38+
},
39+
blockEditorModalTitle: {
40+
id: 'course-authoring.course-unit.modal.block-editor-title.text',
41+
defaultMessage: 'Edit component',
42+
description: 'Block editor modal title.',
43+
},
3444
modalContainerTitle: {
3545
id: 'course-authoring.course-unit.modal.container.title',
3646
defaultMessage: 'Add {componentTitle} component',
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
export type UseMessageHandlersTypes = {
22
courseId: string;
3-
navigate: (path: string) => void;
43
dispatch: (action: any) => void;
54
setIframeOffset: (height: number) => void;
65
handleDeleteXBlock: (usageId: string) => void;
76
handleScrollToXBlock: (scrollOffset: number) => void;
87
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
8+
handleEditXBlock: (blockType: string, usageId: string) => void;
99
handleManageXBlockAccess: (usageId: string) => void;
1010
handleShowLegacyEditXBlockModal: (id: string) => void;
1111
handleCloseLegacyEditorXBlockModal: () => void;
@@ -14,7 +14,6 @@ export type UseMessageHandlersTypes = {
1414
handleOpenManageTagsModal: (id: string) => void;
1515
handleShowProcessingNotification: (variant: string) => void;
1616
handleHideProcessingNotification: () => void;
17-
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
1817
};
1918

2019
export type MessageHandlersTypes = Record<string, (payload: any) => void>;

src/course-unit/xblock-container-iframe/hooks/useMessageHandlers.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
1616
*/
1717
export const useMessageHandlers = ({
1818
courseId,
19-
navigate,
2019
dispatch,
2120
setIframeOffset,
2221
handleDeleteXBlock,
@@ -30,14 +29,14 @@ export const useMessageHandlers = ({
3029
handleOpenManageTagsModal,
3130
handleShowProcessingNotification,
3231
handleHideProcessingNotification,
33-
handleRedirectToXBlockEditPage,
32+
handleEditXBlock,
3433
}: UseMessageHandlersTypes): MessageHandlersTypes => {
3534
const { copyToClipboard } = useClipboard();
3635

3736
return useMemo(() => ({
3837
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
3938
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
40-
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
39+
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
4140
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
4241
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
4342
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
@@ -52,9 +51,14 @@ export const useMessageHandlers = ({
5251
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
5352
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
5453
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
55-
[messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
54+
[messageTypes.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification(
55+
NOTIFICATION_MESSAGES.copying,
56+
),
5657
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
57-
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
58+
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock(
59+
payload.type,
60+
payload.locator,
61+
),
5862
}), [
5963
courseId,
6064
handleDeleteXBlock,

src/course-unit/xblock-container-iframe/index.tsx

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { getConfig } from '@edx/frontend-platform';
12
import {
23
FC, useEffect, useState, useMemo, useCallback,
34
} from 'react';
45
import { useIntl } from '@edx/frontend-platform/i18n';
5-
import { useToggle, Sheet } from '@openedx/paragon';
6+
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
67
import { useDispatch } from 'react-redux';
78
import { useNavigate } from 'react-router-dom';
89

@@ -35,6 +36,7 @@ import messages from './messages';
3536
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
3637
import { useIframeContent } from '../../generic/hooks/useIframeContent';
3738
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
39+
import EditorPage from '../../editors/EditorPage';
3840

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

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

72+
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
73+
closeXBlockEditorModal();
74+
sendMessageToIframe(messageTypes.refreshXBlock, null);
75+
}, [closeXBlockEditorModal, sendMessageToIframe]);
76+
77+
const handleEditXBlock = useCallback((type: string, id: string) => {
78+
setBlockType(type);
79+
setNewBlockId(id);
80+
showXBlockEditorModal();
81+
}, [showXBlockEditorModal]);
82+
6783
const handleDuplicateXBlock = useCallback(
68-
(blockType: string, usageId: string) => {
84+
(type: string, usageId: string) => {
6985
unitXBlockActions.handleDuplicate(usageId);
70-
if (supportedEditors[blockType]) {
71-
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
86+
if (supportedEditors[type]) {
87+
// istanbul ignore next
88+
handleEditXBlock(type, usageId);
7289
}
7390
},
7491
[unitXBlockActions, courseId, navigate],
@@ -147,13 +164,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
147164
dispatch(hideProcessingNotification());
148165
};
149166

150-
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
151-
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
152-
};
153-
154167
const messageHandlers = useMessageHandlers({
155168
courseId,
156-
navigate,
157169
dispatch,
158170
setIframeOffset,
159171
handleDeleteXBlock,
@@ -167,7 +179,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
167179
handleOpenManageTagsModal,
168180
handleShowProcessingNotification,
169181
handleHideProcessingNotification,
170-
handleRedirectToXBlockEditPage,
182+
handleEditXBlock,
171183
});
172184

173185
useIframeMessages(messageHandlers);
@@ -186,6 +198,25 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
186198
close={closeDeleteModal}
187199
onDeleteSubmit={onDeleteSubmit}
188200
/>
201+
<StandardModal
202+
title={intl.formatMessage(messages.blockEditorModalTitle)}
203+
isOpen={isXBlockEditorModalOpen}
204+
onClose={closeXBlockEditorModal}
205+
isOverflowVisible={false}
206+
size="xl"
207+
>
208+
<div className="editor-page">
209+
<EditorPage
210+
courseId={courseId}
211+
blockType={blockType}
212+
blockId={newBlockId}
213+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
214+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
215+
onClose={closeXBlockEditorModal}
216+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
217+
/>
218+
</div>
219+
</StandardModal>
189220
{Object.keys(accessManagedXBlockData).length ? (
190221
<ConfigureModal
191222
isXBlockComponent

0 commit comments

Comments
 (0)