{
const [highlighted, setHighlighted] = React.useState(null);
const [
@@ -128,7 +129,10 @@ export const useVideoListProps = ({
},
selectBtnProps: {
onClick: () => {
- if (highlighted) {
+ /* istanbul ignore next */
+ if (returnFunction) {
+ returnFunction()();
+ } else if (highlighted) {
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
} else {
setShowSelectVideoError(true);
@@ -138,10 +142,15 @@ export const useVideoListProps = ({
};
};
-export const useVideoUploadHandler = ({ replace }) => {
+export const useVideoUploadHandler = ({ replace, uploadHandler }) => {
const learningContextId = useSelector(selectors.app.learningContextId);
const blockId = useSelector(selectors.app.blockId);
const path = `/course/${learningContextId}/editor/video_upload/${blockId}`;
+ if (uploadHandler) {
+ return () => {
+ uploadHandler();
+ };
+ }
if (replace) {
return () => window.location.replace(path);
}
@@ -191,11 +200,12 @@ export const getstatusBadgeVariant = ({ status }) => {
export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status);
-export const useVideoProps = ({ videos }) => {
+export const useVideoProps = ({ videos, uploadHandler, returnFunction }) => {
const searchSortProps = useSearchAndSortProps();
const videoList = useVideoListProps({
searchSortProps,
videos,
+ returnFunction,
});
const {
galleryError,
@@ -203,7 +213,7 @@ export const useVideoProps = ({ videos }) => {
inputError,
selectBtnProps,
} = videoList;
- const fileInput = { click: useVideoUploadHandler({ replace: false }) };
+ const fileInput = { click: useVideoUploadHandler({ replace: false, uploadHandler }) };
return {
galleryError,
diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx
index 5f5a8a8ca8..ac25c85268 100644
--- a/src/editors/containers/VideoGallery/index.jsx
+++ b/src/editors/containers/VideoGallery/index.jsx
@@ -1,5 +1,10 @@
-import React, { useEffect } from 'react';
-import { Image } from '@openedx/paragon';
+import React, { useCallback, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Image, useToggle, StandardModal,
+} from '@openedx/paragon';
+import { useSearchParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { selectors } from '../../data/redux';
import * as hooks from './hooks';
@@ -8,8 +13,11 @@ import { acceptedImgKeys } from './utils';
import messages from './messages';
import { RequestKeys } from '../../data/constants/requests';
import videoThumbnail from '../../data/images/videoThumbnail.svg';
+import VideoUploadEditor from '../VideoUploadEditor';
+import VideoEditor from '../VideoEditor';
-const VideoGallery = () => {
+const VideoGallery = ({ returnFunction, onCancel }) => {
+ const intl = useIntl();
const rawVideos = useSelector(selectors.app.videos);
const isLoaded = useSelector(
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
@@ -21,14 +29,27 @@ const VideoGallery = () => {
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
);
const videos = hooks.buildVideos({ rawVideos });
- const handleVideoUpload = hooks.useVideoUploadHandler({ replace: true });
+ const [isVideoUploadModalOpen, showVideoUploadModal, closeVideoUploadModal] = useToggle();
+ const [isVideoEditorModalOpen, showVideoEditorModal, closeVideoEditorModal] = useToggle();
+ const setSearchParams = useSearchParams()[1];
useEffect(() => {
- // If no videos exists redirects to the video upload screen
+ // If no videos exists opens to the video upload modal
if (isLoaded && videos.length === 0) {
- handleVideoUpload();
+ showVideoUploadModal();
}
}, [isLoaded]);
+
+ const onVideoUpload = useCallback((videoUrl) => {
+ closeVideoUploadModal();
+ showVideoEditorModal();
+ setSearchParams({ selectedVideoUrl: videoUrl });
+ }, [closeVideoUploadModal, showVideoEditorModal, setSearchParams]);
+
+ const uploadHandler = useCallback(() => {
+ showVideoUploadModal();
+ });
+
const {
galleryError,
inputError,
@@ -36,7 +57,7 @@ const VideoGallery = () => {
galleryProps,
searchSortProps,
selectBtnProps,
- } = hooks.useVideoProps({ videos });
+ } = hooks.useVideoProps({ videos, uploadHandler, returnFunction });
const handleCancel = hooks.useCancelHandler();
const modalMessages = {
@@ -60,8 +81,8 @@ const VideoGallery = () => {
{
isFetchError,
}}
/>
+
+
+
+
+
+ {isVideoEditorModalOpen && (
+
+ )}
);
};
-VideoGallery.propTypes = {};
+VideoGallery.propTypes = {
+ onCancel: PropTypes.func,
+ returnFunction: PropTypes.func,
+};
export default VideoGallery;
diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx
index 1cffffea82..6e45a02d27 100644
--- a/src/editors/containers/VideoGallery/index.test.jsx
+++ b/src/editors/containers/VideoGallery/index.test.jsx
@@ -6,6 +6,8 @@ import React from 'react';
import {
act, fireEvent, render, screen,
} from '@testing-library/react';
+import * as reactRouterDom from 'react-router-dom';
+import * as reduxThunks from '../../data/redux';
import VideoGallery from './index';
@@ -120,11 +122,10 @@ describe('VideoGallery', () => {
expect(screen.getByText(video.client_video_id)).toBeInTheDocument()
));
});
- it('navigates to video upload page when there are no videos', async () => {
- expect(window.location.replace).not.toHaveBeenCalled();
+ it('renders video upload modal when there are no videos', async () => {
updateState({ videos: [] });
await renderComponent();
- expect(window.location.replace).toHaveBeenCalled();
+ expect(screen.getByRole('heading', { name: /upload or embed a new video/i })).toBeInTheDocument();
});
it.each([
[/newest/i, [2, 1, 3]],
@@ -191,5 +192,36 @@ describe('VideoGallery', () => {
expect(screen.queryByText('client_id_1')).not.toBeInTheDocument();
expect(screen.queryByText('client_id_3')).not.toBeInTheDocument();
});
+
+ it('calls onVideoUpload correctly when a video is uploaded', async () => {
+ // Mock useSearchParams
+ const setSearchParams = jest.fn();
+ jest.spyOn(reactRouterDom, 'useSearchParams').mockReturnValue([{}, setSearchParams]);
+
+ // Mock the uploadVideo thunk to immediately call postUploadRedirect
+ jest.spyOn(reduxThunks.thunkActions.video, 'uploadVideo').mockImplementation(
+ ({ postUploadRedirect }) => () => {
+ if (postUploadRedirect) {
+ postUploadRedirect('http://test.video/url.mp4');
+ }
+ return { type: 'MOCK_UPLOAD_VIDEO' };
+ },
+ );
+
+ await renderComponent();
+
+ // Open the upload modal by clicking the button
+ const openModalButton = screen.getByRole('button', { name: /upload or embed a new video/i });
+ fireEvent.click(openModalButton);
+
+ // Wait for the input to appear in the modal
+ const urlInput = await screen.findByPlaceholderText('Paste your video ID or URL');
+ fireEvent.change(urlInput, { target: { value: 'http://test.video/url.mp4' } });
+
+ const submitButton = screen.getByRole('button', { name: /submit/i });
+ fireEvent.click(submitButton);
+
+ expect(setSearchParams).toHaveBeenCalledWith({ selectedVideoUrl: 'http://test.video/url.mp4' });
+ });
});
});
diff --git a/src/editors/containers/VideoGallery/messages.js b/src/editors/containers/VideoGallery/messages.js
index e26dd63db3..3dd446b7c9 100644
--- a/src/editors/containers/VideoGallery/messages.js
+++ b/src/editors/containers/VideoGallery/messages.js
@@ -21,7 +21,16 @@ const messages = {
defaultMessage: 'Upload or embed a new video',
description: 'Label for upload button',
},
-
+ videoUploadModalTitle: {
+ id: 'authoring.selectvideomodal.upload.title',
+ defaultMessage: 'Upload or embed a new video',
+ description: 'Label for upload modal',
+ },
+ videoEditorModalTitle: {
+ id: 'authoring.selectvideomodal.edit.title',
+ defaultMessage: 'Edit selected video',
+ description: 'Label for editor modal',
+ },
// Sort Dropdown
sortByDateNewest: {
id: 'authoring.selectvideomodal.sort.datenewest.label',
diff --git a/src/editors/containers/VideoUploadEditor/VideoUploader.jsx b/src/editors/containers/VideoUploadEditor/VideoUploader.jsx
index 028d1c085a..09d943db83 100644
--- a/src/editors/containers/VideoUploadEditor/VideoUploader.jsx
+++ b/src/editors/containers/VideoUploadEditor/VideoUploader.jsx
@@ -10,9 +10,9 @@ import { thunkActions } from '../../data/redux';
import * as hooks from './hooks';
import messages from './messages';
-const URLUploader = () => {
+const URLUploader = ({ onUpload }) => {
const [textInputValue, setTextInputValue] = React.useState('');
- const onURLUpload = hooks.onVideoUpload('selectedVideoUrl');
+ const onURLUpload = hooks.onVideoUpload('selectedVideoUrl', onUpload);
const intl = useIntl();
return (
@@ -58,16 +58,16 @@ const URLUploader = () => {
);
};
-export const VideoUploader = ({ setLoading }) => {
+export const VideoUploader = ({ setLoading, onUpload, onClose }) => {
const dispatch = useDispatch();
const intl = useIntl();
- const goBack = hooks.useHistoryGoBack();
+ const goBack = onClose || hooks.useHistoryGoBack();
const handleProcessUpload = ({ fileData }) => {
dispatch(thunkActions.video.uploadVideo({
supportedFiles: [fileData],
setLoadSpinner: setLoading,
- postUploadRedirect: hooks.onVideoUpload('selectedVideoId'),
+ postUploadRedirect: hooks.onVideoUpload('selectedVideoId', onUpload),
}));
};
@@ -85,14 +85,20 @@ export const VideoUploader = ({ setLoading }) => {
}
+ inputComponent={}
/>
);
};
+URLUploader.propTypes = {
+ onUpload: PropTypes.func,
+};
+
VideoUploader.propTypes = {
setLoading: PropTypes.func.isRequired,
+ onUpload: PropTypes.func,
+ onClose: PropTypes.func,
};
export default VideoUploader;
diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js
index a2774d9c60..3cc1f8468e 100644
--- a/src/editors/containers/VideoUploadEditor/hooks.js
+++ b/src/editors/containers/VideoUploadEditor/hooks.js
@@ -11,15 +11,20 @@ export const {
navigateTo,
} = appHooks;
-export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl') => {
+export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl', onUpload = null) => {
const learningContextId = selectors.app.learningContextId(storeState);
const blockId = selectors.app.blockId(storeState);
+ if (onUpload) {
+ return (videoUrl) => {
+ onUpload(videoUrl, learningContextId, blockId);
+ };
+ }
return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?${uploadType}=${videoUrl}`);
};
-export const onVideoUpload = (uploadType) => {
+export const onVideoUpload = (uploadType, onUpload) => {
const storeState = store.getState();
- return module.postUploadRedirect(storeState, uploadType);
+ return module.postUploadRedirect(storeState, uploadType, onUpload);
};
export const useUploadVideo = async ({
diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx
index ae2be7b5fb..91664d3e0a 100644
--- a/src/editors/containers/VideoUploadEditor/index.jsx
+++ b/src/editors/containers/VideoUploadEditor/index.jsx
@@ -1,17 +1,18 @@
import React from 'react';
+import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Spinner } from '@openedx/paragon';
import './index.scss';
import messages from './messages';
import { VideoUploader } from './VideoUploader';
-const VideoUploadEditor = () => {
+const VideoUploadEditor = ({ onUpload, onClose }) => {
const [loading, setLoading] = React.useState(false);
const intl = useIntl();
return (!loading) ? (
{
);
};
+VideoUploadEditor.propTypes = {
+ onUpload: PropTypes.func,
+ onClose: PropTypes.func,
+};
+
export default VideoUploadEditor;
diff --git a/src/library-authoring/components/ComponentEditorModal.tsx b/src/library-authoring/components/ComponentEditorModal.tsx
index 74ffc85383..023bcb52a0 100644
--- a/src/library-authoring/components/ComponentEditorModal.tsx
+++ b/src/library-authoring/components/ComponentEditorModal.tsx
@@ -41,7 +41,6 @@ export const ComponentEditorModal: React.FC> = () => {
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={onClose}
returnFunction={() => onClose}
- fullScreen={false}
/>
);
};