feat: Prevent image uploads larger than 10 MB and add spinner

This commit is contained in:
Kristin Aoki
2022-08-08 15:44:59 -04:00
committed by GitHub
parent 9b23731acc
commit d739bcbdb5
11 changed files with 340 additions and 76 deletions

View File

@@ -7,7 +7,7 @@ exports[`FetchErrorAlert Snapshots snapshot: is ErrorAlert with Message error (
isError={true}
>
<FormattedMessage
defaultMessage="Failed to obtain course Images. Please Try again."
defaultMessage="Failed to obtain course images. Please try again."
description="Message presented to user when images are not found"
id="authoring.texteditor.selectimagemodal.error.fetchImagesError"
/>

View File

@@ -7,7 +7,7 @@ exports[`UploadErrorAlert Snapshots snapshot: is ErrorAlert with Message error
isError={true}
>
<FormattedMessage
defaultMessage="Failed to Upload Image. Please Try again."
defaultMessage="Failed to upload image. Please try again."
description="Message presented to user when image fails to upload"
id="authoring.texteditor.selectimagemodal.error.uploadImageError"
/>

View File

@@ -6,9 +6,9 @@ import ImageSettingsModal from './ImageSettingsModal';
import SelectImageModal from './SelectImageModal';
import * as module from './ImageUploadModal';
export const propsString = (props) => Object.keys(props)
.map(key => `${key}="${props[key]}"`)
.join(' ');
export const propsString = (props) => (
Object.keys(props).map((key) => `${key}="${props[key]}"`).join(' ')
);
export const imgProps = ({ settings, selection }) => ({
src: selection.externalUrl,
@@ -19,8 +19,13 @@ export const imgProps = ({ settings, selection }) => ({
export const hooks = {
createSaveCallback: ({
close, editorRef, setSelection, selection,
}) => (settings) => {
close,
editorRef,
setSelection,
selection,
}) => (
settings,
) => {
editorRef.current.execCommand(
tinyMCEKeys.commands.insertContent,
false,
@@ -67,7 +72,14 @@ export const ImageUploadModal = ({
);
}
return (
<SelectImageModal {...{ isOpen, close, setSelection }} />
<SelectImageModal
{...{
isOpen,
close,
setSelection,
clearSelection,
}}
/>
);
};

View File

@@ -21,7 +21,7 @@ exports[`SelectImageModal component snapshot 1`] = `
variant="link"
>
<FormattedMessage
defaultMessage="Upload a new image"
defaultMessage="Upload a new image (10 MB max)"
description="Label for upload button"
id="authoring.texteditor.selectimagemodal.upload.label"
/>
@@ -35,7 +35,18 @@ exports[`SelectImageModal component snapshot 1`] = `
<ErrorAlert
dismissError={[MockFunction]}
hideHeading={true}
isError="ShoWERror"
isError="ShoWERror inPUT"
>
<FormattedMessage
defaultMessage="Images must be 10 MB or less. Please resize image and try again."
description=" Message presented to user when file size of image is larger than 10 MB"
id="authoring.texteditor.selectimagemodal.error.fileSizeError"
/>
</ErrorAlert>
<ErrorAlert
dismissError={[MockFunction]}
hideHeading={true}
isError="ShoWERror gAlLery"
>
<FormattedMessage
defaultMessage="Select an image to continue."
@@ -64,3 +75,81 @@ exports[`SelectImageModal component snapshot 1`] = `
</Stack>
</BaseModal>
`;
exports[`SelectImageModal component snapshot: uploaded image not loaded, show spinner 1`] = `
<BaseModal
close={[MockFunction props.close]}
confirmAction={
<Button
select="btnProps"
variant="primary"
>
<FormattedMessage
defaultMessage="Next"
description="Label for Next button"
id="authoring.texteditor.selectimagemodal.next.label"
/>
</Button>
}
footerAction={
<Button
onClick="imgHooks.fileInput.click"
variant="link"
>
<FormattedMessage
defaultMessage="Upload a new image (10 MB max)"
description="Label for upload button"
id="authoring.texteditor.selectimagemodal.upload.label"
/>
</Button>
}
isOpen={true}
title="Add an image"
>
<FetchErrorAlert />
<UploadErrorAlert />
<ErrorAlert
dismissError={[MockFunction]}
hideHeading={true}
isError="ShoWERror inPUT"
>
<FormattedMessage
defaultMessage="Images must be 10 MB or less. Please resize image and try again."
description=" Message presented to user when file size of image is larger than 10 MB"
id="authoring.texteditor.selectimagemodal.error.fileSizeError"
/>
</ErrorAlert>
<ErrorAlert
dismissError={[MockFunction]}
hideHeading={true}
isError="ShoWERror gAlLery"
>
<FormattedMessage
defaultMessage="Select an image to continue."
description="Message presented to user when clicking Next without selecting an image"
id="authoring.texteditor.selectimagemodal.error.selectImageError"
/>
</ErrorAlert>
<Stack
gap={3}
>
<SearchSort
search="sortProps"
/>
<Spinner
animation="border"
className="mie-3"
screenReaderText="loading..."
/>
<FileInput
fileInput={
Object {
"addFile": "imgHooks.fileInput.addFile",
"click": "imgHooks.fileInput.click",
"ref": "imgHooks.fileInput.ref",
}
}
/>
</Stack>
</BaseModal>
`;

View File

@@ -11,6 +11,7 @@ export const state = {
showSelectImageError: (val) => React.useState(val),
searchString: (val) => React.useState(val),
sortBy: (val) => React.useState(val),
showSizeError: (val) => React.useState(val),
};
export const searchAndSortHooks = () => {
@@ -18,30 +19,32 @@ export const searchAndSortHooks = () => {
const [sortBy, setSortBy] = module.state.sortBy(sortKeys.dateNewest);
return {
searchString,
onSearchChange: e => setSearchString(e.target.value),
onSearchChange: (e) => setSearchString(e.target.value),
clearSearchString: () => setSearchString(''),
sortBy,
onSortClick: key => () => setSortBy(key),
onSortClick: (key) => () => setSortBy(key),
};
};
export const filteredList = ({ searchString, imageList }) => imageList.filter(
({ displayName }) => displayName.toLowerCase().includes(searchString.toLowerCase()),
export const filteredList = ({ searchString, imageList }) => (
imageList.filter(({ displayName }) => displayName.toLowerCase().includes(searchString.toLowerCase()))
);
export const displayList = ({ sortBy, searchString, images }) => module.filteredList({
searchString,
imageList: Object.values(images),
}).sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]);
export const displayList = ({ sortBy, searchString, images }) => (
module.filteredList({
searchString,
imageList: Object.values(images),
}).sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]));
export const imgListHooks = ({
searchSortProps,
setSelection,
}) => {
export const imgListHooks = ({ searchSortProps, setSelection }) => {
const dispatch = useDispatch();
const [images, setImages] = module.state.images({});
const [highlighted, setHighlighted] = module.state.highlighted(null);
const [showSelectImageError, setShowSelectImageError] = module.state.showSelectImageError(false);
const [
showSelectImageError,
setShowSelectImageError,
] = module.state.showSelectImageError(false);
const [showSizeError, setShowSizeError] = module.state.showSelectImageError(false);
const list = module.displayList({ ...searchSortProps, images });
React.useEffect(() => {
@@ -49,18 +52,23 @@ export const imgListHooks = ({
}, []);
return {
error: {
galleryError: {
show: showSelectImageError,
set: () => setShowSelectImageError(true),
dismiss: () => setShowSelectImageError(false),
},
inputError: {
show: showSizeError,
set: () => setShowSizeError(true),
dismiss: () => setShowSizeError(false),
},
images,
galleryProps: {
galleryIsEmpty: Object.keys(images).length === 0,
searchIsEmpty: list.length === 0,
displayList: list,
highlighted,
onHighlightChange: e => setHighlighted(e.target.value),
onHighlightChange: (e) => setHighlighted(e.target.value),
},
// highlight by id
selectBtnProps: {
@@ -75,15 +83,40 @@ export const imgListHooks = ({
};
};
export const fileInputHooks = ({ setSelection }) => {
export const checkValidFileSize = ({
selectedFile,
clearSelection,
onSizeFail,
}) => {
// Check if the file size is greater than 10 MB, upload size limit
if (selectedFile.size > 1000000) {
clearSelection();
onSizeFail();
return false;
}
return true;
};
export const fileInputHooks = ({ setSelection, clearSelection, imgList }) => {
const dispatch = useDispatch();
const ref = React.useRef();
const click = () => ref.current.click();
const addFile = (e) => {
dispatch(thunkActions.app.uploadImage({
file: e.target.files[0],
setSelection,
}));
const selectedFile = e.target.files[0];
if (selectedFile && module.checkValidFileSize({
selectedFile,
clearSelection,
onSizeFail: () => {
imgList.inputError.set();
},
})) {
dispatch(
thunkActions.app.uploadImage({
file: selectedFile,
setSelection,
}),
);
}
};
return {
@@ -93,18 +126,24 @@ export const fileInputHooks = ({ setSelection }) => {
};
};
export const imgHooks = ({ setSelection }) => {
export const imgHooks = ({ setSelection, clearSelection }) => {
const searchSortProps = module.searchAndSortHooks();
const imgList = module.imgListHooks({ setSelection, searchSortProps });
const fileInput = module.fileInputHooks({ setSelection });
const fileInput = module.fileInputHooks({
setSelection,
clearSelection,
imgList,
});
const {
error,
galleryError,
galleryProps,
inputError,
selectBtnProps,
} = imgList;
return {
error,
galleryError,
inputError,
fileInput,
galleryProps,
searchSortProps,

View File

@@ -54,7 +54,8 @@ jest.mock('../../../../data/redux', () => ({
const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks);
let hook;
const testValue = 'testVALUE';
const testValue = 'testVALUEVALIDIMAGE';
const testValueInvalidImage = { value: 'testVALUEVALIDIMAGE', size: 90000000 };
describe('SelectImageModal hooks', () => {
beforeEach(() => {
@@ -66,6 +67,7 @@ describe('SelectImageModal hooks', () => {
state.testGetter(state.keys.showSelectImageError);
state.testGetter(state.keys.searchString);
state.testGetter(state.keys.sortBy);
state.testGetter(state.keys.showSizeError);
});
describe('using state', () => {
@@ -122,6 +124,7 @@ describe('SelectImageModal hooks', () => {
images: { p1: 'data1', p2: 'data2', p3: 'other distinct data' },
sortBy: sortKeys.dateNewest,
searchString: 'test search string',
};
const load = (loadProps = {}) => {
jest.spyOn(hooks, hookKeys.filteredList).mockImplementationOnce(
@@ -210,20 +213,20 @@ describe('SelectImageModal hooks', () => {
}));
});
});
describe('error', () => {
describe('galleryError', () => {
test('show is initialized to false and returns properly', () => {
const show = 'sHOWSelectiMaGEeRROr';
expect(hook.error.show).toEqual(false);
expect(hook.galleryError.show).toEqual(false);
state.mockVal(state.keys.showSelectImageError, show);
hook = hooks.imgListHooks(props);
expect(hook.error.show).toEqual(show);
expect(hook.galleryError.show).toEqual(show);
});
test('set sets showSelectImageError to true', () => {
hook.error.set();
hook.galleryError.set();
expect(state.setState.showSelectImageError).toHaveBeenCalledWith(true);
});
test('dismiss sets showSelectImageError to false', () => {
hook.error.dismiss();
hook.galleryError.dismiss();
expect(state.setState.showSelectImageError).toHaveBeenCalledWith(false);
});
// TODO
@@ -238,10 +241,29 @@ describe('SelectImageModal hooks', () => {
});
});
});
describe('checkValidFileSize', () => {
const selectedFileFail = testValueInvalidImage;
const selectedFileSuccess = { value: testValue, size: 2000 };
const clearSelection = jest.fn();
const onSizeFail = jest.fn();
it('returns false for valid file size ', () => {
hook = hooks.checkValidFileSize({ selectedFile: selectedFileFail, clearSelection, onSizeFail });
expect(clearSelection).toHaveBeenCalled();
expect(onSizeFail).toHaveBeenCalled();
expect(hook).toEqual(false);
});
it('returns true for valid file size', () => {
hook = hooks.checkValidFileSize({ selectedFile: selectedFileSuccess, clearSelection, onSizeFail });
expect(hook).toEqual(true);
});
});
describe('fileInputHooks', () => {
const setSelection = jest.fn();
const clearSelection = jest.fn();
const imgList = { inputError: { show: true, dismiss: jest.fn(), set: jest.fn() } };
const spies = {};
beforeEach(() => {
hook = hooks.fileInputHooks({ setSelection });
hook = hooks.fileInputHooks({ setSelection, clearSelection, imgList });
});
it('returns a ref for the file input', () => {
expect(hook.ref).toEqual({ current: undefined });
@@ -254,9 +276,23 @@ describe('SelectImageModal hooks', () => {
expect(click).toHaveBeenCalled();
});
describe('addFile (uploadImage args)', () => {
const event = { target: { files: [testValue] } };
const eventSuccess = { target: { files: [{ value: testValue, size: 2000 }] } };
const eventFailure = { target: { files: [testValueInvalidImage] } };
it('image fails to upload if file size is greater than 1000000', () => {
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 uploadImage thunkAction with the first target file and setSelection', () => {
hook.addFile(event);
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.app.uploadImage({
file: testValue,
setSelection,
@@ -273,6 +309,7 @@ describe('SelectImageModal hooks', () => {
const fileInputHooks = { file: 'input hooks' };
const setSelection = jest.fn();
const clearSelection = jest.fn();
const spies = {};
beforeEach(() => {
spies.imgList = jest.spyOn(hooks, hookKeys.imgListHooks)
@@ -281,13 +318,13 @@ describe('SelectImageModal hooks', () => {
.mockReturnValueOnce(searchAndSortHooks);
spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks)
.mockReturnValueOnce(fileInputHooks);
hook = hooks.imgHooks({ setSelection });
hook = hooks.imgHooks({ setSelection, clearSelection });
});
it('forwards fileInputHooks as fileInput, called with uploadImage prop', () => {
expect(hook.fileInput).toEqual(fileInputHooks);
expect(spies.file.mock.calls.length).toEqual(1);
expect(spies.file).toHaveBeenCalledWith({
setSelection,
setSelection, clearSelection, imgList: imgListHooks,
});
});
it('initializes imgListHooks with setSelection and searchAndSortHooks', () => {
@@ -300,7 +337,9 @@ describe('SelectImageModal hooks', () => {
it('forwards searchAndSortHooks as searchSortProps', () => {
expect(hook.searchSortProps).toEqual(searchAndSortHooks);
expect(spies.file.mock.calls.length).toEqual(1);
expect(spies.file).toHaveBeenCalledWith({ setSelection });
expect(spies.file).toHaveBeenCalledWith({
setSelection, clearSelection, imgList: imgListHooks,
});
});
it('forwards galleryProps and selectBtnProps from the image list hooks', () => {
expect(hook.galleryProps).toEqual(imgListHooks.galleryProps);

View File

@@ -1,9 +1,16 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Button, Stack } from '@edx/paragon';
import { Button, Stack, Spinner } from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { selectors } from '../../../../data/redux';
import { RequestKeys } from '../../../../data/constants/requests';
import hooks from './hooks';
import messages from './messages';
@@ -19,16 +26,21 @@ export const SelectImageModal = ({
isOpen,
close,
setSelection,
clearSelection,
// injected
intl,
// redux
inputIsLoading,
}) => {
const {
error,
galleryError,
inputError,
fileInput,
galleryProps,
searchSortProps,
selectBtnProps,
} = hooks.imgHooks({ setSelection });
} = hooks.imgHooks({ setSelection, clearSelection });
return (
<BaseModal
close={close}
@@ -45,27 +57,37 @@ export const SelectImageModal = ({
)}
title={intl.formatMessage(messages.titleLabel)}
>
{/* Error Alerts */}
<FetchErrorAlert />
<UploadErrorAlert />
<ErrorAlert
dismissError={inputError.dismiss}
hideHeading
isError={inputError.show}
>
<FormattedMessage {...messages.fileSizeError} />
</ErrorAlert>
{/* User Feedback Alerts */}
<ErrorAlert
dismissError={error.dismiss}
dismissError={galleryError.dismiss}
hideHeading
isError={error.show}
isError={galleryError.show}
>
<FormattedMessage {...messages.selectImageError} />
</ErrorAlert>
<Stack gap={3}>
<SearchSort {...searchSortProps} />
<Gallery {...galleryProps} />
{!inputIsLoading ? <Gallery {...galleryProps} /> : (
<Spinner
animation="border"
className="mie-3"
screenReaderText={intl.formatMessage(messages.loading)}
/>
)}
<FileInput fileInput={fileInput} />
</Stack>
</BaseModal>
);
};
@@ -73,8 +95,17 @@ SelectImageModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
setSelection: PropTypes.func.isRequired,
clearSelection: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
// redux
inputIsLoading: PropTypes.bool.isRequired,
};
export default injectIntl(SelectImageModal);
export const mapStateToProps = (state) => ({
inputIsLoading: selectors.requests.isPending(state, { requestKey: RequestKeys.uploadImage }),
});
export const mapDispatchToProps = {};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SelectImageModal));

View File

@@ -2,12 +2,14 @@ import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../testUtils';
import { RequestKeys } from '../../../../data/constants/requests';
import { selectors } from '../../../../data/redux';
import BaseModal from '../BaseModal';
import FileInput from './FileInput';
import Gallery from './Gallery';
import SearchSort from './SearchSort';
import hooks from './hooks';
import { SelectImageModal } from '.';
import { SelectImageModal, mapStateToProps, mapDispatchToProps } from '.';
jest.mock('../BaseModal', () => 'BaseModal');
jest.mock('./FileInput', () => 'FileInput');
@@ -15,11 +17,17 @@ jest.mock('./Gallery', () => 'Gallery');
jest.mock('./SearchSort', () => 'SearchSort');
jest.mock('../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert');
jest.mock('../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert');
jest.mock('../ErrorAlerts/ErrorAlert', () => 'ErrorAlert');
jest.mock('./hooks', () => ({
imgHooks: jest.fn(() => ({
error: {
show: 'ShoWERror',
galleryError: {
show: 'ShoWERror gAlLery',
set: jest.fn(),
dismiss: jest.fn(),
},
inputError: {
show: 'ShoWERror inPUT',
set: jest.fn(),
dismiss: jest.fn(),
},
@@ -34,13 +42,23 @@ jest.mock('./hooks', () => ({
})),
}));
jest.mock('../../../../data/redux', () => ({
selectors: {
requests: {
isPending: (state, { requestKey }) => ({ isPending: { state, requestKey } }),
},
},
}));
describe('SelectImageModal', () => {
describe('component', () => {
const props = {
isOpen: true,
close: jest.fn().mockName('props.close'),
setSelection: jest.fn().mockName('props.setSelection'),
clearSelection: jest.fn().mockName('props.clearSelection'),
intl: { formatMessage },
inputIsLoading: false,
};
let el;
const imgHooks = hooks.imgHooks();
@@ -50,6 +68,11 @@ describe('SelectImageModal', () => {
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
test('snapshot: uploaded image not loaded, show spinner', () => {
props.inputIsLoading = true;
expect(shallow(<SelectImageModal {...props} />)).toMatchSnapshot();
props.inputIsLoading = false;
});
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
expect(el.find(BaseModal).props().confirmAction.props).toEqual(
expect.objectContaining({ ...hooks.imgHooks().selectBtnProps, variant: 'primary' }),
@@ -70,4 +93,17 @@ describe('SelectImageModal', () => {
expect(el.find(FileInput).props()).toMatchObject({ fileInput: imgHooks.fileInput });
});
});
describe('mapStateToProps', () => {
const testState = { some: 'testState' };
test('loads inputIsLoading from requests.isPending selector for uploadImage request', () => {
expect(mapStateToProps(testState).inputIsLoading).toEqual(
selectors.requests.isPending(testState, { requestKey: RequestKeys.uploadImage }),
);
});
});
describe('mapDispatchToProps', () => {
test('is empty', () => {
expect(mapDispatchToProps).toEqual({});
});
});
});

View File

@@ -6,7 +6,7 @@ export const messages = {
},
uploadButtonLabel: {
id: 'authoring.texteditor.selectimagemodal.upload.label',
defaultMessage: 'Upload a new image',
defaultMessage: 'Upload a new image (10 MB max)',
description: 'Label for upload button',
},
titleLabel: {
@@ -55,7 +55,8 @@ export const messages = {
},
emptyGalleryLabel: {
id: 'authoring.texteditor.selectimagemodal.emptyGalleryLabel',
defaultMessage: 'No images found in your gallery. Please upload an image using the button below.',
defaultMessage:
'No images found in your gallery. Please upload an image using the button below.',
description: 'Label for when image gallery is empty.',
},
emptySearchLabel: {
@@ -72,18 +73,26 @@ export const messages = {
},
uploadImageError: {
id: 'authoring.texteditor.selectimagemodal.error.uploadImageError',
defaultMessage: 'Failed to Upload Image. Please Try again.',
defaultMessage: 'Failed to upload image. Please try again.',
description: 'Message presented to user when image fails to upload',
},
fetchImagesError: {
id: 'authoring.texteditor.selectimagemodal.error.fetchImagesError',
defaultMessage: 'Failed to obtain course Images. Please Try again.',
defaultMessage: 'Failed to obtain course images. Please try again.',
description: 'Message presented to user when images are not found',
},
fileSizeError: {
id: 'authoring.texteditor.selectimagemodal.error.fileSizeError',
defaultMessage:
'Images must be 10 MB or less. Please resize image and try again.',
description:
' Message presented to user when file size of image is larger than 10 MB',
},
selectImageError: {
id: 'authoring.texteditor.selectimagemodal.error.selectImageError',
defaultMessage: 'Select an image to continue.',
description: 'Message presented to user when clicking Next without selecting an image',
description:
'Message presented to user when clicking Next without selecting an image',
},
};

View File

@@ -2,6 +2,7 @@
exports[`ImageUploadModal component snapshot: no selection (Select Image Modal) 1`] = `
<SelectImageModal
clearSelection={[MockFunction props.clearSelection]}
close={[MockFunction props.close]}
isOpen={false}
setSelection={[MockFunction props.setSelection]}

View File

@@ -24,13 +24,19 @@ export const networkRequest = ({
onFailure,
}) => (dispatch) => {
dispatch(actions.requests.startRequest(requestKey));
return promise.then((response) => {
if (onSuccess) { onSuccess(response); }
dispatch(actions.requests.completeRequest({ requestKey, response }));
}).catch((error) => {
if (onFailure) { onFailure(error); }
dispatch(actions.requests.failRequest({ requestKey, error }));
});
return promise
.then((response) => {
if (onSuccess) {
onSuccess(response);
}
dispatch(actions.requests.completeRequest({ requestKey, response }));
})
.catch((error) => {
if (onFailure) {
onFailure(error);
}
dispatch(actions.requests.failRequest({ requestKey, error }));
});
};
/**
@@ -119,10 +125,12 @@ export const uploadImage = ({ image, ...rest }) => (dispatch, getState) => {
export const fetchImages = ({ ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.fetchImages,
promise: api.fetchImages({
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
learningContextId: selectors.app.learningContextId(getState()),
}).then((response) => loadImages(response.data.assets)),
promise: api
.fetchImages({
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
learningContextId: selectors.app.learningContextId(getState()),
})
.then((response) => loadImages(response.data.assets)),
...rest,
}));
};