feat: Allow to import transcripts from selected video

This commit is contained in:
XnpioChV
2023-04-20 14:23:12 -05:00
parent 359a5fd505
commit e20dedd0a5
17 changed files with 335 additions and 15 deletions

View File

@@ -40,6 +40,7 @@ export const hooks = {
export const Transcript = ({
index,
language,
transcriptUrl,
// redux
deleteTranscript,
}) => {
@@ -90,6 +91,7 @@ export const Transcript = ({
<TranscriptActionMenu
index={index}
language={language}
transcriptUrl={transcriptUrl}
launchDeleteConfirmation={launchDeleteConfirmation}
/>
)}
@@ -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,
};

View File

@@ -80,6 +80,16 @@ describe('Transcript Component', () => {
shallow(<module.Transcript {...props} />),
).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(<module.Transcript {...props} transcriptUrl="url" />),
).toMatchSnapshot();
});
});
});
});

View File

@@ -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 (
<Dropdown>
<Dropdown.Toggle
@@ -61,16 +62,23 @@ export const TranscriptActionMenu = ({
);
};
TranscriptActionMenu.defaultProps = {
transcriptUrl: undefined,
};
TranscriptActionMenu.propTypes = {
index: PropTypes.number.isRequired,
language: PropTypes.string.isRequired,
transcriptUrl: PropTypes.string,
launchDeleteConfirmation: PropTypes.func.isRequired,
// redux
getTranscriptDownloadUrl: PropTypes.func.isRequired,
buildTranscriptUrl: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
getTranscriptDownloadUrl: selectors.video.getTranscriptDownloadUrl(state),
buildTranscriptUrl: selectors.video.buildTranscriptUrl(state),
});
export const mapDispatchToProps = {

View File

@@ -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(<module.TranscriptActionMenu {...props} />),
).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(<module.TranscriptActionMenu {...props} transcriptUrl="url" />),
).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };

View File

@@ -83,3 +83,21 @@ exports[`Transcript Component component component snapshots: renders as expected
</Card>
</Fragment>
`;
exports[`Transcript Component component component snapshots: renders as expected with transcriptUrl 1`] = `
<Fragment>
<ActionRow>
<LanguageSelector
language="lAnG"
title="sOmenUmBer"
/>
<ActionRow.Spacer />
<TranscriptActionMenu
index="sOmenUmBer"
language="lAnG"
launchDeleteConfirmation={[MockFunction launchDeleteConfirmation]}
transcriptUrl="url"
/>
</ActionRow>
</Fragment>
`;

View File

@@ -53,3 +53,57 @@ exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with defa
/>
</Dropdown>
`;
exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with transcriptUrl props: dont show confirm delete 1`] = `
<Dropdown>
<Dropdown.Toggle
alt="Actions dropdown"
as="IconButton"
iconAs="Icon"
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
variant="primary"
/>
<Dropdown.Menu
className="video_transcript Action Menu"
>
<Dropdown.Item
key="transcript-actions-sOmenUmBer-replace"
onClick={[MockFunction click input]}
>
<FormattedMessage
defaultMessage="Replace"
description="Message Presented To user for action to replace transcript"
id="authoring.videoeditor.transcript.replaceTranscript"
/>
</Dropdown.Item>
<Dropdown.Item
key="transcript-actions-sOmenUmBer-download"
>
<FormattedMessage
defaultMessage="Download"
description="Message Presented To user for action to download transcript"
id="authoring.videoeditor.transcript.downloadTranscript"
/>
</Dropdown.Item>
<Dropdown.Item
key="transcript-actions-sOmenUmBer-delete"
onClick={[MockFunction launchDeleteConfirmation]}
>
<FormattedMessage
defaultMessage="Delete"
description="Message Presented To user for action to delete transcript"
id="authoring.videoeditor.transcript.deleteTranscript"
/>
</Dropdown.Item>
</Dropdown.Menu>
<FileInput
acceptedFiles=".srt"
fileInput={
Object {
"click": [MockFunction click input],
"onAddFile": [MockFunction module.hooks.replaceFileCallback],
}
}
/>
</Dropdown>
`;

View File

