Skip to content

Commit a8fd981

Browse files
committed
fix: do open editor of new xblock when duplicating (openedx#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 1919eb4 commit a8fd981

File tree

15 files changed

+642
-323
lines changed

15 files changed

+642
-323
lines changed

src/course-unit/CourseUnit.test.jsx

Lines changed: 351 additions & 280 deletions
Large diffs are not rendered by default.

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
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,
@@ -32,10 +35,17 @@ const AddComponent = ({
3235
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
3336
const blockId = addComponentTemplateData.parentLocator || parentLocator;
3437
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
38+
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
39+
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
40+
41+
const [blockType, setBlockType] = useState(null);
42+
const [courseId, setCourseId] = useState(null);
43+
const [newBlockId, setNewBlockId] = useState(null);
3544
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
3645
const [selectedComponents, setSelectedComponents] = useState([]);
3746
const [usageId, setUsageId] = useState(null);
3847
const { sendMessageToIframe } = useIframe();
48+
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
3949

4050
const receiveMessage = useCallback(({ data: { type, payload } }) => {
4151
if (type === messageTypes.showMultipleComponentPicker) {
@@ -54,6 +64,12 @@ const AddComponent = ({
5464
closeSelectLibraryContentModal();
5565
}, [selectedComponents]);
5666

67+
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
68+
closeXBlockEditorModal();
69+
closeVideoSelectorModal();
70+
sendMessageToIframe(messageTypes.refreshXBlock, null);
71+
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
72+
5773
const handleLibraryV2Selection = useCallback((selection) => {
5874
handleCreateNewCourseXBlock({
5975
type: COMPONENT_TYPES.libraryV2,
@@ -77,6 +93,21 @@ const AddComponent = ({
7793
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
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:
@@ -201,6 +232,43 @@ const AddComponent = ({
201232
onChangeComponentSelection={setSelectedComponents}
202233
/>
203234
</StandardModal>
235+
<StandardModal
236+
title={intl.formatMessage(messages.videoPickerModalTitle)}
237+
isOpen={isVideoSelectorModalOpen}
238+
onClose={closeVideoSelectorModal}
239+
isOverflowVisible={false}
240+
size="xl"
241+
>
242+
<div className="selector-page">
243+
<VideoSelectorPage
244+
blockId={newBlockId}
245+
courseId={courseId}
246+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
247+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
248+
onCancel={closeVideoSelectorModal}
249+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
250+
/>
251+
</div>
252+
</StandardModal>
253+
<StandardModal
254+
title={intl.formatMessage(messages.blockEditorModalTitle)}
255+
isOpen={isXBlockEditorModalOpen}
256+
onClose={closeXBlockEditorModal}
257+
isOverflowVisible={false}
258+
size="xl"
259+
>
260+
<div className="editor-page">
261+
<EditorPage
262+
courseId={courseId}
263+
blockType={blockType}
264+
blockId={newBlockId}
265+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
266+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
267+
onClose={closeXBlockEditorModal}
268+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
269+
/>
270+
</div>
271+
</StandardModal>
204272
</div>
205273
);
206274
}

src/course-unit/xblock-container-iframe/hooks/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export type UseMessageHandlersTypes = {
55
setIframeOffset: (height: number) => void;
66
handleDeleteXBlock: (usageId: string) => void;
77
handleScrollToXBlock: (scrollOffset: number) => void;
8-
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
8+
handleDuplicateXBlock: (usageId: string) => void;
9+
handleEditXBlock: (blockType: string, usageId: string) => void;
910
handleManageXBlockAccess: (usageId: string) => void;
1011
handleShowLegacyEditXBlockModal: (id: string) => void;
1112
handleCloseLegacyEditorXBlockModal: () => void;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export const useMessageHandlers = ({
3737
return useMemo(() => ({
3838
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
3939
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
40-
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
41-
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
40+
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
41+
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId),
4242
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
4343
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
4444
[messageTypes.toggleCourseXBlockDropdown]: ({

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

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import {
22
FC, useEffect, useState, useMemo, useCallback,
33
} from 'react';
44
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';
5+
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
6+
import { useDispatch, useSelector } from 'react-redux';
87

98
import {
109
hideProcessingNotification,
@@ -13,9 +12,9 @@ import {
1312
import DeleteModal from '../../generic/delete-modal/DeleteModal';
1413
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
1514
import ModalIframe from '../../generic/modal-iframe';
15+
import { getWaffleFlags } from '../../data/selectors';
1616
import { IFRAME_FEATURE_POLICY } from '../../constants';
1717
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
18-
import supportedEditors from '../../editors/supportedEditors';
1918
import { useIframe } from '../../generic/hooks/context/hooks';
2019
import {
2120
fetchCourseSectionVerticalData,
@@ -35,16 +34,22 @@ import messages from './messages';
3534
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
3635
import { useIframeContent } from '../../generic/hooks/useIframeContent';
3736
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
37+
import VideoSelectorPage from '../../editors/VideoSelectorPage';
38+
import EditorPage from '../../editors/EditorPage';
3839

3940
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
4041
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
4142
}) => {
4243
const intl = useIntl();
4344
const dispatch = useDispatch();
44-
const navigate = useNavigate();
4545

4646
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
4747
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
48+
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
49+
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
50+
const [blockType, setBlockType] = useState<string>('');
51+
const { useVideoGalleryFlow } = useSelector(getWaffleFlags);
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,14 +69,27 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
6469
setIframeRef(iframeRef);
6570
}, [setIframeRef]);
6671

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

7795
const handleDeleteXBlock = (usageId: string) => {
@@ -186,6 +204,43 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
186204
close={closeDeleteModal}
187205
onDeleteSubmit={onDeleteSubmit}
188206
/>
207+
<StandardModal
208+
title={intl.formatMessage(messages.videoPickerModalTitle)}
209+
isOpen={isVideoSelectorModalOpen}
210+
onClose={closeVideoSelectorModal}
211+
isOverflowVisible={false}
212+
size="xl"
213+
>
214+
<div className="selector-page">
215+
<VideoSelectorPage
216+
blockId={newBlockId}
217+
courseId={courseId}
218+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
219+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
220+
onCancel={closeVideoSelectorModal}
221+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
222+
/>
223+
</div>
224+
</StandardModal>
225+
<StandardModal
226+
title={intl.formatMessage(messages.blockEditorModalTitle)}
227+
isOpen={isXBlockEditorModalOpen}
228+
onClose={closeXBlockEditorModal}
229+
isOverflowVisible={false}
230+
size="xl"
231+
>
232+
<div className="editor-page">
233+
<EditorPage
234+
courseId={courseId}
235+
blockType={blockType}
236+
blockId={newBlockId}
237+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
238+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
239+
onClose={closeXBlockEditorModal}
240+
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
241+
/>
242+
</div>
243+
</StandardModal>
189244
{Object.keys(accessManagedXBlockData).length ? (
190245
<ConfigureModal
191246
isXBlockComponent

src/data/slice.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const slice = createSlice({
2626
useNewCertificatesPage: true,
2727
useNewTextbooksPage: true,
2828
useNewGroupConfigurationsPage: true,
29+
useVideoGalleryFlow: false,
2930
},
3031
},
3132
reducers: {

src/editors/VideoSelector.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const VideoSelector = ({
99
learningContextId,
1010
lmsEndpointUrl,
1111
studioEndpointUrl,
12+
returnFunction,
13+
onCancel,
1214
}) => {
1315
const dispatch = useDispatch();
1416
const loading = hooks.useInitializeApp({
@@ -26,7 +28,7 @@ const VideoSelector = ({
2628
return null;
2729
}
2830
return (
29-
<VideoGallery />
31+
<VideoGallery returnFunction={returnFunction} onCancel={onCancel} />
3032
);
3133
};
3234

@@ -35,6 +37,8 @@ VideoSelector.propTypes = {
3537
learningContextId: PropTypes.string.isRequired,
3638
lmsEndpointUrl: PropTypes.string.isRequired,
3739
studioEndpointUrl: PropTypes.string.isRequired,
40+
returnFunction: PropTypes.func,
41+
onCancel: PropTypes.func,
3842
};
3943

4044
export default VideoSelector;

src/editors/VideoSelectorPage.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const VideoSelectorPage = ({
1010
courseId,
1111
lmsEndpointUrl,
1212
studioEndpointUrl,
13+
returnFunction,
14+
onCancel,
1315
}) => (
1416
<Provider store={store}>
1517
<ErrorBoundary
@@ -24,6 +26,8 @@ const VideoSelectorPage = ({
2426
learningContextId: courseId,
2527
lmsEndpointUrl,
2628
studioEndpointUrl,
29+
returnFunction,
30+
onCancel,
2731
}}
2832
/>
2933
</ErrorBoundary>
@@ -42,6 +46,8 @@ VideoSelectorPage.propTypes = {
4246
courseId: PropTypes.string,
4347
lmsEndpointUrl: PropTypes.string,
4448
studioEndpointUrl: PropTypes.string,
49+
returnFunction: PropTypes.func,
50+
onCancel: PropTypes.func,
4551
};
4652

4753
export default VideoSelectorPage;

src/editors/containers/VideoGallery/hooks.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const filterList = ({
8686
export const useVideoListProps = ({
8787
searchSortProps,
8888
videos,
89+
returnFunction,
8990
}) => {
9091
const [highlighted, setHighlighted] = React.useState(null);
9192
const [
@@ -128,7 +129,10 @@ export const useVideoListProps = ({
128129
},
129130
selectBtnProps: {
130131
onClick: () => {
131-
if (highlighted) {
132+
/* istanbul ignore next */
133+
if (returnFunction) {
134+
returnFunction()();
135+
} else if (highlighted) {
132136
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
133137
} else {
134138
setShowSelectVideoError(true);
@@ -138,10 +142,15 @@ export const useVideoListProps = ({
138142
};
139143
};
140144

141-
export const useVideoUploadHandler = ({ replace }) => {
145+
export const useVideoUploadHandler = ({ replace, uploadHandler }) => {
142146
const learningContextId = useSelector(selectors.app.learningContextId);
143147
const blockId = useSelector(selectors.app.blockId);
144148
const path = `/course/${learningContextId}/editor/video_upload/${blockId}`;
149+
if (uploadHandler) {
150+
return () => {
151+
uploadHandler();
152+
};
153+
}
145154
if (replace) {
146155
return () => window.location.replace(path);
147156
}
@@ -191,19 +200,20 @@ export const getstatusBadgeVariant = ({ status }) => {
191200

192201
export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status);
193202

194-
export const useVideoProps = ({ videos }) => {
203+
export const useVideoProps = ({ videos, uploadHandler, returnFunction }) => {
195204
const searchSortProps = useSearchAndSortProps();
196205
const videoList = useVideoListProps({
197206
searchSortProps,
198207
videos,
208+
returnFunction,
199209
});
200210
const {
201211
galleryError,
202212
galleryProps,
203213
inputError,
204214
selectBtnProps,
205215
} = videoList;
206-
const fileInput = { click: useVideoUploadHandler({ replace: false }) };
216+
const fileInput = { click: useVideoUploadHandler({ replace: false, uploadHandler }) };
207217

208218
return {
209219
galleryError,

0 commit comments

Comments
 (0)