feat: add import transcripts from youtube (#176)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}) => (
|
||||
<Stack gap={3} className="border rounded border-primary-200 p-4">
|
||||
<ActionRow className="h5">
|
||||
<FormattedMessage {...messages.importHeader} />
|
||||
<ActionRow.Spacer />
|
||||
<IconButton
|
||||
src={Close}
|
||||
iconAs={Icon}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
</ActionRow>
|
||||
<FormattedMessage {...messages.importMessage} />
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={importTranscript}
|
||||
>
|
||||
<FormattedMessage {...messages.importButtonLabel} />
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
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));
|
||||
@@ -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(<module.ImportTranscriptCard {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior inspection', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<module.ImportTranscriptCard {...props} />);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportTranscriptCard snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<Stack
|
||||
className="border rounded border-primary-200 p-4"
|
||||
gap={3}
|
||||
>
|
||||
<ActionRow
|
||||
className="h5"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Import transcript from YouTube?"
|
||||
description="Header for import transcript card"
|
||||
id="authoring.videoEditor.transcripts.importCard.header"
|
||||
/>
|
||||
<ActionRow.Spacer />
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
onClick={[Function]}
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
</ActionRow>
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.videoEditor.transcrtipts.importCard.message"
|
||||
/>
|
||||
<Button
|
||||
onClick={[MockFunction args.importTranscript]}
|
||||
size="sm"
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Import Transcript"
|
||||
description="Label for youTube import transcript button"
|
||||
id="authoring.videoEditor.transcripts.importButton.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -30,9 +30,11 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={2.5}
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Group
|
||||
className="border-primary-100 border-bottom"
|
||||
>
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
@@ -98,7 +100,7 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<div
|
||||
className="border-primary-100 border-top pt-4"
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
@@ -147,9 +149,11 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={2.5}
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Group
|
||||
className="border-primary-100 border-bottom"
|
||||
>
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
@@ -219,7 +223,7 @@ exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<div
|
||||
className="border-primary-100 border-top pt-4"
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
@@ -268,9 +272,11 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={2.5}
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Group
|
||||
className="border-primary-100 border-bottom"
|
||||
>
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
@@ -336,7 +342,67 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<div
|
||||
className="border-primary-100 border-top pt-4"
|
||||
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 allowTranscriptImport true 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"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
setOpen={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
@@ -385,7 +451,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={2.5}
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add video transcripts (.srt files only) for improved accessibility."
|
||||
@@ -393,7 +459,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
id="authoring.videoeditor.transcripts.upload.firstTranscriptMessage"
|
||||
/>
|
||||
<div
|
||||
className="border-primary-100 border-top pt-4"
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
@@ -442,9 +508,11 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={2.5}
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Group
|
||||
className="border-primary-100 border-bottom"
|
||||
>
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
@@ -510,7 +578,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<div
|
||||
className="border-primary-100 border-top pt-4"
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
@@ -559,9 +627,11 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={2.5}
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Group
|
||||
className="border-primary-100 border-bottom"
|
||||
>
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
@@ -627,7 +697,7 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<div
|
||||
className="border-primary-100 border-top pt-4"
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
|
||||
@@ -26,6 +26,7 @@ import { in8lTranscriptLanguages } from '../../../../../../data/constants/video'
|
||||
import ErrorAlert from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import CollapsibleFormWidget from '../CollapsibleFormWidget';
|
||||
|
||||
import ImportTranscriptCard from './ImportTranscriptCard';
|
||||
import Transcript from './Transcript';
|
||||
import { ErrorContext } from '../../../../hooks';
|
||||
import * as module from './index';
|
||||
@@ -79,15 +80,18 @@ export const TranscriptWidget = ({
|
||||
transcripts,
|
||||
allowTranscriptDownloads,
|
||||
showTranscriptByDefault,
|
||||
allowTranscriptImport,
|
||||
updateField,
|
||||
isUploadError,
|
||||
isDeleteError,
|
||||
// intl
|
||||
// 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);
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
fontSize="x-small"
|
||||
@@ -107,10 +111,10 @@ export const TranscriptWidget = ({
|
||||
>
|
||||
<FormattedMessage {...messages.deleteTranscriptError} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={2.5}>
|
||||
<Stack gap={3}>
|
||||
{hasTranscripts ? (
|
||||
<Form.Group>
|
||||
{ transcripts.map((language, index) => (
|
||||
<Form.Group className="border-primary-100 border-bottom">
|
||||
{transcripts.map((language, index) => (
|
||||
<Transcript
|
||||
language={language}
|
||||
index={index}
|
||||
@@ -152,9 +156,12 @@ export const TranscriptWidget = ({
|
||||
) : (
|
||||
<>
|
||||
<FormattedMessage {...messages.addFirstTranscript} />
|
||||
{showImportCard && allowTranscriptImport
|
||||
? <ImportTranscriptCard setOpen={setShowImportCard} />
|
||||
: null}
|
||||
</>
|
||||
)}
|
||||
<div className="border-primary-100 border-top pt-4">
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
size="sm"
|
||||
@@ -177,6 +184,7 @@ TranscriptWidget.propTypes = {
|
||||
transcripts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
allowTranscriptDownloads: PropTypes.bool.isRequired,
|
||||
showTranscriptByDefault: PropTypes.bool.isRequired,
|
||||
allowTranscriptImport: PropTypes.bool.isRequired,
|
||||
updateField: PropTypes.func.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
isDeleteError: PropTypes.bool.isRequired,
|
||||
@@ -186,6 +194,7 @@ export const mapStateToProps = (state) => ({
|
||||
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 }),
|
||||
});
|
||||
|
||||
@@ -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(<module.TranscriptWidget {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with allowTranscriptImport true', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} allowTranscriptImport />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcripts', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} transcripts={['en']} />),
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,6 +22,8 @@ export const RequestKeys = StrictDict({
|
||||
fetchCourseDetails: 'fetchCourseDetails',
|
||||
updateTranscriptLanguage: 'updateTranscriptLanguage',
|
||||
getTranscriptFile: 'getTranscriptFile',
|
||||
checkTranscriptsForImport: 'checkTranscriptsForImport',
|
||||
importTranscript: 'importTranscript',
|
||||
uploadImage: 'uploadImage',
|
||||
fetchAdvanceSettings: 'fetchAdvanceSettings',
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -35,6 +35,7 @@ const initialState = {
|
||||
shareAlike: false,
|
||||
},
|
||||
allowThumbnailUpload: null,
|
||||
allowTranscriptImport: false,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 } },
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user