From 09bb1dab2b43634e9b1d518a4b37a4e20d9e85c2 Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Fri, 23 Dec 2022 10:54:07 -0500 Subject: [PATCH] feat: add import transcripts from youtube (#176) --- .../LicenseWidget/LicenseDisplay.jsx | 2 +- .../TranscriptWidget/ImportTranscriptCard.jsx | 60 ++++++++++ .../ImportTranscriptCard.test.jsx | 57 ++++++++++ .../ImportTranscriptCard.test.jsx.snap | 40 +++++++ .../__snapshots__/index.test.jsx.snap | 104 +++++++++++++++--- .../components/TranscriptWidget/index.jsx | 19 +++- .../TranscriptWidget/index.test.jsx | 13 ++- .../components/TranscriptWidget/messages.js | 15 +++ src/editors/data/constants/requests.js | 2 + src/editors/data/redux/requests/reducer.js | 3 +- .../data/redux/thunkActions/requests.js | 27 +++++ .../data/redux/thunkActions/requests.test.js | 48 +++++++- src/editors/data/redux/thunkActions/video.js | 40 +++++++ .../data/redux/thunkActions/video.test.js | 43 +++++++- src/editors/data/redux/video/reducer.js | 1 + src/editors/data/redux/video/selectors.js | 1 + src/editors/data/services/cms/api.js | 27 +++++ src/editors/data/services/cms/api.test.js | 28 +++++ src/editors/data/services/cms/mockApi.js | 12 ++ .../data/services/cms/mockVideoData.js | 2 - src/editors/data/services/cms/urls.js | 8 ++ src/editors/data/services/cms/urls.test.js | 16 +++ 22 files changed, 531 insertions(+), 37 deletions(-) create mode 100644 src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.jsx create mode 100644 src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.test.jsx create mode 100644 src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/ImportTranscriptCard.test.jsx.snap diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx index 2cf92a661..735f381b0 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDisplay.jsx @@ -50,7 +50,7 @@ LicenseDisplay.propTypes = { shareAlike: PropTypes.bool.isRequired, }).isRequired, level: PropTypes.string.isRequired, - licenseDescription: PropTypes.func.isRequired, + licenseDescription: PropTypes.string.isRequired, }; export default injectIntl(LicenseDisplay); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.jsx new file mode 100644 index 000000000..7ad8dcffa --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; +import { + ActionRow, + Button, + Icon, + IconButton, + Stack, +} from '@edx/paragon'; +import { Close } from '@edx/paragon/icons'; + +import messages from './messages'; +import { thunkActions } from '../../../../../../data/redux'; + +export const ImportTranscriptCard = ({ + setOpen, + // redux + importTranscript, +}) => ( + + + + + setOpen(false)} + /> + + + + + + +); + +ImportTranscriptCard.defaultProps = { + setOpen: true, +}; + +ImportTranscriptCard.propTypes = { + setOpen: PropTypes.func, + // redux + importTranscript: PropTypes.func.isRequired, +}; + +export const mapStateToProps = () => ({}); + +export const mapDispatchToProps = { + importTranscript: thunkActions.video.importTranscript, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportTranscriptCard)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.test.jsx new file mode 100644 index 000000000..abc7165ef --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Button, IconButton } from '@edx/paragon'; + +import { thunkActions } from '../../../../../../data/redux'; +import * as module from './ImportTranscriptCard'; + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useContext: jest.fn(() => ({ transcripts: ['error.transcripts', jest.fn().mockName('error.setTranscripts')] })), +})); + +jest.mock('../../../../../../data/redux', () => ({ + thunkActions: { + video: { + importTranscript: jest.fn().mockName('thunkActions.video.importTranscript'), + }, + }, +})); + +describe('ImportTranscriptCard', () => { + const props = { + setOpen: jest.fn().mockName('setOpen'), + importTranscript: jest.fn().mockName('args.importTranscript'), + }; + let el; + describe('snapshots', () => { + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); + describe('behavior inspection', () => { + beforeEach(() => { + el = shallow(); + }); + test('close behavior is linked to IconButton', () => { + expect(el.find(IconButton) + .props().onClick).toBeDefined(); + }); + test('import behavior is linked to Button onClick', () => { + expect(el.find(Button) + .props().onClick).toEqual(props.importTranscript); + }); + }); + describe('mapStateToProps', () => { + it('returns an empty object', () => { + expect(module.mapStateToProps()).toEqual({}); + }); + }); + describe('mapDispatchToProps', () => { + test('updateField from thunkActions.video.importTranscript', () => { + expect(module.mapDispatchToProps.importTranscript).toEqual(thunkActions.video.importTranscript); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/ImportTranscriptCard.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/ImportTranscriptCard.test.jsx.snap new file mode 100644 index 000000000..e71ea14cb --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/__snapshots__/ImportTranscriptCard.test.jsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImportTranscriptCard snapshots snapshots: renders as expected with default props 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 a03d9e42e..2c18443df 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 @@ -30,9 +30,11 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with /> - + - + - + + + + + + + +`; + +exports[`TranscriptWidget component snapshots snapshots: renders as expected with allowTranscriptImport true 1`] = ` + + + + + + + + + + + - + - + { const [error] = React.useContext(ErrorContext).transcripts; + const [showImportCard, setShowImportCard] = React.useState(true); const fullTextLanguages = module.hooks.transcriptLanguages(transcripts, intl); const hasTranscripts = module.hooks.hasTranscripts(transcripts); + return ( - + {hasTranscripts ? ( - - { transcripts.map((language, index) => ( + + {transcripts.map((language, index) => ( + {showImportCard && allowTranscriptImport + ? + : null} > )} - + ({ transcripts: selectors.video.transcripts(state), allowTranscriptDownloads: selectors.video.allowTranscriptDownloads(state), showTranscriptByDefault: selectors.video.showTranscriptByDefault(state), + allowTranscriptImport: selectors.video.allowTranscriptImport(state), isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadTranscript }), isDeleteError: selectors.requests.isFailed(state, { requestKey: RequestKeys.deleteTranscript }), }); 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 018e396b9..2ff51afe8 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 @@ -29,7 +29,7 @@ jest.mock('../../../../../../data/redux', () => ({ transcripts: jest.fn(state => ({ transcripts: state })), allowTranscriptDownloads: jest.fn(state => ({ allowTranscriptDownloads: state })), showTranscriptByDefault: jest.fn(state => ({ showTranscriptByDefault: state })), - + allowTranscriptImport: jest.fn(state => ({ allowTranscriptImport: state })), }, requests: { isFailed: jest.fn(state => ({ isFailed: state })), @@ -90,6 +90,7 @@ describe('TranscriptWidget', () => { transcripts: [], allowTranscriptDownloads: false, showTranscriptByDefault: false, + allowTranscriptImport: false, updateField: jest.fn().mockName('args.updateField'), isUploadError: false, isDeleteError: false, @@ -101,6 +102,11 @@ describe('TranscriptWidget', () => { shallow(), ).toMatchSnapshot(); }); + test('snapshots: renders as expected with allowTranscriptImport true', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); test('snapshots: renders as expected with transcripts', () => { expect( shallow(), @@ -144,6 +150,11 @@ describe('TranscriptWidget', () => { module.mapStateToProps(testState).showTranscriptByDefault, ).toEqual(selectors.video.showTranscriptByDefault(testState)); }); + test('allowTranscriptImport from video.allowTranscriptImport', () => { + expect( + module.mapStateToProps(testState).allowTranscriptImport, + ).toEqual(selectors.video.allowTranscriptImport(testState)); + }); test('isUploadError from requests.isFinished', () => { expect( module.mapStateToProps(testState).isUploadError, diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/messages.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/messages.js index dc8b1520a..5fa311d0a 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/messages.js +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/messages.js @@ -99,6 +99,21 @@ export const messages = { defaultMessage: 'Only SRT files can be uploaded. Please select a file ending in .srt to upload.', description: 'Message warning users to only upload .srt files', }, + importButtonLabel: { + id: 'authoring.videoEditor.transcripts.importButton.label', + defaultMessage: 'Import Transcript', + description: 'Label for youTube import transcript button', + }, + importHeader: { + id: 'authoring.videoEditor.transcripts.importCard.header', + defaultMessage: 'Import transcript from YouTube?', + description: 'Header for import transcript card', + }, + importMessage: { + id: 'authoring.videoEditor.transcrtipts.importCard.message', + defaultMessage: 'We found transcript for this video on YouTube. Would you like to import it now?', + description: 'Message for import transcript card asking user if they want to import transcript', + }, }; export default messages; diff --git a/src/editors/data/constants/requests.js b/src/editors/data/constants/requests.js index 273ec758f..d82fca90c 100644 --- a/src/editors/data/constants/requests.js +++ b/src/editors/data/constants/requests.js @@ -22,6 +22,8 @@ export const RequestKeys = StrictDict({ fetchCourseDetails: 'fetchCourseDetails', updateTranscriptLanguage: 'updateTranscriptLanguage', getTranscriptFile: 'getTranscriptFile', + checkTranscriptsForImport: 'checkTranscriptsForImport', + importTranscript: 'importTranscript', uploadImage: 'uploadImage', fetchAdvanceSettings: 'fetchAdvanceSettings', }); diff --git a/src/editors/data/redux/requests/reducer.js b/src/editors/data/redux/requests/reducer.js index 6c1b80819..871139d13 100644 --- a/src/editors/data/redux/requests/reducer.js +++ b/src/editors/data/redux/requests/reducer.js @@ -16,7 +16,8 @@ const initialState = { [RequestKeys.deleteTranscript]: { status: RequestStates.inactive }, [RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive }, [RequestKeys.fetchAssets]: { status: RequestStates.inactive }, - + [RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive }, + [RequestKeys.importTranscript]: { status: RequestStates.inactive }, }; // eslint-disable-next-line no-unused-vars diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index c21b06798..52572261f 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -159,6 +159,31 @@ export const uploadThumbnail = ({ thumbnail, videoId, ...rest }) => (dispatch, g })); }; +export const checkTranscriptsForImport = ({ videoId, youTubeId, ...rest }) => (dispatch, getState) => { + dispatch(module.networkRequest({ + requestKey: RequestKeys.checkTranscriptsForImport, + promise: api.checkTranscriptsForImport({ + blockId: selectors.app.blockId(getState()), + videoId, + youTubeId, + studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), + }), + ...rest, + })); +}; + +export const importTranscript = ({ youTubeId, ...rest }) => (dispatch, getState) => { + dispatch(module.networkRequest({ + requestKey: RequestKeys.importTranscript, + promise: api.importTranscript({ + blockId: selectors.app.blockId(getState()), + studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), + youTubeId, + }), + ...rest, + })); +}; + export const deleteTranscript = ({ language, videoId, ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ requestKey: RequestKeys.deleteTranscript, @@ -261,5 +286,7 @@ export default StrictDict({ updateTranscriptLanguage, fetchCourseDetails, getTranscriptFile, + checkTranscriptsForImport, + importTranscript, fetchAdvanceSettings, }); diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index 7f5762f36..23127563f 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -29,11 +29,13 @@ jest.mock('../../services/cms/api', () => ({ fetchAssets: ({ id, url }) => ({ id, url }), uploadAsset: (args) => args, loadImages: jest.fn(), - allowThumbnailUpload: jest.fn(), - uploadThumbnail: jest.fn(), - uploadTranscript: jest.fn(), - deleteTranscript: jest.fn(), - getTranscript: jest.fn(), + allowThumbnailUpload: (args) => args, + uploadThumbnail: (args) => args, + uploadTranscript: (args) => args, + deleteTranscript: (args) => args, + getTranscript: (args) => args, + checkTranscriptsForImport: (args) => args, + importTranscript: (args) => args, })); const apiKeys = keyStore(api); @@ -352,6 +354,42 @@ describe('requests thunkActions module', () => { }, }); }); + describe('checkTranscriptsForImport', () => { + const youTubeId = 'SoME yOUtUbEiD As String'; + const videoId = 'SoME VidEOid As String'; + testNetworkRequestAction({ + action: requests.checkTranscriptsForImport, + args: { youTubeId, videoId, ...fetchParams }, + expectedString: 'with checkTranscriptsForImport promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.checkTranscriptsForImport, + promise: api.checkTranscriptsForImport({ + blockId: selectors.app.blockId(testState), + youTubeId, + videoId, + studioEndpointUrl: selectors.app.studioEndpointUrl(testState), + }), + }, + }); + }); + describe('importTranscript', () => { + const youTubeId = 'SoME yOUtUbEiD As String'; + testNetworkRequestAction({ + action: requests.importTranscript, + args: { youTubeId, ...fetchParams }, + expectedString: 'with importTranscript promise', + expectedData: { + ...fetchParams, + requestKey: RequestKeys.importTranscript, + promise: api.importTranscript({ + blockId: selectors.app.blockId(testState), + youTubeId, + studioEndpointUrl: selectors.app.studioEndpointUrl(testState), + }), + }, + }); + }); describe('getTranscriptFile', () => { const language = 'SoME laNGUage CoNtent As String'; const videoId = 'SoME VidEOid CoNtent As String'; diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 90a444fc3..f36ecd86d 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -3,6 +3,7 @@ import { removeItemOnce } from '../../../utils'; import * as requests from './requests'; import * as module from './video'; import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration'; +import { parseYoutubeId } from '../../services/cms/api'; export const loadVideoData = () => (dispatch, getState) => { const state = getState(); @@ -60,6 +61,20 @@ export const loadVideoData = () => (dispatch, getState) => { allowThumbnailUpload: response.data.allowThumbnailUpload, })), })); + const youTubeId = parseYoutubeId(videoSource); + if (youTubeId) { + dispatch(requests.checkTranscriptsForImport({ + videoId, + youTubeId, + onSuccess: (response) => { + if (response.data.command === 'import') { + dispatch(actions.video.updateField({ + allowTranscriptImport: true, + })); + } + }, + })); + } }; export const determineVideoSource = ({ @@ -224,6 +239,30 @@ export const uploadHandout = ({ file }) => (dispatch) => { // Transcript Thunks: +export const importTranscript = () => (dispatch, getState) => { + const state = getState(); + const { transcripts, videoSource } = state.video; + // Remove the placeholder '' from the unset language from the list of transcripts. + const transcriptsPlaceholderRemoved = (transcripts === []) ? transcripts : removeItemOnce(transcripts, ''); + + dispatch(requests.importTranscript({ + youTubeId: parseYoutubeId(videoSource), + onSuccess: (response) => { + dispatch(actions.video.updateField({ + transcripts: [ + ...transcriptsPlaceholderRemoved, + 'en'], + })); + + if (selectors.video.videoId(state) === '') { + dispatch(actions.video.updateField({ + videoId: response.data.edx_video_id, + })); + } + }, + })); +}; + export const uploadTranscript = ({ language, file }) => (dispatch, getState) => { const state = getState(); const { transcripts, videoId } = state.video; @@ -308,6 +347,7 @@ export default { parseLicense, saveVideoData, uploadThumbnail, + importTranscript, uploadTranscript, deleteTranscript, updateTranscriptLanguage, diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js index 90897df5c..895d4f425 100644 --- a/src/editors/data/redux/thunkActions/video.test.js +++ b/src/editors/data/redux/thunkActions/video.test.js @@ -28,21 +28,28 @@ jest.mock('./requests', () => ({ uploadTranscript: (args) => ({ uploadTranscript: args }), getTranscriptFile: (args) => ({ getTranscriptFile: args }), updateTranscriptLanguage: (args) => ({ updateTranscriptLanguage: args }), + checkTranscriptsForImport: (args) => ({ checkTranscriptsForImport: args }), + importTranscript: (args) => ({ importTranscript: args }), })); jest.mock('../../../utils', () => ({ removeItemOnce: (args) => (args), })); +jest.mock('../../services/cms/api', () => ({ + parseYoutubeId: (args) => (args), +})); + const thunkActionsKeys = keyStore(thunkActions); -const mockLanguage = 'na'; +const mockLanguage = 'en'; const mockFile = 'soMEtRANscRipT'; const mockFilename = 'soMEtRANscRipT.srt'; const mockThumbnail = 'sOMefILE'; const mockThumbnailResponse = { data: { image_url: 'soMEimAGEUrL' } }; const thumbnailUrl = 'soMEimAGEUrL'; const mockAllowThumbnailUpload = { data: { allowThumbnailUpload: 'soMEbOolEAn' } }; +const mockAllowTranscriptImport = { data: { command: 'import' } }; const testMetadata = { download_track: 'dOWNlOAdTraCK', @@ -63,7 +70,7 @@ const testState = { originalThumbnail: null, videoId: 'soMEvIDEo', }; -const testUpload = { transcripts: ['la', 'na'] }; +const testUpload = { transcripts: ['la', 'en'] }; const testReplaceUpload = { file: mockFile, language: mockLanguage, @@ -89,6 +96,8 @@ describe('video thunkActions', () => { }); describe('loadVideoData', () => { let dispatchedLoad; + let dispatchedAction1; + let dispatchedAction2; beforeEach(() => { jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSource).mockReturnValue({ videoSource: 'videOsOurce', @@ -108,14 +117,18 @@ describe('video thunkActions', () => { testMetadata.transcripts, ); thunkActions.loadVideoData()(dispatch, getState); - [[dispatchedLoad], [dispatchedAction]] = dispatch.mock.calls; + [[dispatchedLoad], [dispatchedAction1], [dispatchedAction2]] = dispatch.mock.calls; }); afterEach(() => { jest.restoreAllMocks(); }); it('dispatches allowThumbnailUpload action', () => { expect(dispatchedLoad).not.toEqual(undefined); - expect(dispatchedAction.allowThumbnailUpload).not.toEqual(undefined); + expect(dispatchedAction1.allowThumbnailUpload).not.toEqual(undefined); + }); + it('dispatches checkTranscriptsForImport action', () => { + expect(dispatchedLoad).not.toEqual(undefined); + expect(dispatchedAction2.checkTranscriptsForImport).not.toEqual(undefined); }); it('dispatches actions.video.load', () => { expect(dispatchedLoad.load).toEqual({ @@ -151,10 +164,16 @@ describe('video thunkActions', () => { }); it('dispatches actions.video.updateField on success', () => { dispatch.mockClear(); - dispatchedAction.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload); + dispatchedAction1.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload); expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ allowThumbnailUpload: mockAllowThumbnailUpload.data.allowThumbnailUpload, })); + dispatch.mockClear(); + + dispatchedAction2.checkTranscriptsForImport.onSuccess(mockAllowTranscriptImport); + expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ + allowTranscriptImport: true, + })); }); }); describe('determineVideoSource', () => { @@ -350,6 +369,20 @@ describe('video thunkActions', () => { expect(dispatchedAction.uploadThumbnail).not.toEqual(undefined); }); }); + describe('importTranscript', () => { + beforeEach(() => { + thunkActions.importTranscript()(dispatch, getState); + [[dispatchedAction]] = dispatch.mock.calls; + }); + it('dispatches uploadTranscript action', () => { + expect(dispatchedAction.importTranscript).not.toEqual(undefined); + }); + it('dispatches actions.video.updateField on success', () => { + dispatch.mockClear(); + dispatchedAction.importTranscript.onSuccess(); + expect(dispatch).toHaveBeenCalledWith(actions.video.updateField(testUpload)); + }); + }); describe('deleteTranscript', () => { beforeEach(() => { thunkActions.deleteTranscript({ language: 'la' })(dispatch, getState); diff --git a/src/editors/data/redux/video/reducer.js b/src/editors/data/redux/video/reducer.js index dfddf13c7..a3cdc27b5 100644 --- a/src/editors/data/redux/video/reducer.js +++ b/src/editors/data/redux/video/reducer.js @@ -35,6 +35,7 @@ const initialState = { shareAlike: false, }, allowThumbnailUpload: null, + allowTranscriptImport: false, }; // eslint-disable-next-line no-unused-vars diff --git a/src/editors/data/redux/video/selectors.js b/src/editors/data/redux/video/selectors.js index 55d1d50fd..d16cd5a19 100644 --- a/src/editors/data/redux/video/selectors.js +++ b/src/editors/data/redux/video/selectors.js @@ -28,6 +28,7 @@ export const simpleSelectors = [ stateKeys.courseLicenseType, stateKeys.courseLicenseDetails, stateKeys.allowThumbnailUpload, + stateKeys.allowTranscriptImport, ].reduce((obj, key) => ({ ...obj, [key]: state => state.video[key] }), {}); export const openLanguages = createSelector( diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index 19d8afe53..434df60f5 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -54,6 +54,33 @@ export const apiMethods = { data, ); }, + checkTranscriptsForImport: ({ + studioEndpointUrl, + blockId, + youTubeId, + videoId, + }) => { + const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"},{"mode":"edx_video_id","type":"edx_video_id","video":"${videoId}"}]}`; + return get( + urls.checkTranscriptsForImport({ + studioEndpointUrl, + parameters: encodeURIComponent(getJSON), + }), + ); + }, + importTranscript: ({ + studioEndpointUrl, + blockId, + youTubeId, + }) => { + const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"}]}`; + return get( + urls.replaceTranscript({ + studioEndpointUrl, + parameters: encodeURIComponent(getJSON), + }), + ); + }, getTranscript: ({ studioEndpointUrl, language, diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js index 6264e22a3..6426f93d5 100644 --- a/src/editors/data/services/cms/api.test.js +++ b/src/editors/data/services/cms/api.test.js @@ -20,6 +20,8 @@ jest.mock('./urls', () => ({ videoTranscripts: jest.fn().mockName('urls.videoTranscripts'), allowThumbnailUpload: jest.fn().mockName('urls.allowThumbnailUpload'), thumbnailUpload: jest.fn().mockName('urls.thumbnailUpload'), + checkTranscriptsForImport: jest.fn().mockName('urls.checkTranscriptsForImport'), + replaceTranscript: jest.fn().mockName('urls.replaceTranscript'), })); jest.mock('./utils', () => ({ @@ -251,6 +253,32 @@ describe('cms api', () => { describe('videoTranscripts', () => { const language = 'la'; const videoId = 'sOmeVIDeoiD'; + const youTubeId = 'SOMeyoutUBeid'; + describe('checkTranscriptsForImport', () => { + const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"},{"mode":"edx_video_id","type":"edx_video_id","video":"${videoId}"}]}`; + it('should call get with url.checkTranscriptsForImport', () => { + apiMethods.checkTranscriptsForImport({ + studioEndpointUrl, + blockId, + videoId, + youTubeId, + }); + expect(get).toHaveBeenCalledWith(urls.checkTranscriptsForImport({ + studioEndpointUrl, + parameters: encodeURIComponent(getJSON), + })); + }); + }); + describe('importTranscript', () => { + const getJSON = `{"locator":"${blockId}","videos":[{"mode":"youtube","video":"${youTubeId}","type":"youtube"}]}`; + it('should call get with url.replaceTranscript', () => { + apiMethods.importTranscript({ studioEndpointUrl, blockId, youTubeId }); + expect(get).toHaveBeenCalledWith(urls.replaceTranscript({ + studioEndpointUrl, + parameters: encodeURIComponent(getJSON), + })); + }); + }); describe('uploadTranscript', () => { const transcript = { transcript: 'dAta' }; it('should call post with urls.videoTranscripts and transcript data', () => { diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js index 9a1805baf..94be91054 100644 --- a/src/editors/data/services/cms/mockApi.js +++ b/src/editors/data/services/cms/mockApi.js @@ -126,6 +126,18 @@ export const allowThumbnailUpload = ({ studioEndpointUrl }) => mockPromise({ data: true, }); // eslint-disable-next-line +export const checkTranscripts = ({youTubeId, studioEndpointUrl, blockId, videoId}) => mockPromise({ + data: { + command: 'import', + }, +}); +// eslint-disable-next-line +export const importTranscript = ({youTubeId, studioEndpointUrl, blockId}) => mockPromise({ + data: { + edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7', + }, +}); +// eslint-disable-next-line export const fetchAdvanceSettings = ({ studioEndpointUrl, learningContextId }) => mockPromise({ data: { allow_unsupported_xblocks: { value: true } }, }); diff --git a/src/editors/data/services/cms/mockVideoData.js b/src/editors/data/services/cms/mockVideoData.js index 0bc13fd80..c26fbe9e5 100644 --- a/src/editors/data/services/cms/mockVideoData.js +++ b/src/editors/data/services/cms/mockVideoData.js @@ -23,7 +23,6 @@ export const videoDataProps = { noDerivatives: PropTypes.bool, shareAlike: PropTypes.bool, }), - originalThumbnail: PropTypes.string, }; export const singleVideoData = { @@ -53,5 +52,4 @@ export const singleVideoData = { noDerivatives: false, shareAlike: false, }, - originalThumbnail: 'someString', }; diff --git a/src/editors/data/services/cms/urls.js b/src/editors/data/services/cms/urls.js index b6260fcc2..24bf94769 100644 --- a/src/editors/data/services/cms/urls.js +++ b/src/editors/data/services/cms/urls.js @@ -55,6 +55,14 @@ export const courseDetailsUrl = ({ studioEndpointUrl, learningContextId }) => ( `${studioEndpointUrl}/settings/details/${learningContextId}` ); +export const checkTranscriptsForImport = ({ studioEndpointUrl, parameters }) => ( + `${studioEndpointUrl}/transcripts/check?data=${parameters}` +); + +export const replaceTranscript = ({ studioEndpointUrl, parameters }) => ( + `${studioEndpointUrl}/transcripts/replace?data=${parameters}` +); + export const courseAdvanceSettings = ({ studioEndpointUrl, learningContextId }) => ( `${studioEndpointUrl}/api/contentstore/v0/advanced_settings/${learningContextId}` ); diff --git a/src/editors/data/services/cms/urls.test.js b/src/editors/data/services/cms/urls.test.js index 35ad230a2..53ab06bb9 100644 --- a/src/editors/data/services/cms/urls.test.js +++ b/src/editors/data/services/cms/urls.test.js @@ -12,6 +12,8 @@ import { videoTranscripts, downloadVideoHandoutUrl, courseDetailsUrl, + checkTranscriptsForImport, + replaceTranscript, } from './urls'; describe('cms url methods', () => { @@ -23,6 +25,8 @@ describe('cms url methods', () => { const language = 'la'; const handout = '/aSSet@hANdoUt'; const videoId = '123-SOmeVidEOid-213'; + const parameters = 'SomEParAMEterS'; + describe('return to learning context urls', () => { const unitUrl = { data: { @@ -115,4 +119,16 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}/settings/details/${learningContextId}`); }); }); + describe('checkTranscriptsForImport', () => { + it('returns url with studioEndpointUrl and parameters', () => { + expect(checkTranscriptsForImport({ studioEndpointUrl, parameters })) + .toEqual(`${studioEndpointUrl}/transcripts/check?data=${parameters}`); + }); + }); + describe('replaceTranscript', () => { + it('returns url with studioEndpointUrl and parameters', () => { + expect(replaceTranscript({ studioEndpointUrl, parameters })) + .toEqual(`${studioEndpointUrl}/transcripts/replace?data=${parameters}`); + }); + }); });