diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx index 90d3ec235b..e418e14fa2 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx @@ -54,6 +54,7 @@ const LanguageSelector = ({ language, // Redux openLanguages, // Only allow those languages not already associated with a transcript to be selected + transcriptHandlerUrl, // intl intl, @@ -122,6 +123,7 @@ LanguageSelector.defaultProps = { LanguageSelector.propTypes = { openLanguages: PropTypes.arrayOf(PropTypes.string), + transcriptHandlerUrl: PropTypes.string.isRequired, index: PropTypes.number.isRequired, language: PropTypes.string.isRequired, intl: intlShape.isRequired, @@ -129,6 +131,7 @@ LanguageSelector.propTypes = { export const mapStateToProps = (state) => ({ openLanguages: selectors.video.openLanguages(state), + transcriptHandlerUrl: selectors.video.transcriptHandlerUrl(state), }); export const mapDispatchToProps = {}; 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 b734b31ff4..18be27f6a3 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { connect, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { FormattedMessage, @@ -17,7 +17,7 @@ import { } from '@openedx/paragon'; import { Add, InfoOutline } from '@openedx/paragon/icons'; -import { actions, selectors } from '../../../../../../data/redux'; +import { thunkActions, actions, selectors } from '../../../../../../data/redux'; import messages from './messages'; import { RequestKeys } from '../../../../../../data/constants/requests'; @@ -90,6 +90,7 @@ const TranscriptWidget = ({ updateField, isUploadError, isDeleteError, + isLibrary, // injected intl, }) => { @@ -97,6 +98,10 @@ const TranscriptWidget = ({ const [showImportCard, setShowImportCard] = React.useState(true); const fullTextLanguages = module.hooks.transcriptLanguages(transcripts, intl); const hasTranscripts = module.hooks.hasTranscripts(transcripts); + const dispatch = useDispatch(); + if (isLibrary) { + dispatch(thunkActions.video.updateTranscriptHandlerUrl()); + } return ( ({ @@ -207,6 +213,7 @@ export const mapStateToProps = (state) => ({ allowTranscriptImport: selectors.video.allowTranscriptImport(state), isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadTranscript }), isDeleteError: selectors.requests.isFailed(state, { requestKey: RequestKeys.deleteTranscript }), + isLibrary: selectors.app.isLibrary(state), }); export const mapDispatchToProps = (dispatch) => ({ diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx index 3377e31f45..912b541dc1 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/VideoPreviewWidget/index.jsx @@ -17,7 +17,6 @@ export const VideoPreviewWidget = ({ videoSource, transcripts, blockTitle, - isLibrary, intl, }) => { const imgRef = React.useRef(); @@ -47,10 +46,7 @@ export const VideoPreviewWidget = ({ />

{blockTitle}

- {!isLibrary && ( - // Since content libraries v2 don't support static assets yet, we can't include transcripts. - - )} + {videoType && ( ({ @@ -82,7 +77,6 @@ export const mapStateToProps = (state) => ({ videoSource: selectors.video.videoSource(state), thumbnail: selectors.video.thumbnail(state), blockTitle: selectors.app.blockTitle(state), - isLibrary: selectors.app.isLibrary(state), }); export default injectIntl(connect(mapStateToProps)(VideoPreviewWidget)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx index 4761e39320..7edbcb8be2 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/index.tsx @@ -49,9 +49,7 @@ const VideoSettingsModal: React.FC = ({ )} - {!isLibrary && ( // Since content libraries v2 don't support static assets yet, we can't include transcripts. - - )} + diff --git a/src/editors/data/constants/requests.ts b/src/editors/data/constants/requests.ts index e12905d588..561965af86 100644 --- a/src/editors/data/constants/requests.ts +++ b/src/editors/data/constants/requests.ts @@ -27,4 +27,5 @@ export const RequestKeys = StrictDict({ uploadAsset: 'uploadAsset', fetchAdvancedSettings: 'fetchAdvancedSettings', fetchVideoFeatures: 'fetchVideoFeatures', + getHandlerUrl: 'getHandlerUrl', } as const); diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index edff3bf875..05cbad8396 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -4,6 +4,7 @@ import { RequestKeys } from '../../constants/requests'; import api, { loadImages } from '../../services/cms/api'; import { actions as requestsActions } from '../requests'; import { selectors as appSelectors } from '../app'; +import { selectors as videoSelectors } from '../video'; // This 'module' self-import hack enables mocking during tests. // See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested @@ -15,7 +16,7 @@ import { acceptedImgKeys } from '../../../sharedComponents/ImageUploadModal/Sele // Similar to `import { actions, selectors } from '..';` but avoid circular imports: const actions = { requests: requestsActions }; -const selectors = { app: appSelectors }; +const selectors = { app: appSelectors, video: videoSelectors }; /** * Wrapper around a network request promise, that sends actions to the redux store to @@ -257,17 +258,31 @@ export const uploadTranscript = ({ language, ...rest }) => (dispatch, getState) => { - dispatch(module.networkRequest({ - requestKey: RequestKeys.uploadTranscript, - promise: api.uploadTranscript({ - blockId: selectors.app.blockId(getState()), - transcript, - videoId, - language, - studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), - }), - ...rest, - })); + const isLibrary = selectors.app.isLibrary(getState()); + if (isLibrary) { + dispatch(module.networkRequest({ + requestKey: RequestKeys.uploadTranscript, + promise: api.uploadTranscriptV2({ + handlerUrl: selectors.video.transcriptHandlerUrl(getState()), + transcript, + videoId, + language, + }), + ...rest, + })); + } else { + dispatch(module.networkRequest({ + requestKey: RequestKeys.uploadTranscript, + promise: api.uploadTranscript({ + blockId: selectors.app.blockId(getState()), + transcript, + videoId, + language, + studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), + }), + ...rest, + })); + } }; export const updateTranscriptLanguage = ({ @@ -304,6 +319,18 @@ export const getTranscriptFile = ({ language, videoId, ...rest }) => (dispatch, })); }; +export const getHandlerlUrl = ({ handlerName, ...rest }) => (dispatch, getState) => { + dispatch(module.networkRequest({ + requestKey: RequestKeys.getHandlerlUrl, + promise: api.getHandlerUrl({ + studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), + blockId: selectors.app.blockId(getState()), + handlerName, + }), + ...rest, + })); +}; + export const fetchCourseDetails = ({ ...rest }) => (dispatch, getState) => { dispatch(module.networkRequest({ requestKey: RequestKeys.fetchCourseDetails, @@ -368,4 +395,5 @@ export default StrictDict({ fetchAdvancedSettings, fetchVideoFeatures, uploadVideo, + getHandlerlUrl, }); diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 352d989737..04c8b79b8b 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -383,6 +383,16 @@ export const updateTranscriptLanguage = ({ newLanguageCode, languageBeforeChange })); }; +export const updateTranscriptHandlerUrl = () => (dispatch, getState) => { + dispatch(requests.getHandlerlUrl({ + handlerName: 'studio_transcript', + onSuccess: (response) => { + const transcriptHandlerUrl = response.data.handler_url; + dispatch(actions.video.updateField({ transcriptHandlerUrl })); + }, + })); +}; + export const replaceTranscript = ({ newFile, newFilename, language }) => (dispatch, getState) => { const state = getState(); const { videoId } = state.video; @@ -456,4 +466,5 @@ export default { replaceTranscript, uploadHandout, uploadVideo, + updateTranscriptHandlerUrl, }; diff --git a/src/editors/data/redux/video/reducer.js b/src/editors/data/redux/video/reducer.js index 04bfbcd99c..8770c306fe 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: [], + transcriptHandlerUrl: '', selectedVideoTranscriptUrls: {}, allowTranscriptDownloads: false, duration: { diff --git a/src/editors/data/redux/video/selectors.js b/src/editors/data/redux/video/selectors.js index f71c014cf7..f8c21f0a39 100644 --- a/src/editors/data/redux/video/selectors.js +++ b/src/editors/data/redux/video/selectors.js @@ -27,6 +27,7 @@ export const simpleSelectors = [ stateKeys.allowVideoSharing, stateKeys.thumbnail, stateKeys.transcripts, + stateKeys.transcriptHandlerUrl, stateKeys.selectedVideoTranscriptUrls, stateKeys.allowTranscriptDownloads, stateKeys.duration, diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index e979eb4455..926b541fbb 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -246,6 +246,23 @@ export const apiMethods = { data, ); }, + uploadTranscriptV2: ({ + handlerUrl, + transcript, + videoId, + language, + newLanguage = null, + }) => { + const data = new FormData(); + data.append('file', transcript); + data.append('edx_video_id', videoId); + data.append('language_code', language); + data.append('new_language_code', newLanguage || language); + return post( + urls.uploadTrascriptXblockV2({ handlerUrl }), + data, + ); + }, normalizeContent: ({ blockId, blockType, @@ -345,6 +362,13 @@ export const apiMethods = { urls.courseVideos({ studioEndpointUrl, learningContextId }), data, ), + getHandlerUrl: ({ + studioEndpointUrl, + blockId, + handlerName, + }) => get( + urls.handlerUrl({ studioEndpointUrl, blockId, handlerName }), + ), }; export default apiMethods; diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index a137ffb9f8..fdd35e31da 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -111,3 +111,11 @@ export const videoFeatures = (({ studioEndpointUrl }) => ( export const courseVideos = (({ studioEndpointUrl, learningContextId }) => ( `${studioEndpointUrl}/videos/${learningContextId}` )) satisfies UrlFunction; + +export const handlerUrl = (({studioEndpointUrl, blockId, handlerName}) => ( + `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/handler_url/${handlerName}/` +)) satisfies UrlFunction; + +export const uploadTrascriptXblockV2 = (({ handlerUrl }) => ( + `${handlerUrl}translation` +)) satisfies UrlFunction;