Skip to content

Commit

Permalink
feat: Enable transcripts for video library
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisChV committed Jan 17, 2025
1 parent fd6a6dd commit b7fbac9
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const LanguageSelector = ({
language,
// Redux
openLanguages, // Only allow those languages not already associated with a transcript to be selected
transcriptHandlerUrl,

Check failure on line 57 in src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx

View workflow job for this annotation

GitHub Actions / tests

'transcriptHandlerUrl' is defined but never used
// intl
intl,

Expand Down Expand Up @@ -122,13 +123,15 @@ LanguageSelector.defaultProps = {

LanguageSelector.propTypes = {
openLanguages: PropTypes.arrayOf(PropTypes.string),
transcriptHandlerUrl: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
language: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};

export const mapStateToProps = (state) => ({
openLanguages: selectors.video.openLanguages(state),
transcriptHandlerUrl: selectors.video.transcriptHandlerUrl(state),
});

export const mapDispatchToProps = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -90,13 +90,18 @@ const TranscriptWidget = ({
updateField,
isUploadError,
isDeleteError,
isLibrary,
// injected
intl,
}) => {
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);
const dispatch = useDispatch();
if (isLibrary) {
dispatch(thunkActions.video.updateTranscriptHandlerUrl());
}

return (
<CollapsibleFormWidget
Expand Down Expand Up @@ -197,6 +202,7 @@ TranscriptWidget.propTypes = {
updateField: PropTypes.func.isRequired,
isUploadError: PropTypes.bool.isRequired,
isDeleteError: PropTypes.bool.isRequired,
isLibrary: PropTypes.bool.isRequired,
intl: PropTypes.shape(intlShape).isRequired,
};
export const mapStateToProps = (state) => ({
Expand All @@ -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) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const VideoPreviewWidget = ({
videoSource,
transcripts,
blockTitle,
isLibrary,
intl,
}) => {
const imgRef = React.useRef();
Expand Down Expand Up @@ -47,10 +46,7 @@ export const VideoPreviewWidget = ({
/>
<Stack gap={1} className="justify-content-center">
<h4 className="text-primary mb-0">{blockTitle}</h4>
{!isLibrary && (
// Since content libraries v2 don't support static assets yet, we can't include transcripts.
<LanguageNamesWidget transcripts={transcripts} />
)}
<LanguageNamesWidget transcripts={transcripts} />
{videoType && (
<Hyperlink
className="text-primary x-small"
Expand All @@ -74,15 +70,13 @@ VideoPreviewWidget.propTypes = {
thumbnail: PropTypes.string.isRequired,
transcripts: PropTypes.arrayOf(PropTypes.string).isRequired,
blockTitle: PropTypes.string.isRequired,
isLibrary: PropTypes.bool.isRequired,
};

export const mapStateToProps = (state) => ({
transcripts: selectors.video.transcripts(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));
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ const VideoSettingsModal: React.FC<Props> = ({
<SocialShareWidget />
)}
<ThumbnailWidget />
{!isLibrary && ( // Since content libraries v2 don't support static assets yet, we can't include transcripts.
<TranscriptWidget />
)}
<TranscriptWidget />
<DurationWidget />
<HandoutWidget />
<LicenseWidget />
Expand Down
1 change: 1 addition & 0 deletions src/editors/data/constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export const RequestKeys = StrictDict({
uploadAsset: 'uploadAsset',
fetchAdvancedSettings: 'fetchAdvancedSettings',
fetchVideoFeatures: 'fetchVideoFeatures',
getHandlerUrl: 'getHandlerUrl',
} as const);
52 changes: 40 additions & 12 deletions src/editors/data/redux/thunkActions/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 = ({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -368,4 +395,5 @@ export default StrictDict({
fetchAdvancedSettings,
fetchVideoFeatures,
uploadVideo,
getHandlerlUrl,
});
11 changes: 11 additions & 0 deletions src/editors/data/redux/thunkActions/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,16 @@ export const updateTranscriptLanguage = ({ newLanguageCode, languageBeforeChange
}));
};

export const updateTranscriptHandlerUrl = () => (dispatch, getState) => {

Check failure on line 386 in src/editors/data/redux/thunkActions/video.js

View workflow job for this annotation

GitHub Actions / tests

'getState' is defined but never used
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;
Expand Down Expand Up @@ -456,4 +466,5 @@ export default {
replaceTranscript,
uploadHandout,
uploadVideo,
updateTranscriptHandlerUrl,
};
1 change: 1 addition & 0 deletions src/editors/data/redux/video/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const initialState = {
videoSharingLearnMoreLink: '',
thumbnail: null,
transcripts: [],
transcriptHandlerUrl: '',
selectedVideoTranscriptUrls: {},
allowTranscriptDownloads: false,
duration: {
Expand Down
1 change: 1 addition & 0 deletions src/editors/data/redux/video/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const simpleSelectors = [
stateKeys.allowVideoSharing,
stateKeys.thumbnail,
stateKeys.transcripts,
stateKeys.transcriptHandlerUrl,
stateKeys.selectedVideoTranscriptUrls,
stateKeys.allowTranscriptDownloads,
stateKeys.duration,
Expand Down
24 changes: 24 additions & 0 deletions src/editors/data/services/cms/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
8 changes: 8 additions & 0 deletions src/editors/data/services/cms/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ export const videoFeatures = (({ studioEndpointUrl }) => (
export const courseVideos = (({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/videos/${learningContextId}`
)) satisfies UrlFunction;

export const handlerUrl = (({studioEndpointUrl, blockId, handlerName}) => (

Check failure on line 115 in src/editors/data/services/cms/urls.ts

View workflow job for this annotation

GitHub Actions / tests

A space is required after '{'

Check failure on line 115 in src/editors/data/services/cms/urls.ts

View workflow job for this annotation

GitHub Actions / tests

A space is required before '}'
`${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/handler_url/${handlerName}/`
)) satisfies UrlFunction;

export const uploadTrascriptXblockV2 = (({ handlerUrl }) => (

Check failure on line 119 in src/editors/data/services/cms/urls.ts

View workflow job for this annotation

GitHub Actions / tests

'handlerUrl' is already declared in the upper scope on line 115 column 14
`${handlerUrl}translation`
)) satisfies UrlFunction;

0 comments on commit b7fbac9

Please sign in to comment.