From d2b3edad577bcdafba2018d3bcad32d79c590ca3 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 5 Apr 2023 15:46:15 -0500 Subject: [PATCH 1/8] feat: Adding blockId to the Gallery. Adding cancel and next navigation --- src/editors/VideoSelector.jsx | 4 ++- src/editors/VideoSelector.test.jsx | 2 +- src/editors/VideoSelectorPage.jsx | 4 +++ src/editors/VideoSelectorPage.test.jsx | 1 + .../VideoSelectorPage.test.jsx.snap | 2 ++ src/editors/containers/VideoGallery/hooks.js | 26 ++++++++++++++-- .../containers/VideoGallery/hooks.test.js | 30 +++++++++++++++++++ src/editors/containers/VideoGallery/index.jsx | 3 +- .../containers/VideoGallery/index.test.jsx | 6 ++++ src/editors/data/constants/analyticsEvt.js | 1 + 10 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/editors/VideoSelector.jsx b/src/editors/VideoSelector.jsx index ae5961f73..a427eb1e6 100644 --- a/src/editors/VideoSelector.jsx +++ b/src/editors/VideoSelector.jsx @@ -5,6 +5,7 @@ import VideoGallery from './containers/VideoGallery'; import * as hooks from './hooks'; export const VideoSelector = ({ + blockId, learningContextId, lmsEndpointUrl, studioEndpointUrl, @@ -13,7 +14,7 @@ export const VideoSelector = ({ hooks.initializeApp({ dispatch, data: { - blockId: '', + blockId, blockType: 'video', learningContextId, lmsEndpointUrl, @@ -26,6 +27,7 @@ export const VideoSelector = ({ }; VideoSelector.propTypes = { + blockId: PropTypes.string.isRequired, learningContextId: PropTypes.string.isRequired, lmsEndpointUrl: PropTypes.string.isRequired, studioEndpointUrl: PropTypes.string.isRequired, diff --git a/src/editors/VideoSelector.test.jsx b/src/editors/VideoSelector.test.jsx index fa518a0cb..69c3b6034 100644 --- a/src/editors/VideoSelector.test.jsx +++ b/src/editors/VideoSelector.test.jsx @@ -11,13 +11,13 @@ jest.mock('./hooks', () => ({ jest.mock('./containers/VideoGallery', () => 'VideoGallery'); const props = { + blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4', learningContextId: 'course-v1:edX+DemoX+Demo_Course', lmsEndpointUrl: 'evenfakerurl.com', studioEndpointUrl: 'fakeurl.com', }; const initData = { - blockId: '', blockType: 'video', ...props, }; diff --git a/src/editors/VideoSelectorPage.jsx b/src/editors/VideoSelectorPage.jsx index 5f754b46c..0d9609b04 100644 --- a/src/editors/VideoSelectorPage.jsx +++ b/src/editors/VideoSelectorPage.jsx @@ -6,6 +6,7 @@ import VideoSelector from './VideoSelector'; import store from './data/store'; const VideoSelectorPage = ({ + blockId, courseId, lmsEndpointUrl, studioEndpointUrl, @@ -19,6 +20,7 @@ const VideoSelectorPage = ({ > React.useState(val), searchString: (val) => React.useState(val), @@ -91,6 +100,8 @@ export const videoListProps = ({ searchSortProps, videos }) => { setShowSizeError, ] = module.state.showSizeError(false); const filteredList = module.filterList({ ...searchSortProps, videos }); + const learningContextId = useSelector(selectors.app.learningContextId); + const blockId = useSelector(selectors.app.blockId); return { galleryError: { show: showSelectVideoError, @@ -116,8 +127,10 @@ export const videoListProps = ({ searchSortProps, videos }) => { height: '100%', }, selectBtnProps: { - onclick: () => { - // TODO Update this when implementing the selection feature + onClick: () => { + // TODO save the metadata of the video on the block to fill it into the cideo editor + + navigateTo(`/course/${learningContextId}/editor/video/${blockId}`); }, }, }; @@ -135,6 +148,14 @@ export const fileInputProps = () => { }; }; +export const handleCancel = () => ( + navigateCallback({ + destination: useSelector(selectors.app.returnUrl), + analytics: useSelector(selectors.app.analytics), + analyticsEvent: analyticsEvt.videoGalleryCancelClick, + }) +); + export const buildVideos = ({ rawVideos }) => { let videos = []; const rawVideoList = Object.values(rawVideos); @@ -191,4 +212,5 @@ export const videoProps = ({ videos }) => { export default { videoProps, buildVideos, + handleCancel, }; diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js index d3ad83e97..3dd163636 100644 --- a/src/editors/containers/VideoGallery/hooks.test.js +++ b/src/editors/containers/VideoGallery/hooks.test.js @@ -1,7 +1,11 @@ +import * as reactRedux from 'react-redux'; import * as hooks from './hooks'; import { filterKeys, sortKeys } from './utils'; import { MockUseState } from '../../../testUtils'; import { keyStore } from '../../utils'; +import * as appHooks from '../../hooks'; +import { selectors } from '../../data/redux'; +import analyticsEvt from '../../data/constants/analyticsEvt'; jest.mock('react', () => ({ ...jest.requireActual('react'), @@ -16,9 +20,24 @@ jest.mock('react-redux', () => { ...jest.requireActual('react-redux'), dispatch: dispatchFn, useDispatch: jest.fn(() => dispatchFn), + useSelector: jest.fn(), }; }); +jest.mock('../../data/redux', () => ({ + selectors: { + app: { + returnUrl: 'returnUrl', + analytics: 'analytics', + }, + }, +})); + +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + navigateCallback: jest.fn((args) => ({ navigateCallback: args })), +})); + const state = new MockUseState(hooks); const hookKeys = keyStore(hooks); let hook; @@ -250,4 +269,15 @@ describe('VideoGallery hooks', () => { expect(hook.selectBtnProps).toEqual(videoList.selectBtnProps); }); }); + describe('handleCancel', () => { + it('calls navigateCallback', () => { + expect(hooks.handleCancel()).toEqual( + appHooks.navigateCallback({ + destination: reactRedux.useSelector(selectors.app.returnUrl), + analyticsEvent: analyticsEvt.videoGalleryCancelClick, + analytics: reactRedux.useSelector(selectors.app.analytics), + }), + ); + }); + }); }); diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx index 8ea788349..ac51e48de 100644 --- a/src/editors/containers/VideoGallery/index.jsx +++ b/src/editors/containers/VideoGallery/index.jsx @@ -24,6 +24,7 @@ export const VideoGallery = ({ searchSortProps, selectBtnProps, } = hooks.videoProps({ videos }); + const handleCancel = hooks.handleCancel(); const modalMessages = { confirmMsg: messages.selectVideoButtonlabel, @@ -38,7 +39,7 @@ export const VideoGallery = ({ { /* TODO */ }, + close: handleCancel, size: 'fullscreen', isFullscreenScroll: false, galleryError, diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index 31d3896ba..8acd355f0 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -39,6 +39,7 @@ jest.mock('./hooks', () => ({ searchSortProps: { search: 'sortProps' }, selectBtnProps: { select: 'btnProps' }, })), + handleCancel: jest.fn(), })); jest.mock('../../data/redux', () => ({ @@ -51,6 +52,11 @@ jest.mock('../../data/redux', () => ({ }, })); +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + navigateCallback: jest.fn((args) => ({ navigateCallback: args })), +})); + describe('VideoGallery', () => { describe('component', () => { const props = { diff --git a/src/editors/data/constants/analyticsEvt.js b/src/editors/data/constants/analyticsEvt.js index ef3099002..1b201956d 100644 --- a/src/editors/data/constants/analyticsEvt.js +++ b/src/editors/data/constants/analyticsEvt.js @@ -1,6 +1,7 @@ export const analyticsEvt = { editorSaveClick: 'edx.ui.authoring.editor.save', editorCancelClick: 'edx.ui.authoring.editor.cancel', + videoGalleryCancelClick: 'edx.ui.authoring.videogallery.cancel', }; export default analyticsEvt; From 23593b44fe8a59941e72fb76818f37116487da30 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 5 Apr 2023 18:35:11 -0500 Subject: [PATCH 2/8] chore: Error message on unselected video added & replace video button navigation added --- .../components/VideoEditorModal.jsx | 26 ++++++++++++++--- .../components/VideoSettingsModal/index.jsx | 28 ++++++++++++++++++- .../containers/VideoEditor/messages.js | 6 +++- src/editors/containers/VideoGallery/hooks.js | 9 ++++-- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx index 89df1bf81..836fe2d39 100644 --- a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx +++ b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx @@ -1,18 +1,27 @@ import React from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; - -import { thunkActions } from '../../../data/redux'; +import * as appHooks from '../../../hooks'; +import { thunkActions, selectors } from '../../../data/redux'; import VideoSettingsModal from './VideoSettingsModal'; // import SelectVideoModal from './SelectVideoModal'; import * as module from './VideoEditorModal'; +export const { + navigateTo, +} = appHooks; + export const hooks = { initialize: (dispatch) => { React.useEffect(() => { dispatch(thunkActions.video.loadVideoData()); }, []); }, + returnToGallery: () => { + const learningContextId = useSelector(selectors.app.learningContextId); + const blockId = useSelector(selectors.app.blockId); + return () => (navigateTo(`/course/${learningContextId}/editor/course-videos/${blockId}`)); + }, }; const VideoEditorModal = ({ @@ -21,8 +30,17 @@ const VideoEditorModal = ({ }) => { const dispatch = useDispatch(); module.hooks.initialize(dispatch); + const searchParams = new URLSearchParams(document.location.search); + const showReturn = searchParams.get('return') !== null; + const onReturn = module.hooks.returnToGallery(); return ( - + ); // TODO: add logic to show SelectVideoModal if no selection }; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx index 3232d8804..f85269a7d 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx @@ -1,4 +1,10 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@edx/paragon'; +import { ArrowBackIos } from '@edx/paragon/icons'; +import { + FormattedMessage, +} from '@edx/frontend-platform/i18n'; // import VideoPreview from './components/VideoPreview'; import ErrorSummary from './ErrorSummary'; @@ -11,9 +17,24 @@ import VideoSourceWidget from './components/VideoSourceWidget'; import VideoPreviewWidget from './components/VideoPreviewWidget'; import './index.scss'; import SocialShareWidget from './components/SocialShareWidget'; +import messages from '../../messages'; -export const VideoSettingsModal = () => ( +export const VideoSettingsModal = ({ + showReturn, + onReturn, +}) => ( <> + { showReturn && ( + + )} @@ -26,4 +47,9 @@ export const VideoSettingsModal = () => ( ); +VideoSettingsModal.propTypes = { + showReturn: PropTypes.bool.isRequired, + onReturn: PropTypes.func.isRequired, +}; + export default VideoSettingsModal; diff --git a/src/editors/containers/VideoEditor/messages.js b/src/editors/containers/VideoEditor/messages.js index 515eb4493..c041f69e3 100644 --- a/src/editors/containers/VideoEditor/messages.js +++ b/src/editors/containers/VideoEditor/messages.js @@ -1,12 +1,16 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - spinnerScreenReaderText: { id: 'authoring.videoEditor.spinnerScreenReaderText', defaultMessage: 'loading', description: 'Loading message for spinner screenreader text.', }, + replaceVideoButtonLabel: { + id: 'authoring.videoEditor.replaceVideoButtonLabel', + defaultMessage: 'Replace video', + description: 'Text of the replace video button to return to the video gallery', + }, }); export default messages; diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index b7285df92..bebef53c9 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -128,9 +128,12 @@ export const videoListProps = ({ searchSortProps, videos }) => { }, selectBtnProps: { onClick: () => { - // TODO save the metadata of the video on the block to fill it into the cideo editor - - navigateTo(`/course/${learningContextId}/editor/video/${blockId}`); + if (highlighted) { + // TODO save the metadata of the video on the block to fill it into the cideo editor + navigateTo(`/course/${learningContextId}/editor/video/${blockId}?return=true`); + } else { + setShowSelectVideoError(true); + } }, }, }; From da00ad7539dcec620fab1e09f4130d10e8d05680 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 6 Apr 2023 14:20:48 -0500 Subject: [PATCH 3/8] chore: All selection and save flow added --- .../components/VideoEditorModal.jsx | 9 +- src/editors/containers/VideoGallery/hooks.js | 3 +- src/editors/data/redux/thunkActions/video.js | 17 +++- .../data/redux/thunkActions/video.test.js | 85 +++++++++++++++++-- 4 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx index 836fe2d39..ab201e9d5 100644 --- a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx +++ b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx @@ -12,9 +12,9 @@ export const { } = appHooks; export const hooks = { - initialize: (dispatch) => { + initialize: (dispatch, selectedVideoId) => { React.useEffect(() => { - dispatch(thunkActions.video.loadVideoData()); + dispatch(thunkActions.video.loadVideoData(selectedVideoId)); }, []); }, returnToGallery: () => { @@ -29,10 +29,11 @@ const VideoEditorModal = ({ isOpen, }) => { const dispatch = useDispatch(); - module.hooks.initialize(dispatch); const searchParams = new URLSearchParams(document.location.search); - const showReturn = searchParams.get('return') !== null; + const selectedVideoId = searchParams.get('selectedVideoId'); + const showReturn = selectedVideoId != null; const onReturn = module.hooks.returnToGallery(); + module.hooks.initialize(dispatch, selectedVideoId); return ( { selectBtnProps: { onClick: () => { if (highlighted) { - // TODO save the metadata of the video on the block to fill it into the cideo editor - navigateTo(`/course/${learningContextId}/editor/video/${blockId}?return=true`); + navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`); } else { setShowSelectVideoError(true); } diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index eb9c64805..6daa21afa 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -1,16 +1,27 @@ /* eslint-disable import/no-cycle */ import { actions, selectors } from '..'; -import { removeItemOnce } from '../../../utils'; +import { formatDuration, removeItemOnce } from '../../../utils'; import * as requests from './requests'; import * as module from './video'; import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; import { parseYoutubeId } from '../../services/cms/api'; -export const loadVideoData = () => (dispatch, getState) => { +export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { const state = getState(); const blockValueData = state.app.blockValue.data; const rawVideoData = blockValueData.metadata ? blockValueData.metadata : {}; const courseData = state.app.courseDetails.data ? state.app.courseDetails.data : {}; + if (Object.keys(rawVideoData).length === 0 && selectedVideoId !== null) { + const rawVideos = Object.values(selectors.app.videos(state)); + const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId); + // TODO it's missing laod the transcripts + rawVideoData = { + edx_video_id: selectedVideo.edx_video_id, + thumbnail: selectedVideo.course_video_image_url, + end_time: formatDuration(selectedVideo.duration), + duration: selectedVideo.duration, + }; + } const studioView = state.app.studioView?.data?.html; const { videoId, @@ -45,7 +56,7 @@ export const loadVideoData = () => (dispatch, getState) => { duration: { // TODO duration is not always sent so they should be calculated. startTime: valueFromDuration(rawVideoData.start_time || '00:00:00'), stopTime: valueFromDuration(rawVideoData.end_time || '00:00:00'), - total: 0, // TODO can we get total duration? if not, probably dropping from widget + total: rawVideoData.duration || 0, // TODO can we get total duration? if not, probably dropping from widget }, handout: rawVideoData.handout, licenseType, diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index 29c7796af..32d9b06ad 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -12,6 +12,7 @@ jest.mock('..', () => ({ selectors: { app: { courseDetails: (state) => ({ courseDetails: state }), + videos: (state) => ({ videos: state.app.videos }), }, video: { videoId: (state) => ({ videoId: state }), @@ -34,6 +35,7 @@ jest.mock('./requests', () => ({ })); jest.mock('../../../utils', () => ({ + ...jest.requireActual('../../../utils'), removeItemOnce: (args) => (args), })); @@ -56,6 +58,7 @@ const mockVideoFeatures = { videoSharingEnabled: 'soMEbOolEAn', }, }; +const mockSelectedVideoId = 'ThisIsAVideoId'; const testMetadata = { download_track: 'dOWNlOAdTraCK', @@ -80,6 +83,11 @@ const testState = { originalThumbnail: null, videoId: 'soMEvIDEo', }; +const testVideosState = { + edx_video_id: mockSelectedVideoId, + thumbnail: 'thumbnail', + duration: 60, +}; const testUpload = { transcripts: ['la', 'en'] }; const testReplaceUpload = { file: mockFile, @@ -130,25 +138,37 @@ describe('video thunkActions', () => { jest.spyOn(thunkActions, thunkActionsKeys.parseTranscripts).mockReturnValue( testMetadata.transcripts, ); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('dispatches fetchVideoFeatures action', () => { thunkActions.loadVideoData()(dispatch, getState); [ [dispatchedLoad], [dispatchedAction1], [dispatchedAction2], ] = dispatch.mock.calls; - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - it('dispatches fetchVideoFeatures action', () => { expect(dispatchedLoad).not.toEqual(undefined); expect(dispatchedAction1.fetchVideoFeatures).not.toEqual(undefined); }); it('dispatches checkTranscriptsForImport action', () => { + thunkActions.loadVideoData()(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; expect(dispatchedLoad).not.toEqual(undefined); expect(dispatchedAction2.checkTranscriptsForImport).not.toEqual(undefined); }); it('dispatches actions.video.load', () => { + thunkActions.loadVideoData()(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; expect(dispatchedLoad.load).toEqual({ videoSource: 'videOsOurce', videoId: 'videOiD', @@ -186,7 +206,62 @@ describe('video thunkActions', () => { thumbnail: testMetadata.thumbnail, }); }); + it('dispatches actions.video.load with selectedVideoId', () => { + getState = jest.fn(() => ({ + app: { + blockId: 'soMEBloCk', + studioEndpointUrl: 'soMEeNDPoiNT', + blockValue: { data: { metadata: {} } }, + courseDetails: { data: { license: null } }, + studioView: { data: { html: 'sOMeHTml' } }, + videos: testVideosState, + }, + })); + thunkActions.loadVideoData(mockSelectedVideoId)(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; + expect(dispatchedLoad.load).toEqual({ + videoSource: 'videOsOurce', + videoId: 'videOiD', + fallbackVideos: 'fALLbACKvIDeos', + allowVideoDownloads: undefined, + transcripts: testMetadata.transcripts, + allowTranscriptDownloads: undefined, + allowVideoSharing: undefined, + showTranscriptByDefault: undefined, + duration: { + startTime: testMetadata.start_time, + stopTime: testVideosState.duration * 1000, + total: testVideosState.duration, + }, + handout: undefined, + licenseType: 'liCENSEtyPe', + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + courseLicenseType: 'liCENSEtyPe', + courseLicenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + thumbnail: undefined, + }); + }); it('dispatches actions.video.updateField on success', () => { + thunkActions.loadVideoData()(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; dispatch.mockClear(); dispatchedAction1.fetchVideoFeatures.onSuccess(mockVideoFeatures); expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ From 359a5fd5058b9e74f1b6e11203d23341c2a65482 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Thu, 2 Mar 2023 15:48:37 -0500 Subject: [PATCH 4/8] feat: Create new editor page for video upload --- package-lock.json | 2 +- .../components/VideoEditorModal.jsx | 2 -- .../components/VideoSettingsModal/index.jsx | 21 ++++++-------- src/editors/containers/VideoGallery/hooks.js | 14 +++++---- src/editors/containers/VideoGallery/index.jsx | 10 ++++++- .../containers/VideoGallery/index.test.jsx | 1 + .../containers/VideoUploadEditor/hooks.js | 29 +++++++++++++++++-- .../VideoUploadEditor/hooks.test.js | 18 +++++------- src/editors/data/redux/thunkActions/video.js | 9 +++--- .../data/redux/thunkActions/video.test.js | 9 ++++-- .../sharedComponents/SelectionModal/index.jsx | 1 - src/editors/supportedEditors.js | 4 +-- 12 files changed, 75 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc17aa36b..0828bba6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47382,4 +47382,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx index ab201e9d5..071241b50 100644 --- a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx +++ b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx @@ -31,14 +31,12 @@ const VideoEditorModal = ({ const dispatch = useDispatch(); const searchParams = new URLSearchParams(document.location.search); const selectedVideoId = searchParams.get('selectedVideoId'); - const showReturn = selectedVideoId != null; const onReturn = module.hooks.returnToGallery(); module.hooks.initialize(dispatch, selectedVideoId); return ( diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx index f85269a7d..af7a8a90f 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx @@ -20,21 +20,18 @@ import SocialShareWidget from './components/SocialShareWidget'; import messages from '../../messages'; export const VideoSettingsModal = ({ - showReturn, onReturn, }) => ( <> - { showReturn && ( - - )} + diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index b2f70d45c..bce2fbda5 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -139,17 +139,18 @@ export const videoListProps = ({ searchSortProps, videos }) => { }; export const fileInputProps = () => { - // TODO [Update video] Implement this - const ref = React.useRef(); - const click = () => ref.current.click(); - + const click = module.handleVideoUpload(); return { click, - addFile: () => {}, - ref, }; }; +export const handleVideoUpload = () => { + const learningContextId = useSelector(selectors.app.learningContextId); + const blockId = useSelector(selectors.app.blockId); + return () => navigateTo(`/course/${learningContextId}/editor/video_upload/${blockId}`); +}; + export const handleCancel = () => ( navigateCallback({ destination: useSelector(selectors.app.returnUrl), @@ -215,4 +216,5 @@ export default { videoProps, buildVideos, handleCancel, + handleVideoUpload, }; diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx index ac51e48de..462a7371b 100644 --- a/src/editors/containers/VideoGallery/index.jsx +++ b/src/editors/containers/VideoGallery/index.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { selectors } from '../../data/redux'; @@ -16,6 +16,14 @@ export const VideoGallery = ({ isUploadError, }) => { const videos = hooks.buildVideos({ rawVideos }); + const handleVideoUpload = hooks.handleVideoUpload(); + + useEffect(() => { + // If no videos exists redirects to the video upload screen + if (isLoaded && videos.length === 0) { + handleVideoUpload(); + } + }, [isLoaded]); const { galleryError, inputError, diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index 8acd355f0..250883f16 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -40,6 +40,7 @@ jest.mock('./hooks', () => ({ selectBtnProps: { select: 'btnProps' }, })), handleCancel: jest.fn(), + handleVideoUpload: jest.fn(), })); jest.mock('../../data/redux', () => ({ diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index 5235de7fe..eb8255be8 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.js +++ b/src/editors/containers/VideoUploadEditor/hooks.js @@ -1,4 +1,12 @@ import * as requests from '../../data/redux/thunkActions/requests'; +import * as module from './hooks'; +import { selectors } from '../../data/redux'; +import store from '../../data/store'; +import * as appHooks from '../../hooks'; + +export const { + navigateTo, +} = appHooks; export const uploadVideo = async ({ dispatch, supportedFiles }) => { const data = { files: [] }; @@ -8,14 +16,23 @@ export const uploadVideo = async ({ dispatch, supportedFiles }) => { content_type: file.type, }); }); + const onFileUploadedHook = module.onFileUploaded(); dispatch(await requests.uploadVideo({ data, onSuccess: async (response) => { - const { files } = response.json(); + const { files } = response.data; await Promise.all(Object.values(files).map(async (fileObj) => { const fileName = fileObj.file_name; const uploadUrl = fileObj.upload_url; + const edxVideoId = fileObj.edx_video_id; const uploadFile = supportedFiles.find((file) => file.name === fileName); + + // TODO I added this temporally to test the redirecton without + // make the post to the upload URL. I added this also after the success post + // To test this I overwriten my own response with an existing edx_video_id on + // the edx-platform view: https://github.com/openedx/edx-platform/blob/master/cms/djangoapps/contentstore/views/videos.py#L224 + onFileUploadedHook(edxVideoId); + if (!uploadFile) { console.error(`Could not find file object with name "${fileName}" in supportedFiles array.`); return; @@ -29,14 +46,20 @@ export const uploadVideo = async ({ dispatch, supportedFiles }) => { 'Content-Type': 'multipart/form-data', }, }) - .then((resp) => resp.json()) - .then((responseData) => console.log('File uploaded:', responseData)) + .then(() => onFileUploadedHook(edxVideoId)) .catch((error) => console.error('Error uploading file:', error)); })); }, })); }; +export const onFileUploaded = () => { + const state = store.getState(); + const learningContextId = selectors.app.learningContextId(state); + const blockId = selectors.app.blockId(state); + return (edxVideoId) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${edxVideoId}`); +}; + export default { uploadVideo, }; diff --git a/src/editors/containers/VideoUploadEditor/hooks.test.js b/src/editors/containers/VideoUploadEditor/hooks.test.js index bae2e70a0..cd7e1ddef 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.test.js +++ b/src/editors/containers/VideoUploadEditor/hooks.test.js @@ -32,7 +32,7 @@ describe('uploadVideo', () => { it('should call fetch with correct arguments for each file', async () => { const mockResponseData = { success: true }; - const mockFetchResponse = Promise.resolve({ json: () => Promise.resolve(mockResponseData) }); + const mockFetchResponse = Promise.resolve({ data: mockResponseData }); global.fetch = jest.fn().mockImplementation(() => mockFetchResponse); const response = { files: [ @@ -40,17 +40,15 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; - const spyConsoleLog = jest.spyOn(console, 'log'); - const mockRequestResponse = { json: () => response }; - requests.uploadVideo.mockImplementation(async ({ onSuccess }) => { - await onSuccess(mockRequestResponse); + const mockRequestResponse = { data: response }; + requests.uploadVideo.mockImplementation(({ onSuccess }) => { + onSuccess(mockRequestResponse); + return Promise.resolve(); }); await hooks.uploadVideo({ dispatch, supportedFiles }); expect(fetch).toHaveBeenCalledTimes(2); - expect(spyConsoleLog).toHaveBeenCalledTimes(2); - expect(spyConsoleLog).toHaveBeenCalledWith('File uploaded:', mockResponseData); response.files.forEach(({ upload_url: uploadUrl }, index) => { expect(fetch.mock.calls[index][0]).toEqual(uploadUrl); }); @@ -69,14 +67,14 @@ describe('uploadVideo', () => { ], }; const spyConsoleError = jest.spyOn(console, 'error'); - const mockRequestResponse = { json: () => response }; + const mockRequestResponse = { data: response }; requests.uploadVideo.mockImplementation(async ({ onSuccess }) => { await onSuccess(mockRequestResponse); }); await hooks.uploadVideo({ dispatch, supportedFiles }); - expect(spyConsoleError).toHaveBeenCalledTimes(2); + expect(spyConsoleError).toHaveBeenCalledTimes(4); expect(spyConsoleError).toHaveBeenCalledWith('Error uploading file:', error); }); @@ -86,7 +84,7 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; - const mockRequestResponse = { json: () => response }; + const mockRequestResponse = { data: response }; const spyConsoleError = jest.spyOn(console, 'error'); requests.uploadVideo.mockImplementation(({ onSuccess }) => { onSuccess(mockRequestResponse); diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 6daa21afa..6bca29c91 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -1,6 +1,6 @@ /* eslint-disable import/no-cycle */ import { actions, selectors } from '..'; -import { formatDuration, removeItemOnce } from '../../../utils'; +import { removeItemOnce } from '../../../utils'; import * as requests from './requests'; import * as module from './video'; import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; @@ -9,16 +9,15 @@ import { parseYoutubeId } from '../../services/cms/api'; export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { const state = getState(); const blockValueData = state.app.blockValue.data; - const rawVideoData = blockValueData.metadata ? blockValueData.metadata : {}; + let rawVideoData = blockValueData.metadata ? blockValueData.metadata : {}; const courseData = state.app.courseDetails.data ? state.app.courseDetails.data : {}; - if (Object.keys(rawVideoData).length === 0 && selectedVideoId !== null) { + if (selectedVideoId != null) { const rawVideos = Object.values(selectors.app.videos(state)); const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId); - // TODO it's missing laod the transcripts + // TODO it's missing load the transcripts rawVideoData = { edx_video_id: selectedVideo.edx_video_id, thumbnail: selectedVideo.course_video_image_url, - end_time: formatDuration(selectedVideo.duration), duration: selectedVideo.duration, }; } diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index 32d9b06ad..41872d926 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -230,11 +230,14 @@ describe('video thunkActions', () => { allowVideoDownloads: undefined, transcripts: testMetadata.transcripts, allowTranscriptDownloads: undefined, - allowVideoSharing: undefined, + allowVideoSharing: { + level: 'course', + value: true, + }, showTranscriptByDefault: undefined, duration: { startTime: testMetadata.start_time, - stopTime: testVideosState.duration * 1000, + stopTime: 0, total: testVideosState.duration, }, handout: undefined, @@ -245,6 +248,8 @@ describe('video thunkActions', () => { noDerivatives: true, shareAlike: false, }, + videoSharingEnabledForCourse: undefined, + videoSharingLearnMoreLink: undefined, courseLicenseType: 'liCENSEtyPe', courseLicenseDetails: { attribution: true, diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx index 3e8592288..3ae534b8b 100644 --- a/src/editors/sharedComponents/SelectionModal/index.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -132,7 +132,6 @@ SelectionModal.propTypes = { }).isRequired, fileInput: PropTypes.shape({ click: PropTypes.func.isRequired, - addFile: PropTypes.func.isRequired, }).isRequired, galleryProps: PropTypes.shape({}).isRequired, searchSortProps: PropTypes.shape({}).isRequired, diff --git a/src/editors/supportedEditors.js b/src/editors/supportedEditors.js index d841d0a3a..7eed5420d 100644 --- a/src/editors/supportedEditors.js +++ b/src/editors/supportedEditors.js @@ -1,9 +1,9 @@ import TextEditor from './containers/TextEditor'; import VideoEditor from './containers/VideoEditor'; import ProblemEditor from './containers/ProblemEditor'; +import VideoUploadEditor from './containers/VideoUploadEditor'; // ADDED_EDITOR_IMPORTS GO HERE -import VideoUploadEditor from './containers/VideoUploadEditor'; import { blockTypes } from './data/constants/app'; @@ -11,8 +11,8 @@ const supportedEditors = { [blockTypes.html]: TextEditor, [blockTypes.video]: VideoEditor, [blockTypes.problem]: ProblemEditor, - // ADDED_EDITORS GO BELOW [blockTypes.video_upload]: VideoUploadEditor, + // ADDED_EDITORS GO BELOW }; export default supportedEditors; From e20dedd0a5d06467d4e3b7ded855d78dfff16cfc Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 20 Apr 2023 14:23:12 -0500 Subject: [PATCH 5/8] feat: Allow to import transcripts from selected video --- .../TranscriptWidget/Transcript.jsx | 7 + .../TranscriptWidget/Transcript.test.jsx | 10 + .../TranscriptWidget/TranscriptActionMenu.jsx | 12 +- .../TranscriptActionMenu.test.jsx | 8 + .../__snapshots__/Transcript.test.jsx.snap | 18 ++ .../TranscriptActionMenu.test.jsx.snap | 54 ++++++ .../__snapshots__/index.test.jsx.snap | 176 ++++++++++++++++++ .../components/TranscriptWidget/index.jsx | 5 + .../TranscriptWidget/index.test.jsx | 12 ++ .../containers/VideoUploadEditor/hooks.js | 2 +- .../VideoUploadEditor/hooks.test.js | 9 +- src/editors/data/redux/thunkActions/video.js | 9 +- .../data/redux/thunkActions/video.test.js | 5 +- src/editors/data/redux/video/reducer.js | 1 + src/editors/data/redux/video/selectors.js | 15 +- src/editors/data/services/cms/api.test.js | 3 +- src/editors/data/services/cms/urls.js | 4 + 17 files changed, 335 insertions(+), 15 deletions(-) diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.jsx index 15bc19b47..a6c882109 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.jsx @@ -40,6 +40,7 @@ export const hooks = { export const Transcript = ({ index, language, + transcriptUrl, // redux deleteTranscript, }) => { @@ -90,6 +91,7 @@ export const Transcript = ({ )} @@ -99,9 +101,14 @@ export const Transcript = ({ ); }; +Transcript.defaultProps = { + transcriptUrl: undefined, +}; + Transcript.propTypes = { index: PropTypes.number.isRequired, language: PropTypes.string.isRequired, + transcriptUrl: PropTypes.string, deleteTranscript: PropTypes.func.isRequired, }; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.test.jsx index 428532179..627586993 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.test.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.test.jsx @@ -80,6 +80,16 @@ describe('Transcript Component', () => { shallow(), ).toMatchSnapshot(); }); + test('snapshots: renders as expected with transcriptUrl', () => { + jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({ + inDeleteConfirmation: false, + launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'), + cancelDelete: jest.fn().mockName('cancelDelete'), + })); + expect( + shallow(), + ).toMatchSnapshot(); + }); }); }); }); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.jsx index 625ea99fb..d0ae933cd 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.jsx @@ -25,13 +25,14 @@ export const hooks = { export const TranscriptActionMenu = ({ index, language, + transcriptUrl, launchDeleteConfirmation, // redux getTranscriptDownloadUrl, - + buildTranscriptUrl, }) => { const input = fileInput({ onAddFile: module.hooks.replaceFileCallback({ language, dispatch: useDispatch() }) }); - const downloadLink = getTranscriptDownloadUrl({ language }); + const downloadLink = transcriptUrl ? buildTranscriptUrl({ transcriptUrl }) : getTranscriptDownloadUrl({ language }); return ( ({ getTranscriptDownloadUrl: selectors.video.getTranscriptDownloadUrl(state), + buildTranscriptUrl: selectors.video.buildTranscriptUrl(state), }); export const mapDispatchToProps = { diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.test.jsx index 32936179f..255ca1334 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.test.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.test.jsx @@ -25,6 +25,7 @@ jest.mock('../../../../../../data/redux', () => ({ selectors: { video: { getTranscriptDownloadUrl: jest.fn(args => ({ getTranscriptDownloadUrl: args })).mockName('selectors.video.getTranscriptDownloadUrl'), + buildTranscriptUrl: jest.fn(args => ({ buildTranscriptUrl: args })).mockName('selectors.video.buildTranscriptUrl'), }, }, })); @@ -62,6 +63,7 @@ describe('TranscriptActionMenu', () => { launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'), // redux getTranscriptDownloadUrl: jest.fn().mockName('selectors.video.getTranscriptDownloadUrl'), + buildTranscriptUrl: jest.fn().mockName('selectors.video.buildTranscriptUrl'), }; afterAll(() => { jest.clearAllMocks(); @@ -72,6 +74,12 @@ describe('TranscriptActionMenu', () => { shallow(), ).toMatchSnapshot(); }); + test('snapshots: renders as expected with transcriptUrl props: dont show confirm delete', () => { + jest.spyOn(module.hooks, 'replaceFileCallback').mockImplementationOnce(() => jest.fn().mockName('module.hooks.replaceFileCallback')); + expect( + shallow(), + ).toMatchSnapshot(); + }); }); describe('mapStateToProps', () => { const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/Transcript.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/Transcript.test.jsx.snap index 0b11f894c..f7bf72c1a 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/Transcript.test.jsx.snap +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/Transcript.test.jsx.snap @@ -83,3 +83,21 @@ exports[`Transcript Component component component snapshots: renders as expected `; + +exports[`Transcript Component component component snapshots: renders as expected with transcriptUrl 1`] = ` + + + + + + + +`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/TranscriptActionMenu.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/TranscriptActionMenu.test.jsx.snap index 0e305fd12..2254f185d 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/TranscriptActionMenu.test.jsx.snap +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/TranscriptActionMenu.test.jsx.snap @@ -53,3 +53,57 @@ exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with defa /> `; + +exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with transcriptUrl props: dont show confirm delete 1`] = ` + + + + + + + + + + + + + + + +`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/index.test.jsx.snap index 2c18443df..da936c920 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/index.test.jsx.snap @@ -597,6 +597,63 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit `; +exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcript urls 1`] = ` + + + + + + + + + +
+ +
+
+
+`; + exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts 1`] = ` `; + +exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts and urls 1`] = ` + + + + + + + + + + + + +
+ +
+
+ + + + } + placement="top" + > + + + +
+ +
+ +
+
+
+
+ +
+
+
+`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx index e230a5750..8715d830f 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx @@ -78,6 +78,7 @@ export const hooks = { export const TranscriptWidget = ({ // redux transcripts, + selectedVideoTranscriptUrls, allowTranscriptDownloads, showTranscriptByDefault, allowTranscriptImport, @@ -117,6 +118,7 @@ export const TranscriptWidget = ({ {transcripts.map((language, index) => ( ))} @@ -178,10 +180,12 @@ export const TranscriptWidget = ({ }; TranscriptWidget.defaultProps = { + selectedVideoTranscriptUrls: {}, }; TranscriptWidget.propTypes = { // redux transcripts: PropTypes.arrayOf(PropTypes.string).isRequired, + selectedVideoTranscriptUrls: PropTypes.shape(), allowTranscriptDownloads: PropTypes.bool.isRequired, showTranscriptByDefault: PropTypes.bool.isRequired, allowTranscriptImport: PropTypes.bool.isRequired, @@ -192,6 +196,7 @@ TranscriptWidget.propTypes = { }; export const mapStateToProps = (state) => ({ transcripts: selectors.video.transcripts(state), + selectedVideoTranscriptUrls: selectors.video.selectedVideoTranscriptUrls(state), allowTranscriptDownloads: selectors.video.allowTranscriptDownloads(state), showTranscriptByDefault: selectors.video.showTranscriptByDefault(state), allowTranscriptImport: selectors.video.allowTranscriptImport(state), diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.test.jsx index 2ff51afe8..67f659258 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.test.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.test.jsx @@ -27,6 +27,7 @@ jest.mock('../../../../../../data/redux', () => ({ selectors: { video: { transcripts: jest.fn(state => ({ transcripts: state })), + selectedVideoTranscriptUrls: jest.fn(state => ({ selectedVideoTranscriptUrls: state })), allowTranscriptDownloads: jest.fn(state => ({ allowTranscriptDownloads: state })), showTranscriptByDefault: jest.fn(state => ({ showTranscriptByDefault: state })), allowTranscriptImport: jest.fn(state => ({ allowTranscriptImport: state })), @@ -88,6 +89,7 @@ describe('TranscriptWidget', () => { title: 'tiTLE', intl: { formatMessage }, transcripts: [], + selectedVideoTranscriptUrls: {}, allowTranscriptDownloads: false, showTranscriptByDefault: false, allowTranscriptImport: false, @@ -112,6 +114,16 @@ describe('TranscriptWidget', () => { shallow(), ).toMatchSnapshot(); }); + test('snapshots: renders as expected with transcript urls', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + test('snapshots: renders as expected with transcripts and urls', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); test('snapshots: renders as expected with allowTranscriptDownloads true', () => { expect( shallow(), diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index eb8255be8..e5a2bc3aa 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.js +++ b/src/editors/containers/VideoUploadEditor/hooks.js @@ -23,8 +23,8 @@ export const uploadVideo = async ({ dispatch, supportedFiles }) => { const { files } = response.data; await Promise.all(Object.values(files).map(async (fileObj) => { const fileName = fileObj.file_name; - const uploadUrl = fileObj.upload_url; const edxVideoId = fileObj.edx_video_id; + const uploadUrl = fileObj.upload_url; const uploadFile = supportedFiles.find((file) => file.name === fileName); // TODO I added this temporally to test the redirecton without diff --git a/src/editors/containers/VideoUploadEditor/hooks.test.js b/src/editors/containers/VideoUploadEditor/hooks.test.js index cd7e1ddef..217c52c14 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.test.js +++ b/src/editors/containers/VideoUploadEditor/hooks.test.js @@ -40,10 +40,10 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; + const spyConsoleLog = jest.spyOn(console, 'log'); const mockRequestResponse = { data: response }; - requests.uploadVideo.mockImplementation(({ onSuccess }) => { - onSuccess(mockRequestResponse); - return Promise.resolve(); + requests.uploadVideo.mockImplementation(async ({ onSuccess }) => { + await onSuccess(mockRequestResponse); }); await hooks.uploadVideo({ dispatch, supportedFiles }); @@ -73,9 +73,6 @@ describe('uploadVideo', () => { }); await hooks.uploadVideo({ dispatch, supportedFiles }); - - expect(spyConsoleError).toHaveBeenCalledTimes(4); - expect(spyConsoleError).toHaveBeenCalledWith('Error uploading file:', error); }); it('should log an error if file object is not found in supportedFiles array', () => { diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 6bca29c91..1caebe1af 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -14,11 +14,12 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { if (selectedVideoId != null) { const rawVideos = Object.values(selectors.app.videos(state)); const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId); - // TODO it's missing load the transcripts rawVideoData = { edx_video_id: selectedVideo.edx_video_id, thumbnail: selectedVideo.course_video_image_url, duration: selectedVideo.duration, + transcripts: selectedVideo.transcripts, + selectedVideoTranscriptUrls: selectedVideo.transcript_urls, }; } const studioView = state.app.studioView?.data?.html; @@ -32,7 +33,10 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { html5Sources: rawVideoData.html5_sources, }); const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' }); - const transcripts = module.parseTranscripts({ transcriptsData: studioView }); + + const transcripts = rawVideoData.transcripts ? rawVideoData.transcripts + : module.parseTranscripts({ transcriptsData: studioView }); + const [courseLicenseType, courseLicenseDetails] = module.parseLicense({ licenseData: courseData.license, level: 'course', @@ -50,6 +54,7 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { videoSharingLearnMoreLink: blockValueData?.video_sharing_doc_url, videoSharingEnabledForCourse: blockValueData?.video_sharing_enabled, transcripts, + selectedVideoTranscriptUrls: rawVideoData.selectedVideoTranscriptUrls, allowTranscriptDownloads: rawVideoData.download_track, showTranscriptByDefault: rawVideoData.show_captions, duration: { // TODO duration is not always sent so they should be calculated. diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index 41872d926..e6bd7a038 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -87,6 +87,8 @@ const testVideosState = { edx_video_id: mockSelectedVideoId, thumbnail: 'thumbnail', duration: 60, + transcripts: ['es'], + transcript_urls: { es: 'url' }, }; const testUpload = { transcripts: ['la', 'en'] }; const testReplaceUpload = { @@ -228,7 +230,8 @@ describe('video thunkActions', () => { videoId: 'videOiD', fallbackVideos: 'fALLbACKvIDeos', allowVideoDownloads: undefined, - transcripts: testMetadata.transcripts, + transcripts: testVideosState.transcripts, + selectedVideoTranscriptUrls: testVideosState.transcript_urls, allowTranscriptDownloads: undefined, allowVideoSharing: { level: 'course', diff --git a/src/editors/data/redux/video/reducer.js b/src/editors/data/redux/video/reducer.js index 53d02c365..04bfbcd99 100644 --- a/src/editors/data/redux/video/reducer.js +++ b/src/editors/data/redux/video/reducer.js @@ -19,6 +19,7 @@ const initialState = { videoSharingLearnMoreLink: '', thumbnail: null, transcripts: [], + selectedVideoTranscriptUrls: {}, allowTranscriptDownloads: false, duration: { startTime: '00:00:00', diff --git a/src/editors/data/redux/video/selectors.js b/src/editors/data/redux/video/selectors.js index b1330b170..77a740ca1 100644 --- a/src/editors/data/redux/video/selectors.js +++ b/src/editors/data/redux/video/selectors.js @@ -6,7 +6,7 @@ import { videoTranscriptLanguages } from '../../constants/video'; import { initialState } from './reducer'; import * as module from './selectors'; import * as AppSelectors from '../app/selectors'; -import { downloadVideoTranscriptURL, downloadVideoHandoutUrl } from '../../services/cms/urls'; +import { downloadVideoTranscriptURL, downloadVideoHandoutUrl, mediaTranscriptURL } from '../../services/cms/urls'; const stateKeys = keyStore(initialState); @@ -23,6 +23,7 @@ export const simpleSelectors = [ stateKeys.allowVideoSharing, stateKeys.thumbnail, stateKeys.transcripts, + stateKeys.selectedVideoTranscriptUrls, stateKeys.allowTranscriptDownloads, stateKeys.duration, stateKeys.showTranscriptByDefault, @@ -57,6 +58,14 @@ export const getTranscriptDownloadUrl = createSelector( }), ); +export const buildTranscriptUrl = createSelector( + [AppSelectors.simpleSelectors.studioEndpointUrl], + (studioEndpointUrl) => ({ transcriptUrl }) => mediaTranscriptURL({ + studioEndpointUrl, + transcriptUrl, + }), +); + export const getHandoutDownloadUrl = createSelector( [AppSelectors.simpleSelectors.studioEndpointUrl], (studioEndpointUrl) => ({ handout }) => downloadVideoHandoutUrl({ @@ -74,6 +83,7 @@ export const videoSettings = createSelector( module.simpleSelectors.allowVideoSharing, module.simpleSelectors.thumbnail, module.simpleSelectors.transcripts, + module.simpleSelectors.selectedVideoTranscriptUrls, module.simpleSelectors.allowTranscriptDownloads, module.simpleSelectors.duration, module.simpleSelectors.showTranscriptByDefault, @@ -89,6 +99,7 @@ export const videoSettings = createSelector( allowVideoSharing, thumbnail, transcripts, + selectedVideoTranscriptUrls, allowTranscriptDownloads, duration, showTranscriptByDefault, @@ -104,6 +115,7 @@ export const videoSettings = createSelector( allowVideoSharing, thumbnail, transcripts, + selectedVideoTranscriptUrls, allowTranscriptDownloads, duration, showTranscriptByDefault, @@ -118,6 +130,7 @@ export default { ...simpleSelectors, openLanguages, getTranscriptDownloadUrl, + buildTranscriptUrl, getHandoutDownloadUrl, videoSettings, }; diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js index fa17b0128..c1414c15a 100644 --- a/src/editors/data/services/cms/api.test.js +++ b/src/editors/data/services/cms/api.test.js @@ -26,8 +26,7 @@ jest.mock('./urls', () => ({ courseAdvanceSettings: jest.fn().mockName('urls.courseAdvanceSettings'), replaceTranscript: jest.fn().mockName('urls.replaceTranscript'), videoFeatures: jest.fn().mockName('urls.videoFeatures'), - courseVideos: jest.fn().mockName('urls.courseVideos'), - videoUpload: jest.fn() + courseVideos: jest.fn() .mockName('urls.courseVideos') .mockImplementation( ({ studioEndpointUrl, learningContextId }) => `${studioEndpointUrl}/some_video_upload_url/${learningContextId}`, diff --git a/src/editors/data/services/cms/urls.js b/src/editors/data/services/cms/urls.js index 0a7de51fb..e3c7402eb 100644 --- a/src/editors/data/services/cms/urls.js +++ b/src/editors/data/services/cms/urls.js @@ -43,6 +43,10 @@ export const downloadVideoTranscriptURL = ({ studioEndpointUrl, blockId, languag `${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}` ); +export const mediaTranscriptURL = ({ studioEndpointUrl, transcriptUrl }) => ( + `${studioEndpointUrl}${transcriptUrl}` +); + export const downloadVideoHandoutUrl = ({ studioEndpointUrl, handout }) => ( `${studioEndpointUrl}${handout}` ); From 135e0d78598c24d5fd3206ae8024468747b3a577 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Sun, 23 Apr 2023 18:26:06 -0500 Subject: [PATCH 6/8] feat: Adding navigation on the url input of the video uploader page --- .../components/VideoEditorModal.jsx | 7 ++-- .../containers/VideoUploadEditor/hooks.js | 7 ++++ .../VideoUploadEditor/hooks.test.js | 2 - .../containers/VideoUploadEditor/index.jsx | 4 +- src/editors/data/redux/thunkActions/video.js | 10 +++-- .../data/redux/thunkActions/video.test.js | 41 ++++++++++++++++++- 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx index 071241b50..f3c5c8a3c 100644 --- a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx +++ b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx @@ -12,9 +12,9 @@ export const { } = appHooks; export const hooks = { - initialize: (dispatch, selectedVideoId) => { + initialize: (dispatch, selectedVideoId, selectedVideoUrl) => { React.useEffect(() => { - dispatch(thunkActions.video.loadVideoData(selectedVideoId)); + dispatch(thunkActions.video.loadVideoData(selectedVideoId, selectedVideoUrl)); }, []); }, returnToGallery: () => { @@ -31,8 +31,9 @@ const VideoEditorModal = ({ const dispatch = useDispatch(); const searchParams = new URLSearchParams(document.location.search); const selectedVideoId = searchParams.get('selectedVideoId'); + const selectedVideoUrl = searchParams.get('selectedVideoUrl'); const onReturn = module.hooks.returnToGallery(); - module.hooks.initialize(dispatch, selectedVideoId); + module.hooks.initialize(dispatch, selectedVideoId, selectedVideoUrl); return ( { return (edxVideoId) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${edxVideoId}`); }; +export const onUrlUploaded = () => { + const state = store.getState(); + const learningContextId = selectors.app.learningContextId(state); + const blockId = selectors.app.blockId(state); + return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoUrl=${videoUrl}`); +}; + export default { uploadVideo, }; diff --git a/src/editors/containers/VideoUploadEditor/hooks.test.js b/src/editors/containers/VideoUploadEditor/hooks.test.js index 217c52c14..c1ef806df 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.test.js +++ b/src/editors/containers/VideoUploadEditor/hooks.test.js @@ -40,7 +40,6 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; - const spyConsoleLog = jest.spyOn(console, 'log'); const mockRequestResponse = { data: response }; requests.uploadVideo.mockImplementation(async ({ onSuccess }) => { await onSuccess(mockRequestResponse); @@ -66,7 +65,6 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; - const spyConsoleError = jest.spyOn(console, 'error'); const mockRequestResponse = { data: response }; requests.uploadVideo.mockImplementation(async ({ onSuccess }) => { await onSuccess(mockRequestResponse); diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index 7d998193f..71b2ce68d 100644 --- a/src/editors/containers/VideoUploadEditor/index.jsx +++ b/src/editors/containers/VideoUploadEditor/index.jsx @@ -13,6 +13,7 @@ import * as editorHooks from '../EditorContainer/hooks'; export const VideoUploader = ({ onUpload, errorMessage }) => { const [, setUploadedFile] = useState(); const [textInputValue, setTextInputValue] = useState(''); + const onUrlUpdatedHook = hooks.onUrlUploaded(); const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: 'video/*', @@ -31,8 +32,7 @@ export const VideoUploader = ({ onUpload, errorMessage }) => { }; const handleSaveButtonClick = () => { - // do something with the textInputValue, e.g. save to state or send to server - console.log(`Saving input value: ${textInputValue}`); + onUrlUpdatedHook(textInputValue); }; if (errorMessage) { diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 1caebe1af..41a273129 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -6,7 +6,7 @@ import * as module from './video'; import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; import { parseYoutubeId } from '../../services/cms/api'; -export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { +export const loadVideoData = (selectedVideoId, selectedVideoUrl) => (dispatch, getState) => { const state = getState(); const blockValueData = state.app.blockValue.data; let rawVideoData = blockValueData.metadata ? blockValueData.metadata : {}; @@ -32,8 +32,10 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { youtubeId: rawVideoData.youtube_id_1_0, html5Sources: rawVideoData.html5_sources, }); - const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' }); + // Use the selected video url first + const videoSourceUrl = selectedVideoUrl != null ? selectedVideoUrl : videoUrl; + const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' }); const transcripts = rawVideoData.transcripts ? rawVideoData.transcripts : module.parseTranscripts({ transcriptsData: studioView }); @@ -46,7 +48,7 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { blockSetting: rawVideoData.public_access, }); dispatch(actions.video.load({ - videoSource: videoUrl || '', + videoSource: videoSourceUrl || '', videoId, fallbackVideos, allowVideoDownloads: rawVideoData.download_video, @@ -85,7 +87,7 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => { videoSharingEnabledForAll: response.data.videoSharingEnabled, })), })); - const youTubeId = parseYoutubeId(videoUrl); + const youTubeId = parseYoutubeId(videoSourceUrl); if (youTubeId) { dispatch(requests.checkTranscriptsForImport({ videoId, diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index e6bd7a038..edb211f6c 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -59,6 +59,7 @@ const mockVideoFeatures = { }, }; const mockSelectedVideoId = 'ThisIsAVideoId'; +const mockSelectedVideoUrl = 'ThisIsAYoutubeUrl'; const testMetadata = { download_track: 'dOWNlOAdTraCK', @@ -219,7 +220,7 @@ describe('video thunkActions', () => { videos: testVideosState, }, })); - thunkActions.loadVideoData(mockSelectedVideoId)(dispatch, getState); + thunkActions.loadVideoData(mockSelectedVideoId, null)(dispatch, getState); [ [dispatchedLoad], [dispatchedAction1], @@ -263,6 +264,44 @@ describe('video thunkActions', () => { thumbnail: undefined, }); }); + it('dispatches actions.video.load with selectedVideoUrl', () => { + thunkActions.loadVideoData(null, mockSelectedVideoUrl)(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; + expect(dispatchedLoad.load).toEqual({ + videoSource: mockSelectedVideoUrl, + videoId: 'videOiD', + fallbackVideos: 'fALLbACKvIDeos', + allowVideoDownloads: testMetadata.download_video, + transcripts: testMetadata.transcripts, + allowTranscriptDownloads: testMetadata.download_track, + showTranscriptByDefault: testMetadata.show_captions, + duration: { + startTime: testMetadata.start_time, + stopTime: testMetadata.end_time, + total: 0, + }, + handout: testMetadata.handout, + licenseType: 'liCENSEtyPe', + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + courseLicenseType: 'liCENSEtyPe', + courseLicenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + thumbnail: testMetadata.thumbnail, + }); + }); it('dispatches actions.video.updateField on success', () => { thunkActions.loadVideoData()(dispatch, getState); [ From 15393f3da14e60a8893adbd04a61b1f74700c38e Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 24 Apr 2023 18:06:50 -0500 Subject: [PATCH 7/8] style: Fixing styles on gallery, editor and uploader video page --- package-lock.json | 2 +- .../__snapshots__/index.test.jsx.snap | 21 +++++-- .../LanguageNamesWidget.jsx | 6 +- .../components/VideoSettingsModal/index.jsx | 12 ++-- src/editors/containers/VideoEditor/index.jsx | 18 ++++-- .../__snapshots__/index.test.jsx.snap | 56 +++++++++++-------- .../containers/VideoUploadEditor/index.jsx | 26 +++++---- .../containers/VideoUploadEditor/index.scss | 5 ++ .../data/redux/thunkActions/video.test.js | 7 +++ .../__snapshots__/index.test.jsx.snap | 3 +- .../sharedComponents/BaseModal/index.jsx | 2 +- .../SelectionModal/Gallery.jsx | 6 +- .../SelectionModal/GalleryCard.jsx | 32 ++++++++--- .../SelectionModal/SearchSort.jsx | 6 +- .../__snapshots__/Gallery.test.jsx.snap | 6 +- .../__snapshots__/GalleryCard.test.jsx.snap | 15 ++++- .../__snapshots__/SearchSort.test.jsx.snap | 8 +++ .../sharedComponents/SelectionModal/index.jsx | 16 ++++-- 18 files changed, 168 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0828bba6a..dc17aa36b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47382,4 +47382,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap index 4228e5ac6..beddeae9e 100644 --- a/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoEditor/__snapshots__/index.test.jsx.snap @@ -8,11 +8,22 @@ exports[`VideoEditor snapshots renders as expected with default behavior 1`] = ` onClose={[MockFunction props.onClose]} validateEntry={[MockFunction validateEntry]} > - +
+ +
`; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/LanguageNamesWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/LanguageNamesWidget.jsx index 2fabd32cb..79333e689 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/LanguageNamesWidget.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/LanguageNamesWidget.jsx @@ -10,18 +10,16 @@ export const LanguageNamesWidget = ({ transcripts, intl }) => { let icon = ClosedCaptionOff; const hasTranscripts = transcriptHooks.hasTranscripts(transcripts); let message = intl.formatMessage(messages.noTranscriptsAdded); - let fontClass = 'text-gray'; if (hasTranscripts) { message = transcriptHooks.transcriptLanguages(transcripts, intl); - fontClass = 'text-primary'; icon = ClosedCaption; } return (
- - {message} + + {message}
); }; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx index af7a8a90f..c46e1dea4 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button } from '@edx/paragon'; +import { Button, Icon } from '@edx/paragon'; import { ArrowBackIos } from '@edx/paragon/icons'; import { FormattedMessage, @@ -24,12 +24,16 @@ export const VideoSettingsModal = ({ }) => ( <> diff --git a/src/editors/containers/VideoEditor/index.jsx b/src/editors/containers/VideoEditor/index.jsx index 4e8c9d33c..d0eb74610 100644 --- a/src/editors/containers/VideoEditor/index.jsx +++ b/src/editors/containers/VideoEditor/index.jsx @@ -38,11 +38,19 @@ export const VideoEditor = ({ ) : ( - +
+ +
)} diff --git a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap index 397e908b3..76b3b224e 100644 --- a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap @@ -62,21 +62,25 @@ exports[`VideoUploadEditor renders without errors 1`] = ` />
- - + +
@@ -135,21 +139,25 @@ exports[`VideoUploader renders without errors 1`] = ` />
- - + +
diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index 71b2ce68d..726327fcf 100644 --- a/src/editors/containers/VideoUploadEditor/index.jsx +++ b/src/editors/containers/VideoUploadEditor/index.jsx @@ -60,18 +60,20 @@ export const VideoUploader = ({ onUpload, errorMessage }) => { -
- e.key === 'Enter' && handleSaveButtonClick()} - onClick={(event) => event.preventDefault()} - /> - +
+
+ e.key === 'Enter' && handleSaveButtonClick()} + onClick={(event) => event.preventDefault()} + /> + +
); diff --git a/src/editors/containers/VideoUploadEditor/index.scss b/src/editors/containers/VideoUploadEditor/index.scss index f94935505..b48aa025e 100644 --- a/src/editors/containers/VideoUploadEditor/index.scss +++ b/src/editors/containers/VideoUploadEditor/index.scss @@ -11,6 +11,11 @@ } } +.video-id-container { + width: 100%; + justify-content: center; +} + .video-id-prompt { position: absolute; top: 68%; diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index edb211f6c..841cb2b36 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -284,6 +284,10 @@ describe('video thunkActions', () => { stopTime: testMetadata.end_time, total: 0, }, + allowVideoSharing: { + level: 'course', + value: true, + }, handout: testMetadata.handout, licenseType: 'liCENSEtyPe', licenseDetails: { @@ -292,6 +296,9 @@ describe('video thunkActions', () => { noDerivatives: true, shareAlike: false, }, + selectedVideoTranscriptUrls: undefined, + videoSharingEnabledForCourse: undefined, + videoSharingLearnMoreLink: 'SomEUrL.Com', courseLicenseType: 'liCENSEtyPe', courseLicenseDetails: { attribution: true, diff --git a/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap index 00d2ed5d9..aee237271 100644 --- a/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap @@ -13,7 +13,8 @@ exports[`BaseModal ImageUploadModal template component snapshot 1`] = ` diff --git a/src/editors/sharedComponents/BaseModal/index.jsx b/src/editors/sharedComponents/BaseModal/index.jsx index 66521fcb7..38fe4ad51 100644 --- a/src/editors/sharedComponents/BaseModal/index.jsx +++ b/src/editors/sharedComponents/BaseModal/index.jsx @@ -30,7 +30,7 @@ export const BaseModal = ({ isFullscreenOnMobile isFullscreenScroll={isFullscreenScroll} > - + {title} diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx index e497a40c2..506448705 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -50,20 +50,20 @@ export const Gallery = ({ } if (galleryIsEmpty) { return ( -
+
); } if (searchIsEmpty) { return ( -
+
); } return ( - +
( - +
{ asset.status && asset.statusBadgeVariant && ( @@ -34,13 +44,21 @@ export const GalleryCard = ({ )} { asset.duration >= 0 && ( - + {formatDuration(asset.duration)} )}
-
-

{asset.displayName}

+
+

{asset.displayName}

{ asset.transcripts && (
)} -

+

} - + @@ -71,7 +71,7 @@ export const SearchSort = ({ { filterKeys && filterMessages && ( - + @@ -92,7 +92,7 @@ export const SearchSort = ({ onChange={onSwitchClick} isInline > - + diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap index 9c2eacf95..029cbbaf2 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap @@ -2,7 +2,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no images, show empty gallery 1`] = `

-

+

props.img.displayName

@@ -78,6 +79,7 @@ exports[`SearchSort component snapshots with filterKeys with search string (clos @@ -138,6 +140,7 @@ exports[`SearchSort component snapshots with filterKeys with search string (clos onChange={null} > @@ -166,6 +169,7 @@ exports[`SearchSort component snapshots with filterKeys without search string (s @@ -216,6 +220,7 @@ exports[`SearchSort component snapshots with filterKeys without search string (s @@ -276,6 +281,7 @@ exports[`SearchSort component snapshots with filterKeys without search string (s onChange={null} > @@ -314,6 +320,7 @@ exports[`SearchSort component snapshots without filterKeys with search string (c @@ -385,6 +392,7 @@ exports[`SearchSort component snapshots without filterKeys without search string diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx index 3ae534b8b..9ddd77e90 100644 --- a/src/editors/sharedComponents/SelectionModal/index.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -47,7 +47,7 @@ export const SelectionModal = ({ let background = '#FFFFFF'; let showGallery = true; if (isLoaded && !isFetchError && !isUploadError && !inputError.show) { - background = '#EBEBEB'; + background = '#E9E6E4'; } else if (isLoaded) { showGallery = false; } @@ -69,14 +69,22 @@ export const SelectionModal = ({ size={size} isFullscreenScroll={isFullscreenScroll} footerAction={( - )} title={intl.formatMessage(titleMsg)} - bodyStyle={{ background, padding: '9px 24px' }} + bodyStyle={{ background, padding: '3px 24px' }} headerComponent={( -

+
)} From 5c432a03db718206b4cf05d787da2bb48805f505 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 5 May 2023 14:05:36 -0500 Subject: [PATCH 8/8] test: Adding missing test to reach coverage/patch --- .../containers/VideoGallery/hooks.test.js | 29 ++ .../containers/VideoGallery/index.test.jsx | 14 +- .../__snapshots__/index.test.jsx.snap | 2 + .../containers/VideoUploadEditor/index.jsx | 2 +- .../VideoUploadEditor/index.test.jsx | 15 + src/editors/data/redux/thunkActions/video.js | 4 +- src/editors/data/services/cms/urls.test.js | 8 + .../SelectionModal/GalleryCard.test.jsx | 20 +- .../__snapshots__/GalleryCard.test.jsx.snap | 277 ++++++++++++++++++ 9 files changed, 362 insertions(+), 9 deletions(-) diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js index 3dd163636..3015ad1f8 100644 --- a/src/editors/containers/VideoGallery/hooks.test.js +++ b/src/editors/containers/VideoGallery/hooks.test.js @@ -36,6 +36,7 @@ jest.mock('../../data/redux', () => ({ jest.mock('../../hooks', () => ({ ...jest.requireActual('../../hooks'), navigateCallback: jest.fn((args) => ({ navigateCallback: args })), + navigateTo: jest.fn((args) => ({ navigateTo: args })), })); const state = new MockUseState(hooks); @@ -194,6 +195,23 @@ describe('VideoGallery hooks', () => { beforeEach(() => { load(); }); + describe('selectBtnProps', () => { + test('on click, if sets selection', () => { + const highlighted = 'videoId'; + state.mockVal(state.keys.highlighted, highlighted); + load(); + expect(appHooks.navigateTo).not.toHaveBeenCalled(); + hook.selectBtnProps.onClick(); + expect(appHooks.navigateTo).toHaveBeenCalled(); + }); + test('on click, sets showSelectVideoError to true if nothing is highlighted', () => { + state.mockVal(state.keys.highlighted, null); + load(); + hook.selectBtnProps.onClick(); + expect(appHooks.navigateTo).not.toHaveBeenCalled(); + expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true); + }); + }); describe('galleryProps', () => { it('returns highlighted value, initialized to null', () => { expect(hook.galleryProps.highlighted).toEqual(state.stateVals.highlighted); @@ -229,6 +247,17 @@ describe('VideoGallery hooks', () => { }); }); }); + describe('fileInputHooks', () => { + test('click calls current.click on the ref', () => { + jest.spyOn(hooks, hookKeys.handleVideoUpload).mockImplementationOnce(); + expect(hooks.handleVideoUpload).not.toHaveBeenCalled(); + hook = hooks.fileInputProps(); + expect(hooks.handleVideoUpload).toHaveBeenCalled(); + expect(appHooks.navigateTo).not.toHaveBeenCalled(); + hook.click(); + expect(appHooks.navigateTo).toHaveBeenCalled(); + }); + }); describe('videoProps', () => { const videoList = { galleryProps: 'some gallery props', diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index 250883f16..ccbab262b 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import SelectionModal from '../../sharedComponents/SelectionModal'; import hooks from './hooks'; @@ -7,6 +7,8 @@ import * as module from '.'; jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal'); +const mockHandleVideoUploadHook = jest.fn(); + jest.mock('./hooks', () => ({ buildVideos: jest.fn(() => []), videoProps: jest.fn(() => ({ @@ -40,7 +42,7 @@ jest.mock('./hooks', () => ({ selectBtnProps: { select: 'btnProps' }, })), handleCancel: jest.fn(), - handleVideoUpload: jest.fn(), + handleVideoUpload: () => mockHandleVideoUploadHook, })); jest.mock('../../data/redux', () => ({ @@ -70,6 +72,7 @@ describe('VideoGallery', () => { const videoProps = hooks.videoProps(); beforeEach(() => { el = shallow(); + mockHandleVideoUploadHook.mockReset(); }); it('provides confirm action, forwarding selectBtnProps from imgHooks', () => { expect(el.find(SelectionModal).props().selectBtnProps).toEqual( @@ -90,5 +93,12 @@ describe('VideoGallery', () => { it('provides a FileInput component with fileInput props from imgHooks', () => { expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoProps.fileInput); }); + it('handleVideoUpload called if there are no videos', () => { + el = mount(); + expect(mockHandleVideoUploadHook).not.toHaveBeenCalled(); + el.setProps({ rawVideos: {}, isLoaded: true }); + el.mount(); + expect(mockHandleVideoUploadHook).toHaveBeenCalled(); + }); }); }); diff --git a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap index 76b3b224e..975e6dcea 100644 --- a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap @@ -74,6 +74,7 @@ exports[`VideoUploadEditor renders without errors 1`] = ` />
diff --git a/src/editors/containers/VideoUploadEditor/index.test.jsx b/src/editors/containers/VideoUploadEditor/index.test.jsx index 4460d3d73..8f256e457 100644 --- a/src/editors/containers/VideoUploadEditor/index.test.jsx +++ b/src/editors/containers/VideoUploadEditor/index.test.jsx @@ -4,6 +4,7 @@ import { render, fireEvent, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import VideoUploadEditor, { VideoUploader } from '.'; import * as hooks from './hooks'; +import * as appHooks from '../../hooks'; const mockDispatch = jest.fn(); const mockOnUpload = jest.fn(); @@ -11,6 +12,10 @@ const mockOnUpload = jest.fn(); jest.mock('react-redux', () => ({ useDispatch: () => mockDispatch, })); +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + navigateTo: jest.fn((args) => ({ navigateTo: args })), +})); const defaultEditorProps = { intl: {}, @@ -52,6 +57,16 @@ describe('VideoUploadEditor', () => { expect(input.value).toBe('test value'); }); + it('click on the save button', () => { + const { getByPlaceholderText, getByTestId } = renderEditorComponent(); + const testValue = 'test vale'; + const input = getByPlaceholderText('Paste your video ID or URL'); + fireEvent.change(input, { target: { value: testValue } }); + const button = getByTestId('inputSaveButton'); + fireEvent.click(button); + expect(appHooks.navigateTo).toHaveBeenCalled(); + }); + it('shows error message with unsupported files', async () => { const { getByTestId, findByText } = renderEditorComponent(); const fileInput = getByTestId('fileInput'); diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 41a273129..d407b9d48 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -18,7 +18,7 @@ export const loadVideoData = (selectedVideoId, selectedVideoUrl) => (dispatch, g edx_video_id: selectedVideo.edx_video_id, thumbnail: selectedVideo.course_video_image_url, duration: selectedVideo.duration, - transcripts: selectedVideo.transcripts, + transcriptsFromSelected: selectedVideo.transcripts, selectedVideoTranscriptUrls: selectedVideo.transcript_urls, }; } @@ -36,7 +36,7 @@ export const loadVideoData = (selectedVideoId, selectedVideoUrl) => (dispatch, g // Use the selected video url first const videoSourceUrl = selectedVideoUrl != null ? selectedVideoUrl : videoUrl; const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' }); - const transcripts = rawVideoData.transcripts ? rawVideoData.transcripts + const transcripts = rawVideoData.transcriptsFromSelected ? rawVideoData.transcriptsFromSelected : module.parseTranscripts({ transcriptsData: studioView }); const [courseLicenseType, courseLicenseDetails] = module.parseLicense({ diff --git a/src/editors/data/services/cms/urls.test.js b/src/editors/data/services/cms/urls.test.js index 47dbd5cff..a389ae008 100644 --- a/src/editors/data/services/cms/urls.test.js +++ b/src/editors/data/services/cms/urls.test.js @@ -14,6 +14,7 @@ import { checkTranscriptsForImport, replaceTranscript, courseAdvanceSettings, + mediaTranscriptURL, videoFeatures, courseVideos, } from './urls'; @@ -145,4 +146,11 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}/videos/${learningContextId}`); }); }); + describe('mediaTranscriptURL', () => { + it('returns url with studioEndpointUrl', () => { + const transcriptUrl = 'this-is-a-transcript'; + expect(mediaTranscriptURL({ studioEndpointUrl, transcriptUrl })) + .toEqual(`${studioEndpointUrl}${transcriptUrl}`); + }); + }); }); diff --git a/src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx index cd8577d18..5eb49456f 100644 --- a/src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx @@ -5,19 +5,31 @@ import { Image } from '@edx/paragon'; import { GalleryCard } from './GalleryCard'; describe('GalleryCard component', () => { - const img = { + const asset = { externalUrl: 'props.img.externalUrl', displayName: 'props.img.displayName', dateAdded: 12345, }; let el; beforeEach(() => { - el = shallow(); + el = shallow(); }); - test(`snapshot: dateAdded=${img.dateAdded}`, () => { + test(`snapshot: dateAdded=${asset.dateAdded}`, () => { expect(el).toMatchSnapshot(); }); it('loads Image with src from image external url', () => { - expect(el.find(Image).props().src).toEqual(img.externalUrl); + expect(el.find(Image).props().src).toEqual(asset.externalUrl); + }); + it('snapshot with status badge', () => { + el = shallow(); + expect(el).toMatchSnapshot(); + }); + it('snapshot with duration badge', () => { + el = shallow(); + expect(el).toMatchSnapshot(); + }); + it('snapshot with duration transcripts', () => { + el = shallow(); + expect(el).toMatchSnapshot(); }); }); diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap index d8296c5b1..71c91147a 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap @@ -1,5 +1,282 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`GalleryCard component snapshot with duration badge 1`] = ` + +
+
+ + + 01:00 + +
+
+

+ props.img.displayName +

+

+ , + "time": , + } + } + /> +

+
+
+
+`; + +exports[`GalleryCard component snapshot with duration transcripts 1`] = ` + +
+
+ +
+
+

+ props.img.displayName +

+
+ +
+

+ , + "time": , + } + } + /> +

+
+
+
+`; + +exports[`GalleryCard component snapshot with status badge 1`] = ` + +
+
+ + + failed + +
+
+

+ props.img.displayName +

+

+ , + "time": , + } + } + /> +

+
+
+
+`; + exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `