diff --git a/src/editors/containers/TextEditor/components/SelectImageModal/index.jsx b/src/editors/containers/TextEditor/components/SelectImageModal/index.jsx
index b1033e243..b9bf9faa4 100644
--- a/src/editors/containers/TextEditor/components/SelectImageModal/index.jsx
+++ b/src/editors/containers/TextEditor/components/SelectImageModal/index.jsx
@@ -106,7 +106,7 @@ SelectImageModal.propTypes = {
};
export const mapStateToProps = (state) => ({
- inputIsLoading: selectors.requests.isPending(state, { requestKey: RequestKeys.uploadImage }),
+ inputIsLoading: selectors.requests.isPending(state, { requestKey: RequestKeys.uploadAsset }),
});
export const mapDispatchToProps = {};
diff --git a/src/editors/containers/TextEditor/components/SelectImageModal/index.test.jsx b/src/editors/containers/TextEditor/components/SelectImageModal/index.test.jsx
index 1780fc255..0738b6295 100644
--- a/src/editors/containers/TextEditor/components/SelectImageModal/index.test.jsx
+++ b/src/editors/containers/TextEditor/components/SelectImageModal/index.test.jsx
@@ -95,9 +95,9 @@ describe('SelectImageModal', () => {
});
describe('mapStateToProps', () => {
const testState = { some: 'testState' };
- test('loads inputIsLoading from requests.isPending selector for uploadImage request', () => {
+ test('loads inputIsLoading from requests.isPending selector for uploadAsset request', () => {
expect(mapStateToProps(testState).inputIsLoading).toEqual(
- selectors.requests.isPending(testState, { requestKey: RequestKeys.uploadImage }),
+ selectors.requests.isPending(testState, { requestKey: RequestKeys.uploadAsset }),
);
});
});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget.jsx
deleted file mode 100644
index 0aca58d38..000000000
--- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { useDispatch } from 'react-redux';
-// import PropTypes from 'prop-types';
-
-import hooks from './hooks';
-import CollapsibleFormWidget from './CollapsibleFormWidget';
-
-/**
- * Collapsible Form widget controlling video handouts
- */
-export const HandoutWidget = () => {
- const dispatch = useDispatch();
- const { handout } = hooks.widgetValues({
- dispatch,
- fields: { [hooks.selectorKeys.handout]: hooks.genericWidget },
- });
- return (
-
- {handout.formValue}
-
- );
-};
-
-export default HandoutWidget;
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/__snapshots__/index.test.jsx.snap
new file mode 100644
index 000000000..77d570038
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HandoutWidget snapshots snapshots: renders as expected with default props 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`HandoutWidget snapshots snapshots: renders as expected with handout 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ className="mt-1"
+ subtitle="sOMeUrl "
+ />
+
+
+`;
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/hooks.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/hooks.jsx
new file mode 100644
index 000000000..3dc7cc477
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/hooks.jsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { useDispatch } from 'react-redux';
+import { thunkActions } from '../../../../../../data/redux';
+import * as module from './hooks';
+
+export const state = {
+ showSizeError: (args) => React.useState(args),
+};
+
+export const parseHandoutName = ({ handout }) => {
+ if (handout) {
+ const handoutName = handout.slice(handout.lastIndexOf('@') + 1);
+ return handoutName;
+ }
+ return 'None';
+};
+
+export const checkValidFileSize = ({
+ file,
+ onSizeFail,
+}) => {
+ // Check if the file size is greater than 20 MB, upload size limit
+ if (file.size > 20000000) {
+ onSizeFail();
+ return false;
+ }
+ return true;
+};
+
+export const fileInput = ({ fileSizeError }) => {
+ const dispatch = useDispatch();
+ const ref = React.useRef();
+ const click = () => ref.current.click();
+ const addFile = (e) => {
+ const file = e.target.files[0];
+ if (file && module.checkValidFileSize({
+ file,
+ onSizeFail: () => {
+ fileSizeError.set();
+ },
+ })) {
+ dispatch(thunkActions.video.uploadHandout({
+ file,
+ }));
+ }
+ };
+ return {
+ click,
+ addFile,
+ ref,
+ };
+};
+
+export const fileSizeError = () => {
+ const [showSizeError, setShowSizeError] = module.state.showSizeError(false);
+ return {
+ fileSizeError: {
+ show: showSizeError,
+ set: () => setShowSizeError(true),
+ dismiss: () => setShowSizeError(false),
+ },
+ };
+};
+
+export default { fileInput, fileSizeError, parseHandoutName };
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/hooks.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/hooks.test.jsx
new file mode 100644
index 000000000..a733ff570
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/hooks.test.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { dispatch } from 'react-redux';
+import { thunkActions } from '../../../../../../data/redux';
+import { MockUseState } from '../../../../../../../testUtils';
+import { keyStore } from '../../../../../../utils';
+import * as hooks from './hooks';
+
+jest.mock('react', () => ({
+ ...jest.requireActual('react'),
+ useRef: jest.fn(val => ({ current: val })),
+ useEffect: jest.fn(),
+ useCallback: (cb, prereqs) => ({ cb, prereqs }),
+}));
+
+jest.mock('react-redux', () => {
+ const dispatchFn = jest.fn();
+ return {
+ ...jest.requireActual('react-redux'),
+ dispatch: dispatchFn,
+ useDispatch: jest.fn(() => dispatchFn),
+ };
+});
+
+jest.mock('../../../../../../data/redux', () => ({
+ thunkActions: {
+ video: {
+ uploadHandout: jest.fn(),
+ },
+ },
+}));
+
+const state = new MockUseState(hooks);
+const hookKeys = keyStore(hooks);
+let hook;
+const testValue = '@testVALUEVALIDhANdoUT';
+const selectedFileSuccess = { name: testValue, size: 20000 };
+describe('VideoEditorHandout hooks', () => {
+ describe('state hooks', () => {
+ state.testGetter(state.keys.showSizeError);
+ });
+
+ describe('parseHandoutName', () => {
+ test('it returns none when given null', () => {
+ expect(hooks.parseHandoutName({ handout: null })).toEqual('None');
+ });
+ test('it creates a list based on transcript object', () => {
+ expect(hooks.parseHandoutName({ handout: testValue })).toEqual('testVALUEVALIDhANdoUT');
+ });
+ });
+
+ describe('checkValidSize', () => {
+ const onSizeFail = jest.fn();
+ it('returns false for file size', () => {
+ hook = hooks.checkValidFileSize({ file: { name: testValue, size: 20000001 }, onSizeFail });
+ expect(onSizeFail).toHaveBeenCalled();
+ expect(hook).toEqual(false);
+ });
+ it('returns true for valid file size', () => {
+ hook = hooks.checkValidFileSize({ file: selectedFileSuccess, onSizeFail });
+ expect(hook).toEqual(true);
+ });
+ });
+ describe('fileInput', () => {
+ const spies = {};
+ const fileSizeError = { set: jest.fn() };
+ beforeEach(() => {
+ jest.clearAllMocks();
+ hook = hooks.fileInput({ fileSizeError });
+ });
+ it('returns a ref for the file input', () => {
+ expect(hook.ref).toEqual({ current: undefined });
+ });
+ test('click calls current.click on the ref', () => {
+ const click = jest.fn();
+ React.useRef.mockReturnValueOnce({ current: { click } });
+ hook = hooks.fileInput({ fileSizeError });
+ hook.click();
+ expect(click).toHaveBeenCalled();
+ });
+ describe('addFile', () => {
+ const eventSuccess = { target: { files: [{ selectedFileSuccess }] } };
+ const eventFailure = { target: { files: [{ name: testValue, size: 20000001 }] } };
+ it('image fails to upload if file size is greater than 2000000', () => {
+ const checkValidFileSize = false;
+ spies.checkValidFileSize = jest.spyOn(hooks, hookKeys.checkValidFileSize)
+ .mockReturnValueOnce(checkValidFileSize);
+ hook.addFile(eventFailure);
+ expect(spies.checkValidFileSize.mock.calls.length).toEqual(1);
+ expect(spies.checkValidFileSize).toHaveReturnedWith(false);
+ });
+ it('dispatches updateField action with the first target file', () => {
+ const checkValidFileSize = true;
+ spies.checkValidFileSize = jest.spyOn(hooks, hookKeys.checkValidFileSize)
+ .mockReturnValueOnce(checkValidFileSize);
+ hook.addFile(eventSuccess);
+ expect(spies.checkValidFileSize.mock.calls.length).toEqual(1);
+ expect(spies.checkValidFileSize).toHaveReturnedWith(true);
+ expect(dispatch).toHaveBeenCalledWith(
+ thunkActions.video.uploadHandout({
+ thumbnail: eventSuccess.target.files[0],
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx
new file mode 100644
index 000000000..65c047173
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+
+import {
+ Button,
+ Stack,
+ Icon,
+ IconButton,
+ Card,
+ Dropdown,
+} from '@edx/paragon';
+import { FileUpload, MoreHoriz } from '@edx/paragon/icons';
+import {
+ FormattedMessage,
+ injectIntl,
+ intlShape,
+} from '@edx/frontend-platform/i18n';
+
+import { actions, selectors } from '../../../../../../data/redux';
+import * as hooks from './hooks';
+import messages from './messages';
+
+import FileInput from '../../../../../../sharedComponents/FileInput';
+import { ErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
+import { UploadErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/UploadErrorAlert';
+import CollapsibleFormWidget from '../CollapsibleFormWidget';
+import { ErrorContext } from '../../../../hooks';
+
+/**
+ * Collapsible Form widget controlling video handouts
+ */
+export const HandoutWidget = ({
+ // injected
+ intl,
+ // redux
+ handout,
+ getHandoutDownloadUrl,
+ updateField,
+}) => {
+ const [error] = React.useContext(ErrorContext).handout;
+ const { fileSizeError } = hooks.fileSizeError();
+ const fileInput = hooks.fileInput({ fileSizeError });
+ const handoutName = hooks.parseHandoutName({ handout });
+ const downloadLink = getHandoutDownloadUrl({ handout });
+
+ return (
+
+
+
+
+
+
+ {handout ? (
+
+
+
+
+
+
+
+
+
+
+ updateField({ handout: null })}>
+
+
+
+
+ )}
+ />
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+};
+
+HandoutWidget.propTypes = {
+ // injected
+ intl: intlShape.isRequired,
+ // redux
+ handout: PropTypes.shape({}).isRequired,
+ updateField: PropTypes.func.isRequired,
+ isUploadError: PropTypes.bool.isRequired,
+ getHandoutDownloadUrl: PropTypes.func.isRequired,
+};
+export const mapStateToProps = (state) => ({
+ handout: selectors.video.handout(state),
+ getHandoutDownloadUrl: selectors.video.getHandoutDownloadUrl(state),
+});
+
+export const mapDispatchToProps = (dispatch) => ({
+ updateField: (payload) => dispatch(actions.video.updateField(payload)),
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(HandoutWidget));
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx
new file mode 100644
index 000000000..68ee4d3bf
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { formatMessage } from '../../../../../../../testUtils';
+import { actions, selectors } from '../../../../../../data/redux';
+import { HandoutWidget, mapStateToProps, mapDispatchToProps } from '.';
+
+jest.mock('react', () => ({
+ ...jest.requireActual('react'),
+ useContext: jest.fn(() => ({ handout: ['error.handout', jest.fn().mockName('error.setHandout')] })),
+}));
+
+jest.mock('../../../../../../data/redux', () => ({
+ actions: {
+ video: {
+ updateField: jest.fn().mockName('actions.video.updateField'),
+ },
+ },
+ selectors: {
+ video: {
+ getHandoutDownloadUrl: jest.fn(args => ({ getHandoutDownloadUrl: args })).mockName('selectors.video.getHandoutDownloadUrl'),
+ handout: jest.fn(state => ({ handout: state })),
+ },
+ },
+}));
+
+describe('HandoutWidget', () => {
+ const props = {
+ subtitle: 'SuBTItle',
+ title: 'tiTLE',
+ intl: { formatMessage },
+ handout: '',
+ getHandoutDownloadUrl: jest.fn().mockName('args.getHandoutDownloadUrl'),
+ updateField: jest.fn().mockName('args.updateField'),
+ };
+
+ describe('snapshots', () => {
+ test('snapshots: renders as expected with default props', () => {
+ expect(
+ shallow(),
+ ).toMatchSnapshot();
+ });
+ test('snapshots: renders as expected with handout', () => {
+ expect(
+ shallow(),
+ ).toMatchSnapshot();
+ });
+ });
+ describe('mapStateToProps', () => {
+ const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
+ test('handout from video.handout', () => {
+ expect(
+ mapStateToProps(testState).handout,
+ ).toEqual(selectors.video.handout(testState));
+ });
+ test('getHandoutDownloadUrl from video.getHandoutDownloadUrl', () => {
+ expect(
+ mapStateToProps(testState).getHandoutDownloadUrl,
+ ).toEqual(selectors.video.getHandoutDownloadUrl(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ const dispatch = jest.fn();
+ test('updateField from actions.video.updateField', () => {
+ expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
+ });
+ });
+});
diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/messages.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/messages.js
new file mode 100644
index 000000000..8520ccb0f
--- /dev/null
+++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/messages.js
@@ -0,0 +1,50 @@
+export const messages = {
+ titleLabel: {
+ id: 'authoring.videoeditor.handout.title.label',
+ defaultMessage: 'Handout',
+ description: 'Title for the handout widget',
+ },
+ uploadButtonLabel: {
+ id: 'authoring.videoeditor.handout.upload.label',
+ defaultMessage: 'Upload Handout',
+ description: 'Label for upload button',
+ },
+ addHandoutMessage: {
+ id: 'authoring.videoeditor.handout.upload.addHandoutMessage',
+ defaultMessage: `Add a handout to accompany this video. Learners can download
+ this file by clicking "Download Handout" below the video.`,
+ description: 'Message displayed when uploading a handout',
+ },
+ uploadHandoutError: {
+ id: 'authoring.videoeditor.handout.error.uploadHandoutError',
+ defaultMessage: 'Failed to upload handout. Please try again.',
+ description: 'Message presented to user when handout fails to upload',
+ },
+ fileSizeError: {
+ id: 'authoring.videoeditor.handout.error.fileSizeError',
+ defaultMessage: 'Handout files must be 20 MB or less. Please resize the file and try again.',
+ description: 'Message presented to user when handout file size is larger than 20 MB',
+ },
+ handoutHelpMessage: {
+ id: 'authoring.videoeditor.handout.handoutHelpMessage',
+ defaultMessage: 'Learners can download this file by clicking "Download Handout" below the video.',
+ description: 'Message presented to user when a handout is present',
+ },
+ deleteHandout: {
+ id: 'authoring.videoeditor.handout.deleteHandout',
+ defaultMessage: 'Delete',
+ description: 'Message Presented To user for action to delete handout',
+ },
+ replaceHandout: {
+ id: 'authoring.videoeditor.handout.replaceHandout',
+ defaultMessage: 'Replace',
+ description: 'Message Presented To user for action to replace handout',
+ },
+ downloadHandout: {
+ id: 'authoring.videoeditor.handout.downloadHandout',
+ defaultMessage: 'Download',
+ description: 'Message Presented To user for action to download handout',
+ },
+};
+
+export default messages;
diff --git a/src/editors/data/constants/requests.js b/src/editors/data/constants/requests.js
index be0c129df..7b43fdc91 100644
--- a/src/editors/data/constants/requests.js
+++ b/src/editors/data/constants/requests.js
@@ -13,7 +13,7 @@ export const RequestKeys = StrictDict({
fetchStudioView: 'fetchStudioView',
fetchUnit: 'fetchUnit',
saveBlock: 'saveBlock',
- uploadImage: 'uploadImage',
+ uploadAsset: 'uploadAsset',
allowThumbnailUpload: 'allowThumbnailUpload',
uploadThumbnail: 'uploadThumbnail',
uploadTranscript: 'uploadTranscript',
diff --git a/src/editors/data/redux/requests/reducer.js b/src/editors/data/redux/requests/reducer.js
index 7d714ede7..84e79f10a 100644
--- a/src/editors/data/redux/requests/reducer.js
+++ b/src/editors/data/redux/requests/reducer.js
@@ -10,12 +10,11 @@ const initialState = {
[RequestKeys.fetchStudioView]: { status: RequestStates.inactive },
[RequestKeys.saveBlock]: { status: RequestStates.inactive },
[RequestKeys.fetchImages]: { status: RequestStates.inactive },
- [RequestKeys.uploadImage]: { status: RequestStates.inactive },
+ [RequestKeys.uploadAsset]: { status: RequestStates.inactive },
[RequestKeys.allowThumbnailUpload]: { status: RequestStates.inactive },
[RequestKeys.uploadThumbnail]: { status: RequestStates.inactive },
[RequestKeys.uploadTranscript]: { status: RequestStates.inactive },
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },
-
};
// eslint-disable-next-line no-unused-vars
diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js
index 4fabd3388..f6d45b895 100644
--- a/src/editors/data/redux/thunkActions/app.js
+++ b/src/editors/data/redux/thunkActions/app.js
@@ -59,8 +59,8 @@ export const saveBlock = ({ content, returnToUnit }) => (dispatch) => {
};
export const uploadImage = ({ file, setSelection }) => (dispatch) => {
- dispatch(requests.uploadImage({
- image: file,
+ dispatch(requests.uploadAsset({
+ asset: file,
onSuccess: (response) => setSelection(camelizeKeys(response.data.asset)),
}));
};
diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js
index 263e49eb1..486825a10 100644
--- a/src/editors/data/redux/thunkActions/app.test.js
+++ b/src/editors/data/redux/thunkActions/app.test.js
@@ -7,7 +7,7 @@ jest.mock('./requests', () => ({
fetchUnit: (args) => ({ fetchUnit: args }),
saveBlock: (args) => ({ saveBlock: args }),
fetchImages: (args) => ({ fetchImages: args }),
- uploadImage: (args) => ({ uploadImage: args }),
+ uploadAsset: (args) => ({ uploadAsset: args }),
fetchStudioView: (args) => ({ fetchStudioView: args }),
}));
@@ -143,14 +143,14 @@ describe('app thunkActions', () => {
thunkActions.uploadImage({ file: testValue, setSelection })(dispatch);
[[dispatchedAction]] = dispatch.mock.calls;
});
- it('dispatches uploadImage action', () => {
- expect(dispatchedAction.uploadImage).not.toBe(undefined);
+ it('dispatches uploadAsset action', () => {
+ expect(dispatchedAction.uploadAsset).not.toBe(undefined);
});
test('passes file as image prop', () => {
- expect(dispatchedAction.uploadImage.image).toEqual(testValue);
+ expect(dispatchedAction.uploadAsset.asset).toEqual(testValue);
});
test('onSuccess: calls setSelection with camelized response.data.asset', () => {
- dispatchedAction.uploadImage.onSuccess({ data: { asset: testValue } });
+ dispatchedAction.uploadAsset.onSuccess({ data: { asset: testValue } });
expect(setSelection).toHaveBeenCalledWith(camelizeKeys(testValue));
});
});
diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js
index 39be38647..c02316ba7 100644
--- a/src/editors/data/redux/thunkActions/requests.js
+++ b/src/editors/data/redux/thunkActions/requests.js
@@ -110,12 +110,12 @@ export const saveBlock = ({ content, ...rest }) => (dispatch, getState) => {
...rest,
}));
};
-export const uploadImage = ({ image, ...rest }) => (dispatch, getState) => {
+export const uploadAsset = ({ asset, ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
- requestKey: RequestKeys.uploadImage,
- promise: api.uploadImage({
+ requestKey: RequestKeys.uploadAsset,
+ promise: api.uploadAsset({
learningContextId: selectors.app.learningContextId(getState()),
- image,
+ asset,
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
}),
...rest,
@@ -193,7 +193,7 @@ export default StrictDict({
fetchStudioView,
fetchUnit,
saveBlock,
- uploadImage,
+ uploadAsset,
allowThumbnailUpload,
uploadThumbnail,
deleteTranscript,
diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js
index 58d2a48cd..4da286cbc 100644
--- a/src/editors/data/redux/thunkActions/requests.test.js
+++ b/src/editors/data/redux/thunkActions/requests.test.js
@@ -26,7 +26,7 @@ jest.mock('../../services/cms/api', () => ({
fetchByUnitId: ({ id, url }) => ({ id, url }),
saveBlock: (args) => args,
fetchImages: ({ id, url }) => ({ id, url }),
- uploadImage: (args) => args,
+ uploadAsset: (args) => args,
loadImages: jest.fn(),
allowThumbnailUpload: jest.fn(),
uploadThumbnail: jest.fn(),
@@ -267,18 +267,18 @@ describe('requests thunkActions module', () => {
},
});
});
- describe('uploadImage', () => {
- const image = 'SoME iMage CoNtent As String';
+ describe('uploadAsset', () => {
+ const asset = 'SoME iMage CoNtent As String';
testNetworkRequestAction({
- action: requests.uploadImage,
- args: { image, ...fetchParams },
- expectedString: 'with uploadImage promise',
+ action: requests.uploadAsset,
+ args: { asset, ...fetchParams },
+ expectedString: 'with uploadAsset promise',
expectedData: {
...fetchParams,
- requestKey: RequestKeys.uploadImage,
- promise: api.uploadImage({
+ requestKey: RequestKeys.uploadAsset,
+ promise: api.uploadAsset({
learningContextId: selectors.app.learningContextId(testState),
- image,
+ asset,
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
}),
},
diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js
index aa29440c2..8b034eea8 100644
--- a/src/editors/data/redux/thunkActions/video.js
+++ b/src/editors/data/redux/thunkActions/video.js
@@ -180,6 +180,18 @@ export const uploadThumbnail = ({ thumbnail }) => (dispatch, getState) => {
}));
};
+// Handout Thunks:
+
+export const uploadHandout = ({ file }) => (dispatch) => {
+ dispatch(requests.uploadAsset({
+ asset: file,
+ onSuccess: (response) => {
+ const handout = response.data.asset.url;
+ dispatch(actions.video.updateField({ handout }));
+ },
+ }));
+};
+
// Transcript Thunks:
export const uploadTranscript = ({ language, filename, file }) => (dispatch, getState) => {
@@ -256,4 +268,5 @@ export default {
uploadTranscript,
deleteTranscript,
replaceTranscript,
+ uploadHandout,
};
diff --git a/src/editors/data/redux/thunkActions/video.test.js b/src/editors/data/redux/thunkActions/video.test.js
index 2c43047f2..dec480432 100644
--- a/src/editors/data/redux/thunkActions/video.test.js
+++ b/src/editors/data/redux/thunkActions/video.test.js
@@ -17,6 +17,7 @@ jest.mock('..', () => ({
},
}));
jest.mock('./requests', () => ({
+ uploadAsset: (args) => ({ uploadAsset: args }),
allowThumbnailUpload: (args) => ({ allowThumbnailUpload: args }),
uploadThumbnail: (args) => ({ uploadThumbnail: args }),
deleteTranscript: (args) => ({ deleteTranscript: args }),
@@ -248,6 +249,23 @@ describe('video thunkActions', () => {
]);
});
});
+ describe('uploadHandout', () => {
+ beforeEach(() => {
+ thunkActions.uploadHandout({ file: mockFilename })(dispatch);
+ [[dispatchedAction]] = dispatch.mock.calls;
+ });
+ it('dispatches uploadAsset action', () => {
+ expect(dispatchedAction.uploadAsset).not.toBe(undefined);
+ });
+ test('passes file as image prop', () => {
+ expect(dispatchedAction.uploadAsset.asset).toEqual(mockFilename);
+ });
+ test('onSuccess: calls setSelection with camelized response.data.asset', () => {
+ const handout = mockFilename;
+ dispatchedAction.uploadAsset.onSuccess({ data: { asset: { url: mockFilename } } });
+ expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ handout }));
+ });
+ });
describe('uploadThumbnail', () => {
beforeEach(() => {
thunkActions.uploadThumbnail({ thumbnail: mockThumbnail })(dispatch, getState);
diff --git a/src/editors/data/redux/video/selectors.js b/src/editors/data/redux/video/selectors.js
index cf6193586..b6c071e26 100644
--- a/src/editors/data/redux/video/selectors.js
+++ b/src/editors/data/redux/video/selectors.js
@@ -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 } from '../../services/cms/urls';
+import { downloadVideoTranscriptURL, downloadVideoHandoutUrl } from '../../services/cms/urls';
const stateKeys = keyStore(initialState);
@@ -51,6 +51,14 @@ export const getTranscriptDownloadUrl = createSelector(
}),
);
+export const getHandoutDownloadUrl = createSelector(
+ [AppSelectors.simpleSelectors.studioEndpointUrl],
+ (studioEndpointUrl) => ({ handout }) => downloadVideoHandoutUrl({
+ studioEndpointUrl,
+ handout,
+ }),
+);
+
export const videoSettings = createSelector(
[
module.simpleSelectors.videoSource,
@@ -101,5 +109,6 @@ export default {
...simpleSelectors,
openLanguages,
getTranscriptDownloadUrl,
+ getHandoutDownloadUrl,
videoSettings,
};
diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js
index 0932ecd9d..71cd7a6a9 100644
--- a/src/editors/data/services/cms/api.js
+++ b/src/editors/data/services/cms/api.js
@@ -17,13 +17,13 @@ export const apiMethods = {
fetchImages: ({ learningContextId, studioEndpointUrl }) => get(
urls.courseImages({ studioEndpointUrl, learningContextId }),
),
- uploadImage: ({
+ uploadAsset: ({
learningContextId,
studioEndpointUrl,
- image,
+ asset,
}) => {
const data = new FormData();
- data.append('file', image);
+ data.append('file', asset);
return post(
urls.courseAssets({ studioEndpointUrl, learningContextId }),
data,
diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js
index f105c9468..64aa57c16 100644
--- a/src/editors/data/services/cms/api.test.js
+++ b/src/editors/data/services/cms/api.test.js
@@ -173,15 +173,15 @@ describe('cms api', () => {
});
});
- describe('uploadImage', () => {
- const image = { photo: 'dAta' };
+ describe('uploadAsset', () => {
+ const asset = { photo: 'dAta' };
it('should call post with urls.courseAssets and imgdata', () => {
const mockFormdata = new FormData();
- mockFormdata.append('file', image);
- apiMethods.uploadImage({
+ mockFormdata.append('file', asset);
+ apiMethods.uploadAsset({
learningContextId,
studioEndpointUrl,
- image,
+ asset,
});
expect(post).toHaveBeenCalledWith(
urls.videoTranscripts({ studioEndpointUrl, learningContextId }),
diff --git a/src/editors/data/services/cms/mockApi.js b/src/editors/data/services/cms/mockApi.js
index ce9e32754..1a32d38eb 100644
--- a/src/editors/data/services/cms/mockApi.js
+++ b/src/editors/data/services/cms/mockApi.js
@@ -160,13 +160,13 @@ export const saveBlock = ({
}),
});
-export const uploadImage = ({
+export const uploadAsset = ({
learningContextId,
studioEndpointUrl,
// image,
}) => mockPromise({
url: urls.courseAssets({ studioEndpointUrl, learningContextId }),
- image: {
+ asset: {
asset: {
display_name: 'journey_escape.jpg',
content_type: 'image/jpeg',
diff --git a/src/editors/data/services/cms/urls.js b/src/editors/data/services/cms/urls.js
index 27424164c..70ebcecec 100644
--- a/src/editors/data/services/cms/urls.js
+++ b/src/editors/data/services/cms/urls.js
@@ -50,3 +50,7 @@ export const videoTranscripts = ({ studioEndpointUrl, blockId }) => (
export const downloadVideoTranscriptURL = ({ studioEndpointUrl, blockId, language }) => (
`${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`
);
+
+export const downloadVideoHandoutUrl = ({ studioEndpointUrl, handout }) => (
+ `${studioEndpointUrl}${handout}`
+);
diff --git a/src/editors/data/services/cms/urls.test.js b/src/editors/data/services/cms/urls.test.js
index 59bb37c99..141047bf4 100644
--- a/src/editors/data/services/cms/urls.test.js
+++ b/src/editors/data/services/cms/urls.test.js
@@ -9,6 +9,7 @@ import {
courseImages,
downloadVideoTranscriptURL,
videoTranscripts,
+ downloadVideoHandoutUrl,
} from './urls';
describe('cms url methods', () => {
@@ -18,6 +19,7 @@ describe('cms url methods', () => {
const courseId = 'course-v1:courseId123';
const libraryV1Id = 'library-v1:libaryId123';
const language = 'la';
+ const handout = '/aSSet@hANdoUt';
describe('return to learning context urls', () => {
const unitUrl = {
data: {
@@ -92,4 +94,10 @@ describe('cms url methods', () => {
.toEqual(`${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`);
});
});
+ describe('downloadVideoHandoutUrl', () => {
+ it('returns url with studioEndpointUrl and handout', () => {
+ expect(downloadVideoHandoutUrl({ studioEndpointUrl, handout }))
+ .toEqual(`${studioEndpointUrl}${handout}`);
+ });
+ });
});
diff --git a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx
index fd2552f17..74f4e664a 100644
--- a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx
+++ b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx
@@ -32,7 +32,7 @@ UploadErrorAlert.propTypes = {
isUploadError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
- isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadImage }),
+ isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
});
export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(UploadErrorAlert);
diff --git a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx
index 1ddf44b28..8ae5d03ff 100644
--- a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx
+++ b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx
@@ -23,7 +23,7 @@ describe('UploadErrorAlert', () => {
test('isUploadError from requests.isFinished', () => {
expect(
mapStateToProps(testState).isUploadError,
- ).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadImage }));
+ ).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadAsset }));
});
});
});