feat: Prevent image uploads larger than 10 MB and add spinner
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user