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 = ({ > - +
+ +
`; diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.jsx index 89df1bf81..f3c5c8a3c 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) => { + initialize: (dispatch, selectedVideoId, selectedVideoUrl) => { React.useEffect(() => { - dispatch(thunkActions.video.loadVideoData()); + dispatch(thunkActions.video.loadVideoData(selectedVideoId, selectedVideoUrl)); }, []); }, + returnToGallery: () => { + const learningContextId = useSelector(selectors.app.learningContextId); + const blockId = useSelector(selectors.app.blockId); + return () => (navigateTo(`/course/${learningContextId}/editor/course-videos/${blockId}`)); + }, }; const VideoEditorModal = ({ @@ -20,9 +29,18 @@ const VideoEditorModal = ({ isOpen, }) => { const dispatch = useDispatch(); - module.hooks.initialize(dispatch); + 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, selectedVideoUrl); return ( - + ); // TODO: add logic to show SelectVideoModal if no selection }; 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/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 3232d8804..c46e1dea4 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, Icon } 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,25 @@ 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 = ({ + onReturn, +}) => ( <> + @@ -26,4 +48,9 @@ export const VideoSettingsModal = () => ( ); +VideoSettingsModal.propTypes = { + showReturn: PropTypes.bool.isRequired, + onReturn: PropTypes.func.isRequired, +}; + export default 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/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 2eabcfc28..bce2fbda5 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -1,6 +1,10 @@ import React from 'react'; +import { useSelector } from 'react-redux'; import * as module from './hooks'; import messages from './messages'; +import * as appHooks from '../../hooks'; +import { selectors } from '../../data/redux'; +import analyticsEvt from '../../data/constants/analyticsEvt'; import { filterKeys, filterMessages, @@ -9,6 +13,11 @@ import { sortFunctions, } from './utils'; +export const { + navigateCallback, + navigateTo, +} = appHooks; + export const state = { highlighted: (val) => 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,25 +127,38 @@ export const videoListProps = ({ searchSortProps, videos }) => { height: '100%', }, selectBtnProps: { - onclick: () => { - // TODO Update this when implementing the selection feature + onClick: () => { + if (highlighted) { + navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`); + } else { + setShowSelectVideoError(true); + } }, }, }; }; 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), + analytics: useSelector(selectors.app.analytics), + analyticsEvent: analyticsEvt.videoGalleryCancelClick, + }) +); + export const buildVideos = ({ rawVideos }) => { let videos = []; const rawVideoList = Object.values(rawVideos); @@ -191,4 +215,6 @@ export const videoProps = ({ videos }) => { export default { videoProps, buildVideos, + handleCancel, + handleVideoUpload, }; diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js index d3ad83e97..3015ad1f8 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,25 @@ 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 })), + navigateTo: jest.fn((args) => ({ navigateTo: args })), +})); + const state = new MockUseState(hooks); const hookKeys = keyStore(hooks); let hook; @@ -175,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); @@ -210,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', @@ -250,4 +298,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..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, @@ -24,6 +32,7 @@ export const VideoGallery = ({ searchSortProps, selectBtnProps, } = hooks.videoProps({ videos }); + const handleCancel = hooks.handleCancel(); const modalMessages = { confirmMsg: messages.selectVideoButtonlabel, @@ -38,7 +47,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..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(() => ({ @@ -39,6 +41,8 @@ jest.mock('./hooks', () => ({ searchSortProps: { search: 'sortProps' }, selectBtnProps: { select: 'btnProps' }, })), + handleCancel: jest.fn(), + handleVideoUpload: () => mockHandleVideoUploadHook, })); jest.mock('../../data/redux', () => ({ @@ -51,6 +55,11 @@ jest.mock('../../data/redux', () => ({ }, })); +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + navigateCallback: jest.fn((args) => ({ navigateCallback: args })), +})); + describe('VideoGallery', () => { describe('component', () => { const props = { @@ -63,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( @@ -83,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 397e908b3..975e6dcea 100644 --- a/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoUploadEditor/__snapshots__/index.test.jsx.snap @@ -62,21 +62,26 @@ exports[`VideoUploadEditor renders without errors 1`] = ` />
- - + +
@@ -135,21 +140,26 @@ exports[`VideoUploader renders without errors 1`] = ` />
- - + +
diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index 5235de7fe..c6f9d8bb9 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 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 + // 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,27 @@ 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 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 bae2e70a0..c1ef806df 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,8 +40,7 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; - const spyConsoleLog = jest.spyOn(console, 'log'); - const mockRequestResponse = { json: () => response }; + const mockRequestResponse = { data: response }; requests.uploadVideo.mockImplementation(async ({ onSuccess }) => { await onSuccess(mockRequestResponse); }); @@ -49,8 +48,6 @@ describe('uploadVideo', () => { 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); }); @@ -68,16 +65,12 @@ describe('uploadVideo', () => { { file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' }, ], }; - 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).toHaveBeenCalledWith('Error uploading file:', error); }); it('should log an error if file object is not found in supportedFiles array', () => { @@ -86,7 +79,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/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index 7d998193f..54780e719 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) { @@ -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/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/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; diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index eb9c64805..d407b9d48 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -6,11 +6,22 @@ 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, selectedVideoUrl) => (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 (selectedVideoId != null) { + const rawVideos = Object.values(selectors.app.videos(state)); + const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId); + rawVideoData = { + edx_video_id: selectedVideo.edx_video_id, + thumbnail: selectedVideo.course_video_image_url, + duration: selectedVideo.duration, + transcriptsFromSelected: selectedVideo.transcripts, + selectedVideoTranscriptUrls: selectedVideo.transcript_urls, + }; + } const studioView = state.app.studioView?.data?.html; const { videoId, @@ -21,8 +32,13 @@ export const loadVideoData = () => (dispatch, getState) => { youtubeId: rawVideoData.youtube_id_1_0, html5Sources: rawVideoData.html5_sources, }); + + // Use the selected video url first + const videoSourceUrl = selectedVideoUrl != null ? selectedVideoUrl : videoUrl; const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' }); - const transcripts = module.parseTranscripts({ transcriptsData: studioView }); + const transcripts = rawVideoData.transcriptsFromSelected ? rawVideoData.transcriptsFromSelected + : module.parseTranscripts({ transcriptsData: studioView }); + const [courseLicenseType, courseLicenseDetails] = module.parseLicense({ licenseData: courseData.license, level: 'course', @@ -32,7 +48,7 @@ export const loadVideoData = () => (dispatch, getState) => { blockSetting: rawVideoData.public_access, }); dispatch(actions.video.load({ - videoSource: videoUrl || '', + videoSource: videoSourceUrl || '', videoId, fallbackVideos, allowVideoDownloads: rawVideoData.download_video, @@ -40,12 +56,13 @@ export const loadVideoData = () => (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. 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, @@ -70,7 +87,7 @@ export const loadVideoData = () => (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 29c7796af..841cb2b36 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,8 @@ const mockVideoFeatures = { videoSharingEnabled: 'soMEbOolEAn', }, }; +const mockSelectedVideoId = 'ThisIsAVideoId'; +const mockSelectedVideoUrl = 'ThisIsAYoutubeUrl'; const testMetadata = { download_track: 'dOWNlOAdTraCK', @@ -80,6 +84,13 @@ const testState = { originalThumbnail: null, videoId: 'soMEvIDEo', }; +const testVideosState = { + edx_video_id: mockSelectedVideoId, + thumbnail: 'thumbnail', + duration: 60, + transcripts: ['es'], + transcript_urls: { es: 'url' }, +}; const testUpload = { transcripts: ['la', 'en'] }; const testReplaceUpload = { file: mockFile, @@ -130,25 +141,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 +209,113 @@ 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, null)(dispatch, getState); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; + expect(dispatchedLoad.load).toEqual({ + videoSource: 'videOsOurce', + videoId: 'videOiD', + fallbackVideos: 'fALLbACKvIDeos', + allowVideoDownloads: undefined, + transcripts: testVideosState.transcripts, + selectedVideoTranscriptUrls: testVideosState.transcript_urls, + allowTranscriptDownloads: undefined, + allowVideoSharing: { + level: 'course', + value: true, + }, + showTranscriptByDefault: undefined, + duration: { + startTime: testMetadata.start_time, + stopTime: 0, + total: testVideosState.duration, + }, + handout: undefined, + licenseType: 'liCENSEtyPe', + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + videoSharingEnabledForCourse: undefined, + videoSharingLearnMoreLink: undefined, + courseLicenseType: 'liCENSEtyPe', + courseLicenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + 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, + }, + allowVideoSharing: { + level: 'course', + value: true, + }, + handout: testMetadata.handout, + licenseType: 'liCENSEtyPe', + licenseDetails: { + attribution: true, + noncommercial: true, + noDerivatives: true, + shareAlike: false, + }, + selectedVideoTranscriptUrls: undefined, + videoSharingEnabledForCourse: undefined, + videoSharingLearnMoreLink: 'SomEUrL.Com', + 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); + [ + [dispatchedLoad], + [dispatchedAction1], + [dispatchedAction2], + ] = dispatch.mock.calls; dispatch.mockClear(); dispatchedAction1.fetchVideoFeatures.onSuccess(mockVideoFeatures); expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ 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}` ); 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/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 && (
)} -

+

{ - 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/SearchSort.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx index 11a5f9145..762255877 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx @@ -57,7 +57,7 @@ export const SearchSort = ({ { !showSwitch && } - + @@ -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`] = `

+ + 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`] = ` + +
+
+ +
+
+

+ 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 3e8592288..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={( -

+
)} @@ -132,7 +140,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;