@@ -597,6 +597,63 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
</CollapsibleFormWidget>
`;
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcript urls 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="None"
title="Transcripts"
>
<ErrorAlert
dismissError={null}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="Failed to upload transcript. Please try again."
description="Message presented to user when transcript fails to upload"
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
/>
</ErrorAlert>
<ErrorAlert
dismissError={null}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="Failed to delete transcript. Please try again."
description="Message presented to user when transcript fails to delete"
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
/>
</ErrorAlert>
<Stack
gap={3}
>
<FormattedMessage
defaultMessage="Add video transcripts (.srt files only) for improved accessibility."
description="Message for adding first transcript"
id="authoring.videoeditor.transcripts.upload.firstTranscriptMessage"
/>
<div
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a transcript"
description="Label for upload button"
id="authoring.videoeditor.transcripts.upload.label"
/>
</Button>
</div>
</Stack>
</CollapsibleFormWidget>
`;
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
@@ -715,3 +772,122 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
</Stack>
</CollapsibleFormWidget>
`;
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts and urls 1`] = `
<CollapsibleFormWidget
fontSize="x-small"
isError={true}
subtitle="Spanish"
title="Transcripts"
>
<ErrorAlert
dismissError={null}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="Failed to upload transcript. Please try again."
description="Message presented to user when transcript fails to upload"
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
/>
</ErrorAlert>
<ErrorAlert
dismissError={null}
hideHeading={true}
isError={false}
>
<FormattedMessage
defaultMessage="Failed to delete transcript. Please try again."
description="Message presented to user when transcript fails to delete"
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
/>
</ErrorAlert>
<Stack
gap={3}
>
<Form.Group
className="border-primary-100 border-bottom"
>
<Transcript
index={0}
language="es"
/>
<ActionRow
className="mt-3.5"
>
<Form.Checkbox
checked={false}
className="decorative-control-label"
onChange={[Function]}
>
<div
className="small text-gray-700"
>
<FormattedMessage
defaultMessage="Allow transcript downloads"
description="Label for allow transcript downloads checkbox"
id="authoring.videoeditor.transcripts.allowDownloadCheckboxLabel"
/>
</div>
</Form.Checkbox>
<OverlayTrigger
key="top"
overlay={
<Tooltip
id="tooltip-top"
>
<FormattedMessage
defaultMessage="Learners will see a link to download the transcript below the video."
description="Message for show by default checkbox"
id="authoring.videoeditor.transcripts.upload.allowDownloadTooltipMessage"
/>
</Tooltip>
}
placement="top"
>
<Icon
style={
Object {
"height": "16px",
"width": "16px",
}
}
/>
</OverlayTrigger>
<ActionRow.Spacer />
</ActionRow>
<Form.Checkbox
checked={false}
className="mt-3 decorative-control-label"
onChange={[Function]}
>
<div
className="small text-gray-700"
>
<FormattedMessage
defaultMessage="Show transcript in the video player by default"
description="Label for show by default checkbox"
id="authoring.videoeditor.transcripts.upload.showByDefaultCheckboxLabel"
/>
</div>
</Form.Checkbox>
</Form.Group>
<div
className="mt-2"
>
<Button
className="text-primary-500 font-weight-bold justify-content-start pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a transcript"
description="Label for upload button"
id="authoring.videoeditor.transcripts.upload.label"
/>
</Button>
</div>
</Stack>
</CollapsibleFormWidget>
`;

View File

