feat: add handout widget
This commit is contained in:
@@ -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 = {};
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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)>
|
||||
`;
|
||||
@@ -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 };
|
||||
@@ -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],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -13,7 +13,7 @@ export const RequestKeys = StrictDict({
|
||||
fetchStudioView: 'fetchStudioView',
|
||||
fetchUnit: 'fetchUnit',
|
||||
saveBlock: 'saveBlock',
|
||||
uploadImage: 'uploadImage',
|
||||
uploadAsset: 'uploadAsset',
|
||||
allowThumbnailUpload: 'allowThumbnailUpload',
|
||||
uploadThumbnail: 'uploadThumbnail',
|
||||
uploadTranscript: 'uploadTranscript',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user