Skip to content

Commit 944d131

Browse files
authored
fix: do open editor of new xblock when duplicating (#2017)
* feat: display editors as modals (#1838) * fix: do open editor of new xblock when duplicating (#1887) Fixes bug where after duplicating an xblock, the editor modal of the old xblock is being open instead of the new copied xblock.
1 parent dd731a0 commit 944d131

File tree

29 files changed

+709
-466
lines changed

29 files changed

+709
-466
lines changed

src/course-unit/CourseUnit.test.jsx

Lines changed: 360 additions & 322 deletions
Large diffs are not rendered by default.

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

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
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,
88
} from '@openedx/paragon';
99

1010
import { getCourseSectionVertical } from '../data/selectors';
11+
import { getWaffleFlags } from '../../data/selectors';
1112
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
1213
import ComponentModalView from './add-component-modals/ComponentModalView';
1314
import AddComponentButton from './add-component-btn';
@@ -16,6 +17,8 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
1617
import { messageTypes } from '../constants';
1718
import { useIframe } from '../../generic/hooks/context/hooks';
1819
import { useEventListener } from '../../generic/hooks';
20+
import VideoSelectorPage from '../../editors/VideoSelectorPage';
21+
import EditorPage from '../../editors/EditorPage';
1922

2023
const AddComponent = ({
2124
parentLocator,
@@ -24,18 +27,24 @@ const AddComponent = ({
2427
addComponentTemplateData,
2528
handleCreateNewCourseXBlock,
2629
}) => {
27-
const navigate = useNavigate();
2830
const intl = useIntl();
2931
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
3032
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
3133
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
3234
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
3335
const blockId = addComponentTemplateData.parentLocator || parentLocator;
3436
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
37+
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
38+
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
39+
40+
const [blockType, setBlockType] = useState(null);
41+
const [courseId, setCourseId] = useState(null);
42+
const [newBlockId, setNewBlockId] = useState(null);
3543
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
3644
const [selectedComponents, setSelectedComponents] = useState([]);
3745
const [usageId, setUsageId] = useState(null);
3846
const { sendMessageToIframe } = useIframe();
47+
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
3948

4049
const receiveMessage = useCallback(({ data: { type, payload } }) => {
4150
if (type === messageTypes.showMultipleComponentPicker) {
@@ -54,6 +63,12 @@ const AddComponent = ({
5463
closeSelectLibraryContentModal();
5564
}, [selectedComponents]);
5665

66+
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
67+
closeXBlockEditorModal();
68+
closeVideoSelectorModal();
69+
sendMessageToIframe(messageTypes.refreshXBlock, null);
70+
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
71+
5772
const handleLibraryV2Selection = useCallback((selection) => {
5873
handleCreateNewCourseXBlock({
5974
type: COMPONENT_TYPES.libraryV2,
@@ -71,12 +86,28 @@ const AddComponent = ({
7186
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
7287
break;
7388
case COMPONENT_TYPES.problem:
74-
case COMPONENT_TYPES.video:
7589
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
76-
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
77-
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
90+
setCourseId(courseKey);
91+
setBlockType(type);
92+
setNewBlockId(locator);
93+
showXBlockEditorModal();
7894
});
7995
break;
96+
case COMPONENT_TYPES.video:
97+
handleCreateNewCourseXBlock(
98+
{ type, parentLocator: blockId },
99+
/* istanbul ignore next */ ({ courseKey, locator }) => {
100+
setCourseId(courseKey);
101+
setBlockType(type);
102+
setNewBlockId(locator);
103+
if (useVideoGalleryFlow) {
104+
showVideoSelectorModal();
105+
} else {
106+
showXBlockEditorModal();
107+
}
108+
},
109+
);
110+
break;
80111
// TODO: The library functional will be a bit different of current legacy (CMS)
81112
// behaviour and this ticket is on hold (blocked by other development team).
82113
case COMPONENT_TYPES.library:
@@ -99,9 +130,11 @@ const AddComponent = ({
99130
type,
100131
boilerplate: moduleName,
101132
parentLocator: blockId,
102-
}, ({ courseKey, locator }) => {
103-
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
104-
navigate(`/course/${courseKey}/editor/html/${locator}`);
133+
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
134+
setCourseId(courseKey);
135+
setBlockType(type);
136+
setNewBlockId(locator);
137+
showXBlockEditorModal();
105138
});
106139
break;
107140
default:
@@ -201,6 +234,43 @@ const AddComponent = ({
201234
onChangeComponentSelection={setSelectedComponents}
202235
/>
203236
</StandardModal>
237+
<StandardModal
238+
title={intl.formatMessage(messages.videoPickerModalTitle)}
239+
isOpen={isVideoSelectorModalOpen}
240+
onClose={closeVideoSelectorModal}
241+
isOverflowVisible={false}
242+
size="xl"
243+
>
244+
<div className="selector-page">
245+
<VideoSelectorPage
246+
blockId={newBlockId}
247+
courseId={courseId}
248+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
249+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
250+
onCancel={closeVideoSelectorModal}
251+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
252+
/>
253+
</div>
254+
</StandardModal>
255+
<StandardModal
256+
title={intl.formatMessage(messages.blockEditorModalTitle)}
257+
isOpen={isXBlockEditorModalOpen}
258+
onClose={closeXBlockEditorModal}
259+
isOverflowVisible={false}
260+
size="xl"
261+
>
262+
<div className="editor-page">
263+
<EditorPage
264+
courseId={courseId}
265+
blockType={blockType}
266+
blockId={newBlockId}
267+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
268+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
269+
onClose={closeXBlockEditorModal}
270+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
271+
/>
272+
</div>
273+
</StandardModal>
204274
</div>
205275
);
206276
}

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: 2 additions & 3 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;
8-
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
7+
handleDuplicateXBlock: (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: 10 additions & 6 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,15 +29,15 @@ 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}`),
41-
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
39+
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
40+
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId),
4241
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
4342
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
4443
[messageTypes.toggleCourseXBlockDropdown]: ({
@@ -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: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
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 { useDispatch } from 'react-redux';
7-
import { useNavigate } from 'react-router-dom';
6+
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
7+
import { useDispatch, useSelector } from 'react-redux';
88

99
import {
1010
hideProcessingNotification,
@@ -13,9 +13,9 @@ import {
1313
import DeleteModal from '../../generic/delete-modal/DeleteModal';
1414
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
1515
import ModalIframe from '../../generic/modal-iframe';
16+
import { getWaffleFlags } from '../../data/selectors';
1617
import { IFRAME_FEATURE_POLICY } from '../../constants';
1718
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
18-
import supportedEditors from '../../editors/supportedEditors';
1919
import { useIframe } from '../../generic/hooks/context/hooks';
2020
import {
2121
fetchCourseSectionVerticalData,
@@ -35,16 +35,22 @@ import messages from './messages';
3535
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
3636
import { useIframeContent } from '../../generic/hooks/useIframeContent';
3737
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
38+
import VideoSelectorPage from '../../editors/VideoSelectorPage';
39+
import EditorPage from '../../editors/EditorPage';
3840

3941
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
4042
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
4143
}) => {
4244
const intl = useIntl();
4345
const dispatch = useDispatch();
44-
const navigate = useNavigate();
4546

4647
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
4748
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
49+
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
50+
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
51+
const [blockType, setBlockType] = useState<string>('');
52+
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
53+
const [newBlockId, setNewBlockId] = useState<string>('');
4854
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
4955
const [iframeOffset, setIframeOffset] = useState(0);
5056
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
@@ -64,14 +70,27 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
6470
setIframeRef(iframeRef);
6571
}, [setIframeRef]);
6672

73+
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
74+
closeXBlockEditorModal();
75+
closeVideoSelectorModal();
76+
sendMessageToIframe(messageTypes.refreshXBlock, null);
77+
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
78+
79+
const handleEditXBlock = useCallback((type: string, id: string) => {
80+
setBlockType(type);
81+
setNewBlockId(id);
82+
if (type === 'video' && useVideoGalleryFlow) {
83+
showVideoSelectorModal();
84+
} else {
85+
showXBlockEditorModal();
86+
}
87+
}, [showVideoSelectorModal, showXBlockEditorModal]);
88+
6789
const handleDuplicateXBlock = useCallback(
68-
(blockType: string, usageId: string) => {
90+
(usageId: string) => {
6991
unitXBlockActions.handleDuplicate(usageId);
70-
if (supportedEditors[blockType]) {
71-
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
72-
}
7392
},
74-
[unitXBlockActions, courseId, navigate],
93+
[unitXBlockActions, courseId],
7594
);
7695

7796
const handleDeleteXBlock = (usageId: string) => {
@@ -147,13 +166,8 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
147166
dispatch(hideProcessingNotification());
148167
};
149168

150-
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
151-
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
152-
};
153-
154169
const messageHandlers = useMessageHandlers({
155170
courseId,
156-
navigate,
157171
dispatch,
158172
setIframeOffset,
159173
handleDeleteXBlock,
@@ -167,7 +181,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
167181
handleOpenManageTagsModal,
168182
handleShowProcessingNotification,
169183
handleHideProcessingNotification,
170-
handleRedirectToXBlockEditPage,
184+
handleEditXBlock,
171185
});
172186

173187
useIframeMessages(messageHandlers);
@@ -186,6 +200,43 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
186200
close={closeDeleteModal}
187201
onDeleteSubmit={onDeleteSubmit}
188202
/>
203+
<StandardModal
204+
title={intl.formatMessage(messages.videoPickerModalTitle)}
205+
isOpen={isVideoSelectorModalOpen}
206+
onClose={closeVideoSelectorModal}
207+
isOverflowVisible={false}
208+
size="xl"
209+
>
210+
<div className="selector-page">
211+
<VideoSelectorPage
212+
blockId={newBlockId}
213+
courseId={courseId}
214+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
215+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
216+
onCancel={closeVideoSelectorModal}
217+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
218+
/>
219+
</div>
220+
</StandardModal>
221+
<StandardModal
222+
title={intl.formatMessage(messages.blockEditorModalTitle)}
223+
isOpen={isXBlockEditorModalOpen}
224+
onClose={closeXBlockEditorModal}
225+
isOverflowVisible={false}
226+
size="xl"
227+
>
228+
<div className="editor-page">
229+
<EditorPage
230+
courseId={courseId}
231+
blockType={blockType}
232+
blockId={newBlockId}
233+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
234+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
235+
onClose={closeXBlockEditorModal}
236+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
237+
/>
238+
</div>
239+
</StandardModal>
189240
{Object.keys(accessManagedXBlockData).length ? (
190241
<ConfigureModal
191242
isXBlockComponent

0 commit comments

Comments
 (0)