feat: add handout widget

This commit is contained in:
Kristin Aoki
2022-10-19 12:17:59 -04:00
committed by GitHub
parent 8a2c337170
commit cb01ff17a0
25 changed files with 654 additions and 64 deletions

View File

@@ -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 = {};

View File

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

View File

@@ -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 (
<CollapsibleFormWidget title="Handout">
<p>{handout.formValue}</p>
</CollapsibleFormWidget>
);
};
export default HandoutWidget;

View File

@@ -0,0 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HandoutWidget snapshots snapshots: renders as expected with default props 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={true}
subtitle="None"
title="Handout"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
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"
id="authoring.videoeditor.handout.error.fileSizeError"
/>
</ErrorAlert>
<UploadErrorAlert
message={
Object {
"defaultMessage": "Failed to upload handout. Please try again.",
"description": "Message presented to user when handout fails to upload",
"id": "authoring.videoeditor.handout.error.uploadHandoutError",
}
}
/>
<FileInput
fileInput={
Object {
"addFile": [Function],
"click": [Function],
"ref": Object {
"current": undefined,
},
}
}
/>
<Stack
gap={3}
>
<FormattedMessage
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"
id="authoring.videoeditor.handout.upload.addHandoutMessage"
/>
<Button
onClick={[Function]}
variant="link"
>
<FormattedMessage
defaultMessage="Upload Handout"
description="Label for upload button"
id="authoring.videoeditor.handout.upload.label"
/>
</Button>
</Stack>
</injectIntl(ShimmedIntlComponent)>
`;
exports[`HandoutWidget snapshots snapshots: renders as expected with handout 1`] = `
<injectIntl(ShimmedIntlComponent)
isError={true}
subtitle="sOMeUrl "
title="Handout"
>
<ErrorAlert
dismissError={[Function]}
hideHeading={true}
isError={false}
>
<FormattedMessage
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"
id="authoring.videoeditor.handout.error.fileSizeError"
/>
</ErrorAlert>
<UploadErrorAlert
message={
Object {
"defaultMessage": "Failed to upload handout. Please try again.",
"description": "Message presented to user when handout fails to upload",
"id": "authoring.videoeditor.handout.error.uploadHandoutError",
}
}
/>
<FileInput
fileInput={
Object {
"addFile": [Function],
"click": [Function],
"ref": Object {
"current": undefined,
},
}
}
/>
<Card>
<Card.Header
actions={
<Dropdown>
<Dropdown.Toggle
alt="Actions dropdown"
as="IconButton"
iconAs="Icon"
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
variant="primary"
/>
<Dropdown.Menu
className="video_handout Action Menu"
>
<Dropdown.Item
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Replace"
description="Message Presented To user for action to replace handout"
id="authoring.videoeditor.handout.replaceHandout"
/>
</Dropdown.Item>
<Dropdown.Item
target="_blank"
>
<FormattedMessage
defaultMessage="Download"
description="Message Presented To user for action to download handout"
id="authoring.videoeditor.handout.downloadHandout"
/>
</Dropdown.Item>
<Dropdown.Item
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Delete"
description="Message Presented To user for action to delete handout"
id="authoring.videoeditor.handout.deleteHandout"
/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
}
className="mt-1"
subtitle="sOMeUrl "
/>
</Card>
</injectIntl(ShimmedIntlComponent)>
`;

View File

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

View File

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

View File

@@ -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 (
<CollapsibleFormWidget
isError={Object.keys(error).length !== 0}
title={intl.formatMessage(messages.titleLabel)}
subtitle={handoutName}
>
<ErrorAlert
dismissError={fileSizeError.dismiss}
hideHeading
isError={fileSizeError.show}
>
<FormattedMessage {...messages.fileSizeError} />
</ErrorAlert>
<UploadErrorAlert message={messages.uploadHandoutError} />
<FileInput fileInput={fileInput} />
{handout ? (
<Card>
<Card.Header
className="mt-1"
subtitle={handoutName}
actions={(
<Dropdown>
<Dropdown.Toggle
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
as={IconButton}
src={MoreHoriz}
iconAs={Icon}
variant="primary"
alt="Actions dropdown"
/>
<Dropdown.Menu className="video_handout Action Menu">
<Dropdown.Item
key="handout-actions-replace"
onClick={fileInput.click}
>
<FormattedMessage {...messages.replaceHandout} />
</Dropdown.Item>
<Dropdown.Item key="handout-actions-download" target="_blank" href={downloadLink}>
<FormattedMessage {...messages.downloadHandout} />
</Dropdown.Item>
<Dropdown.Item key="handout-actions-delete" onClick={() => updateField({ handout: null })}>
<FormattedMessage {...messages.deleteHandout} />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)}
/>
</Card>
) : (
<Stack gap={3}>
<FormattedMessage {...messages.addHandoutMessage} />
<Button iconBefore={FileUpload} onClick={fileInput.click} variant="link">
<FormattedMessage {...messages.uploadButtonLabel} />
</Button>
</Stack>
)}
</CollapsibleFormWidget>
);
};
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));

View File

@@ -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(<HandoutWidget {...props} />),
).toMatchSnapshot();
});
test('snapshots: renders as expected with handout', () => {
expect(
shallow(<HandoutWidget {...props} handout="sOMeUrl " />),
).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));
});
});
});

View File

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

View File

@@ -13,7 +13,7 @@ export const RequestKeys = StrictDict({
fetchStudioView: 'fetchStudioView',
fetchUnit: 'fetchUnit',
saveBlock: 'saveBlock',
uploadImage: 'uploadImage',
uploadAsset: 'uploadAsset',
allowThumbnailUpload: 'allowThumbnailUpload',
uploadThumbnail: 'uploadThumbnail',
uploadTranscript: 'uploadTranscript',

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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',

View File

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

View File

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

View File

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

View File

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