@@ -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) => (
<Transcript
language={language}
transcriptUrl={selectedVideoTranscriptUrls[language]}
index={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),

View File

@@ -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(<module.TranscriptWidget {...props} transcripts={['en']} />),
).toMatchSnapshot();
});
test('snapshots: renders as expected with transcript urls', () => {
expect(
shallow(<module.TranscriptWidget {...props} selectedVideoTranscriptUrls={{ en: 'url' }} />),
).toMatchSnapshot();
});
test('snapshots: renders as expected with transcripts and urls', () => {
expect(
shallow(<module.TranscriptWidget {...props} transcripts={['es']} selectedVideoTranscriptUrls={{ en: 'url' }} />),
).toMatchSnapshot();
});
test('snapshots: renders as expected with allowTranscriptDownloads true', () => {
expect(
shallow(<module.TranscriptWidget {...props} allowTranscriptDownloads transcripts={['en']} />),

View File

@@ -23,8 +23,8 @@ export const uploadVideo = async ({ dispatch, supportedFiles }) => {
const { files } = response.data;
await Promise.all(Object.values(files).map(async (fileObj) => {
const fileName = fileObj.file_name;
const uploadUrl = fileObj.upload_url;
const edxVideoId = fileObj.edx_video_id;
const uploadUrl = fileObj.upload_url;
const uploadFile = supportedFiles.find((file) => file.name === fileName);
// TODO I added this temporally to test the redirecton without

View File

@@ -40,10 +40,10 @@ describe('uploadVideo', () => {
{ file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' },
],
};
const spyConsoleLog = jest.spyOn(console, 'log');
const mockRequestResponse = { data: response };
requests.uploadVideo.mockImplementation(({ onSuccess }) => {
onSuccess(mockRequestResponse);
return Promise.resolve();
requests.uploadVideo.mockImplementation(async ({ onSuccess }) => {
await onSuccess(mockRequestResponse);
});
await hooks.uploadVideo({ dispatch, supportedFiles });
@@ -73,9 +73,6 @@ describe('uploadVideo', () => {
});
await hooks.uploadVideo({ dispatch, supportedFiles });
expect(spyConsoleError).toHaveBeenCalledTimes(4);
expect(spyConsoleError).toHaveBeenCalledWith('Error uploading file:', error);
});
it('should log an error if file object is not found in supportedFiles array', () => {

View File

@@ -14,11 +14,12 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => {
if (selectedVideoId != null) {
const rawVideos = Object.values(selectors.app.videos(state));
const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId);
// TODO it's missing load the transcripts
rawVideoData = {
edx_video_id: selectedVideo.edx_video_id,
thumbnail: selectedVideo.course_video_image_url,
duration: selectedVideo.duration,
transcripts: selectedVideo.transcripts,
selectedVideoTranscriptUrls: selectedVideo.transcript_urls,
};
}
const studioView = state.app.studioView?.data?.html;
@@ -32,7 +33,10 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => {
html5Sources: rawVideoData.html5_sources,
});
const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' });
const transcripts = module.parseTranscripts({ transcriptsData: studioView });
const transcripts = rawVideoData.transcripts ? rawVideoData.transcripts
: module.parseTranscripts({ transcriptsData: studioView });
const [courseLicenseType, courseLicenseDetails] = module.parseLicense({
licenseData: courseData.license,
level: 'course',
@@ -50,6 +54,7 @@ export const loadVideoData = (selectedVideoId) => (dispatch, getState) => {
videoSharingLearnMoreLink: blockValueData?.video_sharing_doc_url,
videoSharingEnabledForCourse: blockValueData?.video_sharing_enabled,
transcripts,
selectedVideoTranscriptUrls: rawVideoData.selectedVideoTranscriptUrls,
allowTranscriptDownloads: rawVideoData.download_track,
showTranscriptByDefault: rawVideoData.show_captions,
duration: { // TODO duration is not always sent so they should be calculated.

View File

@@ -87,6 +87,8 @@ const testVideosState = {
edx_video_id: mockSelectedVideoId,
thumbnail: 'thumbnail',
duration: 60,
transcripts: ['es'],
transcript_urls: { es: 'url' },
};
const testUpload = { transcripts: ['la', 'en'] };
const testReplaceUpload = {
@@ -228,7 +230,8 @@ describe('video thunkActions', () => {
videoId: 'videOiD',
fallbackVideos: 'fALLbACKvIDeos',
allowVideoDownloads: undefined,
transcripts: testMetadata.transcripts,
transcripts: testVideosState.transcripts,
selectedVideoTranscriptUrls: testVideosState.transcript_urls,
allowTranscriptDownloads: undefined,
allowVideoSharing: {
level: 'course',

View File

@@ -19,6 +19,7 @@ const initialState = {
videoSharingLearnMoreLink: '',
thumbnail: null,
transcripts: [],
selectedVideoTranscriptUrls: {},
allowTranscriptDownloads: false,
duration: {
startTime: '00:00:00',

View File

@@ -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,
};

View File

@@ -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}`,

View File

@@ -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}`
);