Feat: Add Request Alerts and upload file (#43)
Fixes to Upload data type, as well as adding in of two error alerts for upload and fetch. Co-authored-by: Ben Warzeski <bwarzeski@edx.org> Co-authored-by: Raymond Zhou <56318341+rayzhou-bit@users.noreply.github.com
This commit is contained in:
23633
package-lock.json
generated
23633
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@
|
||||
"homepage": "https://github.com/edx/frontend-lib-content-components#readme",
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "9.1.1",
|
||||
"@edx/frontend-platform": "1.15.2",
|
||||
"@edx/frontend-platform": "1.15.5",
|
||||
"@edx/paragon": "19.6.0",
|
||||
"@testing-library/dom": "^8.11.1",
|
||||
"@testing-library/react": "12.1.1",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/paragon": ">= 7.0.0 < 20.0.0",
|
||||
"@edx/frontend-platform": "1.14.0",
|
||||
"@edx/frontend-platform": ">1.15.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from '../SelectImageModal/messages';
|
||||
|
||||
export const hooks = {
|
||||
state: {
|
||||
isDismissed: (val) => React.useState(val),
|
||||
},
|
||||
dismissalHooks: ({ isError }) => {
|
||||
const [isDismissed, setIsDismissed] = hooks.state.isDismissed(false);
|
||||
React.useEffect(() => {
|
||||
setIsDismissed(isDismissed && !isError);
|
||||
},
|
||||
[isError]);
|
||||
return {
|
||||
isDismissed,
|
||||
dismissAlert: () => setIsDismissed(true),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorAlert = ({
|
||||
isError,
|
||||
children,
|
||||
}) => {
|
||||
const { isDismissed, dismissAlert } = hooks.dismissalHooks({ isError });
|
||||
if (!isError || isDismissed) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
icon={Info}
|
||||
dismissible
|
||||
onClose={dismissAlert}
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
{...messages.errorTitle}
|
||||
/>
|
||||
</Alert.Heading>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorAlert.propTypes = {
|
||||
isError: PropTypes.bool.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default ErrorAlert;
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import * as module from './ErrorAlert';
|
||||
import { MockUseState } from '../../../../../testUtils';
|
||||
|
||||
const { ErrorAlert } = module;
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn(val => ({ current: val })),
|
||||
useEffect: jest.fn(),
|
||||
useCallback: (cb, prereqs) => ({ cb, prereqs }),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(module.hooks);
|
||||
let hook;
|
||||
const testValue = 'testVALUE';
|
||||
|
||||
describe('ErrorAlert component', () => {
|
||||
describe('Hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.isDismissed);
|
||||
});
|
||||
describe('using state', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
afterEach(() => { state.restore(); });
|
||||
describe('dismissalHooks', () => {
|
||||
beforeEach(() => {
|
||||
hook = module.hooks.dismissalHooks({ isError: testValue });
|
||||
});
|
||||
it('returns isDismissed value, initialized to false', () => {
|
||||
expect(state.stateVals.isDismissed).toEqual(hook.isDismissed);
|
||||
});
|
||||
test('dismissAlert sets isDismissed to true', () => {
|
||||
hook.dismissAlert();
|
||||
expect(state.setState.isDismissed).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('On Render, calls setIsDismissed', () => {
|
||||
expect(React.useEffect.mock.calls.length).toEqual(1);
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs[0]).toEqual(testValue);
|
||||
cb();
|
||||
expect(state.setState.isDismissed).toHaveBeenCalledWith(state.stateVals.isDismissed && !testValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Component', () => {
|
||||
describe('Snapshots', () => {
|
||||
let props;
|
||||
beforeAll(() => {
|
||||
props = {
|
||||
isError: false,
|
||||
};
|
||||
jest.spyOn(module.hooks, 'dismissalHooks').mockImplementation((value) => ({ isError: value }));
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshot: is Null when no error (ErrorAlert)', () => {
|
||||
expect(shallow(<ErrorAlert {...props}> <p> An Error Message </p></ErrorAlert>)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: Loads children and component when error (ErrorAlert)', () => {
|
||||
expect(shallow(<ErrorAlert {...props} isError> <p> An Error Message </p> </ErrorAlert>)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../SelectImageModal/messages';
|
||||
import ErrorAlert from './ErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
|
||||
export const FetchErrorAlert = ({
|
||||
// redux
|
||||
isFetchError,
|
||||
// inject
|
||||
}) => (
|
||||
<ErrorAlert
|
||||
isError={isFetchError}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.fetchImagesError}
|
||||
/>
|
||||
</ErrorAlert>
|
||||
);
|
||||
|
||||
FetchErrorAlert.propTypes = {
|
||||
// redux
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }),
|
||||
});
|
||||
export const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FetchErrorAlert);
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FetchErrorAlert, mapStateToProps } from './FetchErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
|
||||
jest.mock('../../../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FetchErrorAlert', () => {
|
||||
describe('Snapshots', () => {
|
||||
test('snapshot: is ErrorAlert with Message error (ErrorAlert)', () => {
|
||||
expect(shallow(<FetchErrorAlert isFetchError />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('isFetchError from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).isFetchError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.fetchImages }));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../SelectImageModal/messages';
|
||||
import ErrorAlert from './ErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
|
||||
export const UploadErrorAlert = ({
|
||||
// redux
|
||||
isUploadError,
|
||||
// inject
|
||||
}) => (
|
||||
<ErrorAlert
|
||||
isError={isUploadError}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.uploadImageError}
|
||||
/>
|
||||
</ErrorAlert>
|
||||
);
|
||||
|
||||
UploadErrorAlert.propTypes = {
|
||||
// redux
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadImage }),
|
||||
});
|
||||
export const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadErrorAlert);
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UploadErrorAlert, mapStateToProps } from './UploadErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
|
||||
jest.mock('../../../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('UploadErrorAlert', () => {
|
||||
describe('Snapshots', () => {
|
||||
test('snapshot: is ErrorAlert with Message error (ErrorAlert)', () => {
|
||||
expect(shallow(<UploadErrorAlert isUploadError />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('isUploadError from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).isUploadError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadImage }));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ErrorAlert component Component Snapshots snapshot: is Null when no error (ErrorAlert) 1`] = `""`;
|
||||
|
||||
exports[`ErrorAlert component Component Snapshots snapshot: Loads children and component when error (ErrorAlert) 1`] = `
|
||||
<Alert
|
||||
dismissible={true}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Error"
|
||||
description="Title of message presented to user when something goes wrong"
|
||||
id="authoring.texteditor.selectimagemodal.error.errorTitle"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
|
||||
<p>
|
||||
An Error Message
|
||||
</p>
|
||||
|
||||
</Alert>
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FetchErrorAlert Snapshots snapshot: is ErrorAlert with Message error (ErrorAlert) 1`] = `
|
||||
<ErrorAlert
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
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"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UploadErrorAlert Snapshots snapshot: is ErrorAlert with Message error (ErrorAlert) 1`] = `
|
||||
<ErrorAlert
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to Upload Image. Please Try again."
|
||||
description="Message presented to user when image fails to upload"
|
||||
id="authoring.texteditor.selectimagemodal.error.uploadImageError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
`;
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
export const ErrorAlert = ({
|
||||
error,
|
||||
setError,
|
||||
}) => {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
icon={Info}
|
||||
dismissible
|
||||
onClose={() => setError(null)}
|
||||
>
|
||||
<Alert.Heading>
|
||||
Image not uploaded
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
Something went wrong while uploading your image. Please try again.
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorAlert.propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
setError: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ErrorAlert;
|
||||
@@ -30,6 +30,8 @@ exports[`SelectImageModal component snapshot 1`] = `
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<Connect(FetchErrorAlert) />
|
||||
<Connect(UploadErrorAlert) />
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,24 @@ jest.mock('../../../../data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const dispatchFn = jest.fn();
|
||||
return {
|
||||
...jest.requireActual('react-redux'),
|
||||
dispatch: dispatchFn,
|
||||
useDispatch: jest.fn(() => dispatchFn),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../data/redux', () => ({
|
||||
thunkActions: {
|
||||
app: {
|
||||
fetchImages: jest.fn(),
|
||||
uploadImage: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
let hook;
|
||||
|
||||
@@ -11,6 +11,8 @@ import BaseModal from '../BaseModal';
|
||||
import SearchSort from './SearchSort';
|
||||
import Gallery from './Gallery';
|
||||
import FileInput from './FileInput';
|
||||
import FetchErrorAlert from '../ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../ErrorAlerts/UploadErrorAlert';
|
||||
|
||||
export const SelectImageModal = ({
|
||||
isOpen,
|
||||
@@ -25,7 +27,6 @@ export const SelectImageModal = ({
|
||||
selectBtnProps,
|
||||
fileInput,
|
||||
} = hooks.imgHooks({ setSelection });
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
@@ -42,6 +43,8 @@ export const SelectImageModal = ({
|
||||
)}
|
||||
title={intl.formatMessage(messages.titleLabel)}
|
||||
>
|
||||
<FetchErrorAlert />
|
||||
<UploadErrorAlert />
|
||||
<Stack gap={3}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
<Gallery {...galleryProps} />
|
||||
|
||||
@@ -49,6 +49,21 @@ export const messages = {
|
||||
defaultMessage: 'loading...',
|
||||
description: 'Gallery loading spinner screen-reader text',
|
||||
},
|
||||
uploadImageError: {
|
||||
id: 'authoring.texteditor.selectimagemodal.error.uploadImageError',
|
||||
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.',
|
||||
description: 'Message presented to user when images are not found',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'authoring.texteditor.selectimagemodal.error.errorTitle',
|
||||
defaultMessage: 'Error',
|
||||
description: 'Title of message presented to user when something goes wrong',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -18,10 +18,14 @@ export const apiMethods = {
|
||||
courseId,
|
||||
studioEndpointUrl,
|
||||
image,
|
||||
}) => post(
|
||||
urls.courseAssets({ studioEndpointUrl, courseId }),
|
||||
image,
|
||||
),
|
||||
}) => {
|
||||
const data = new FormData();
|
||||
data.append('file', image);
|
||||
return post(
|
||||
urls.courseAssets({ studioEndpointUrl, courseId }),
|
||||
data,
|
||||
);
|
||||
},
|
||||
normalizeContent: ({
|
||||
blockId,
|
||||
blockType,
|
||||
|
||||
@@ -105,7 +105,9 @@ describe('cms api', () => {
|
||||
|
||||
describe('uploadImage', () => {
|
||||
const image = { photo: 'dAta' };
|
||||
it('should call post with urls.block and normalizeContent', () => {
|
||||
it('should call post with urls.courseAssets and imgdata', () => {
|
||||
const mockFormdata = new FormData();
|
||||
mockFormdata.append('file', image);
|
||||
apiMethods.uploadImage({
|
||||
courseId,
|
||||
studioEndpointUrl,
|
||||
@@ -113,7 +115,7 @@ describe('cms api', () => {
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
urls.courseAssets({ studioEndpointUrl, courseId }),
|
||||
image,
|
||||
mockFormdata,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,6 @@ export const saveBlock = ({
|
||||
}),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const uploadImage = ({
|
||||
courseId,
|
||||
studioEndpointUrl,
|
||||
|
||||
@@ -60,6 +60,9 @@ jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
});
|
||||
|
||||
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
Alert: {
|
||||
Heading: 'Alert.Heading',
|
||||
},
|
||||
ActionRow: {
|
||||
Spacer: 'ActionRow.Spacer',
|
||||
},
|
||||
|
||||
9
www/package-lock.json
generated
9
www/package-lock.json
generated
@@ -1250,10 +1250,10 @@
|
||||
"@edx/frontend-lib-content-components": {
|
||||
"version": "file:..",
|
||||
"requires": {
|
||||
"@edx/brand": "npm:@edx/brand-edx@2.0.3",
|
||||
"@reduxjs/toolkit": "^1.7.2",
|
||||
"@tinymce/tinymce-react": "^3.13.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.2",
|
||||
@@ -2420,7 +2420,7 @@
|
||||
"integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA=="
|
||||
},
|
||||
"@edx/brand": {
|
||||
"version": "npm:@edx/brand-openedx@1.1.0",
|
||||
"version": "npm:@edx/brand@1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/brand-openedx/-/brand-openedx-1.1.0.tgz",
|
||||
"integrity": "sha512-ne2ZKF1r0akkt0rEzCAQAk4cTDTI2GiWCpc+T7ldQpw9X57OnUB16dKsFNe40C9uEjL5h3Ps/ZsFM5dm4cIkEQ=="
|
||||
},
|
||||
@@ -26378,6 +26378,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user