Photo upload error handling (#57)
* Parse error response for photo upload * Show profile photo upload errors * Update test to match new shape of error * Clearer comments
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Container, Row, Col } from 'reactstrap';
|
||||
import { Container, Row, Col, Alert } from 'reactstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { logEvent } from '../analytics/analytics';
|
||||
|
||||
@@ -77,7 +77,7 @@ export class ProfilePage extends React.Component {
|
||||
profileImage,
|
||||
username,
|
||||
dateJoined,
|
||||
errors,
|
||||
photoUploadError,
|
||||
name,
|
||||
visibilityName,
|
||||
country,
|
||||
@@ -99,7 +99,6 @@ export class ProfilePage extends React.Component {
|
||||
closeHandler: this.handleClose,
|
||||
submitHandler: this.handleSubmit,
|
||||
changeHandler: this.handleChange,
|
||||
errors,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -119,6 +118,7 @@ export class ProfilePage extends React.Component {
|
||||
isEditable={this.props.isCurrentUserProfile}
|
||||
/>
|
||||
<div>
|
||||
{photoUploadError !== null ? <Alert color="danger">{photoUploadError.userMessage}</Alert> : null}
|
||||
<h2 className="mb-0">{username}</h2>
|
||||
<DateJoined date={dateJoined} />
|
||||
</div>
|
||||
@@ -240,7 +240,7 @@ ProfilePage.propTypes = {
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
|
||||
// Page state helpers
|
||||
errors: PropTypes.objectOf(PropTypes.string),
|
||||
photoUploadError: PropTypes.objectOf(PropTypes.string),
|
||||
|
||||
// Actions
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
@@ -262,7 +262,7 @@ ProfilePage.propTypes = {
|
||||
ProfilePage.defaultProps = {
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
photoUploadError: {},
|
||||
profileImage: {},
|
||||
name: null,
|
||||
username: null,
|
||||
|
||||
@@ -76,7 +76,7 @@ const profilePage = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
errors: Object.assign({}, state.errors, action.payload.errors),
|
||||
errors: Object.assign({}, state.errors, { photo: action.payload.error }),
|
||||
};
|
||||
case SAVE_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
|
||||
@@ -122,9 +122,12 @@ export function* handleSaveProfile(action) {
|
||||
yield put(saveProfileReset());
|
||||
yield put(resetDrafts());
|
||||
} catch (e) {
|
||||
// TODO: If this is any other kind of exception than a known validation error from the server,
|
||||
// this code will fail gracelessly when it can't find fieldErrors on the error.
|
||||
yield put(saveProfileFailure(e.fieldErrors));
|
||||
if (e.processedData.fieldErrors) {
|
||||
yield put(saveProfileFailure(e.processedData.fieldErrors));
|
||||
} else {
|
||||
// TODO: Currently failing silently on other kinds of errors
|
||||
yield put(saveProfileReset());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +144,12 @@ export function* handleSaveProfilePhoto(action) {
|
||||
yield put(saveProfilePhotoSuccess());
|
||||
yield put(saveProfilePhotoReset());
|
||||
} catch (e) {
|
||||
yield put(saveProfilePhotoFailure(e.message));
|
||||
if (e.processedData) {
|
||||
yield put(saveProfilePhotoFailure(e.processedData));
|
||||
} else {
|
||||
// TODO: Currently failing silently on other kinds of errors
|
||||
yield put(saveProfilePhotoReset());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,8 +74,10 @@ describe('RootSaga', () => {
|
||||
|
||||
it('should successfully publish a failure action on exception', () => {
|
||||
const error = new Error('uhoh');
|
||||
error.fieldErrors = {
|
||||
uhoh: 'not good',
|
||||
error.processedData = {
|
||||
fieldErrors: {
|
||||
uhoh: 'not good',
|
||||
},
|
||||
};
|
||||
const action = profileActions.saveProfile(
|
||||
'my username',
|
||||
|
||||
@@ -67,7 +67,7 @@ export const visibilityDraftsFieldSelector = createSelector(
|
||||
export const formErrorSelector = createSelector(
|
||||
accountErrorsSelector,
|
||||
formIdSelector,
|
||||
(errors, formId) => errors[formId] || null,
|
||||
(errors, formId) => (errors[formId] ? errors[formId].userMessage : null),
|
||||
);
|
||||
|
||||
export const editableFormSelector = createSelector(
|
||||
@@ -264,6 +264,7 @@ export const profilePageSelector = createSelector(
|
||||
savePhotoStateSelector,
|
||||
isCurrentUserProfileSelector,
|
||||
draftSocialLinksByPlatformSelector,
|
||||
accountErrorsSelector,
|
||||
(
|
||||
account,
|
||||
formValues,
|
||||
@@ -272,6 +273,7 @@ export const profilePageSelector = createSelector(
|
||||
savePhotoState,
|
||||
isCurrentUserProfile,
|
||||
draftSocialLinksByPlatform,
|
||||
errors,
|
||||
) => ({
|
||||
// Account data we need
|
||||
username: account.username,
|
||||
@@ -312,5 +314,6 @@ export const profilePageSelector = createSelector(
|
||||
saveState,
|
||||
savePhotoState,
|
||||
isCurrentUserProfile,
|
||||
photoUploadError: errors.photo || null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -39,12 +39,7 @@ export async function patchProfile(username, params) {
|
||||
},
|
||||
).catch((error) => {
|
||||
const processedError = Object.create(error);
|
||||
const fieldErrors = Object.entries(processAccountData(error.response.data.field_errors))
|
||||
.reduce((acc, [fieldKey, messages]) => {
|
||||
acc[fieldKey] = messages.userMessage;
|
||||
return acc;
|
||||
}, {});
|
||||
processedError.fieldErrors = fieldErrors;
|
||||
processedError.processedData = processAccountData(error.response.data);
|
||||
throw processedError;
|
||||
});
|
||||
|
||||
@@ -63,7 +58,11 @@ export async function postProfilePhoto(username, formData) {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
);
|
||||
).catch((error) => {
|
||||
const processedError = Object.create(error);
|
||||
processedError.processedData = camelCaseObject(error.response.data);
|
||||
throw processedError;
|
||||
});
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user