Add saving of profile photo (#19)
* Add saving of profile photo * Add removal of profile photo Needs work on knowing when a default photo has been supplied. * Add action creator tests * Fix some reference issues after merge * Fix broken test
This commit is contained in:
16
src/App.scss
16
src/App.scss
@@ -21,20 +21,28 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
}
|
||||
|
||||
|
||||
.profile-avatar-wrap {
|
||||
margin-right: 1rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
max-width: 12rem;
|
||||
margin-right: 0;
|
||||
margin-top: -8rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
max-width: 50%;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 12rem;
|
||||
max-width: none;
|
||||
height: 12rem;
|
||||
margin-right: 0;
|
||||
margin-top: -8rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.profile-avatar-edit-button {
|
||||
|
||||
@@ -3,6 +3,8 @@ import AsyncActionType from './AsyncActionType';
|
||||
export const EDITABLE_FIELD_OPEN = 'EDITABLE_FIELD_OPEN';
|
||||
export const EDITABLE_FIELD_CLOSE = 'EDITABLE_FIELD_CLOSE';
|
||||
export const SAVE_USER_PROFILE = new AsyncActionType('PROFILE', 'SAVE_USER_PROFILE');
|
||||
export const SAVE_USER_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_USER_PROFILE_PHOTO');
|
||||
export const DELETE_USER_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_USER_PROFILE_PHOTO');
|
||||
|
||||
export const openEditableField = fieldName => ({
|
||||
type: EDITABLE_FIELD_OPEN,
|
||||
@@ -40,3 +42,51 @@ export const saveUserProfile = (username, userAccountState, fieldName) => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const saveUserProfilePhotoBegin = () => ({
|
||||
type: SAVE_USER_PROFILE_PHOTO.BEGIN,
|
||||
});
|
||||
|
||||
export const saveUserProfilePhotoSuccess = () => ({
|
||||
type: SAVE_USER_PROFILE_PHOTO.SUCCESS,
|
||||
});
|
||||
|
||||
export const saveUserProfilePhotoReset = () => ({
|
||||
type: SAVE_USER_PROFILE_PHOTO.RESET,
|
||||
});
|
||||
|
||||
export const saveUserProfilePhotoFailure = error => ({
|
||||
type: SAVE_USER_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const saveUserProfilePhoto = (username, formData) => ({
|
||||
type: SAVE_USER_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username,
|
||||
formData,
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteUserProfilePhotoBegin = () => ({
|
||||
type: DELETE_USER_PROFILE_PHOTO.BEGIN,
|
||||
});
|
||||
|
||||
export const deleteUserProfilePhotoSuccess = () => ({
|
||||
type: DELETE_USER_PROFILE_PHOTO.SUCCESS,
|
||||
});
|
||||
|
||||
export const deleteUserProfilePhotoReset = () => ({
|
||||
type: DELETE_USER_PROFILE_PHOTO.RESET,
|
||||
});
|
||||
|
||||
export const deleteUserProfilePhotoFailure = error => ({
|
||||
type: DELETE_USER_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const deleteUserProfilePhoto = username => ({
|
||||
type: DELETE_USER_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,6 +9,18 @@ import {
|
||||
saveUserProfileFailure,
|
||||
saveUserProfileReset,
|
||||
saveUserProfile,
|
||||
SAVE_USER_PROFILE_PHOTO,
|
||||
saveUserProfilePhotoBegin,
|
||||
saveUserProfilePhotoSuccess,
|
||||
saveUserProfilePhotoFailure,
|
||||
saveUserProfilePhotoReset,
|
||||
saveUserProfilePhoto,
|
||||
DELETE_USER_PROFILE_PHOTO,
|
||||
deleteUserProfilePhotoBegin,
|
||||
deleteUserProfilePhotoSuccess,
|
||||
deleteUserProfilePhotoFailure,
|
||||
deleteUserProfilePhotoReset,
|
||||
deleteUserProfilePhoto,
|
||||
} from './profile';
|
||||
|
||||
describe('editable field actions', () => {
|
||||
@@ -81,3 +93,112 @@ describe('SAVE profile actions', () => {
|
||||
expect(saveUserProfileFailure(error)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('SAVE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo save', () => {
|
||||
const formData = 'multipart form data';
|
||||
const expectedAction = {
|
||||
type: SAVE_USER_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username: 'myusername',
|
||||
formData,
|
||||
},
|
||||
};
|
||||
expect(saveUserProfilePhoto('myusername', formData)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save beginning', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_USER_PROFILE_PHOTO.BEGIN,
|
||||
};
|
||||
expect(saveUserProfilePhotoBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save success', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_USER_PROFILE_PHOTO.SUCCESS,
|
||||
};
|
||||
expect(saveUserProfilePhotoSuccess()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save success', () => {
|
||||
const expectedAction = {
|
||||
type: SAVE_USER_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(saveUserProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo save failure', () => {
|
||||
const error = 'Test failure';
|
||||
const expectedAction = {
|
||||
type: SAVE_USER_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
};
|
||||
expect(saveUserProfilePhotoFailure(error)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('DELETE profile photo actions', () => {
|
||||
it('should create an action to signal the start of a profile photo deletion', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_USER_PROFILE_PHOTO.BASE,
|
||||
payload: {
|
||||
username: 'myusername',
|
||||
},
|
||||
};
|
||||
expect(deleteUserProfilePhoto('myusername')).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion beginning', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_USER_PROFILE_PHOTO.BEGIN,
|
||||
};
|
||||
expect(deleteUserProfilePhotoBegin()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion success', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_USER_PROFILE_PHOTO.SUCCESS,
|
||||
};
|
||||
expect(deleteUserProfilePhotoSuccess()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion success', () => {
|
||||
const expectedAction = {
|
||||
type: DELETE_USER_PROFILE_PHOTO.RESET,
|
||||
};
|
||||
expect(deleteUserProfilePhotoReset()).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal user profile photo deletion failure', () => {
|
||||
const error = 'Test failure';
|
||||
const expectedAction = {
|
||||
type: DELETE_USER_PROFILE_PHOTO.FAILURE,
|
||||
payload: { error },
|
||||
};
|
||||
expect(deleteUserProfilePhotoFailure(error)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Editable field opening and closing actions', () => {
|
||||
const fieldName = 'fullName';
|
||||
|
||||
it('should create an action to signal the opening a field', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_OPEN,
|
||||
fieldName,
|
||||
};
|
||||
expect(openEditableField(fieldName)).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to signal the closing a field', () => {
|
||||
const expectedAction = {
|
||||
type: EDITABLE_FIELD_CLOSE,
|
||||
fieldName,
|
||||
};
|
||||
expect(closeEditableField(fieldName)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Input } from 'reactstrap';
|
||||
import { Input, Spinner } from 'reactstrap';
|
||||
|
||||
class ProfileAvatar extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -10,8 +10,8 @@ class ProfileAvatar extends React.Component {
|
||||
this.form = React.createRef();
|
||||
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onClickDelete = this.onClickDelete.bind(this);
|
||||
this.onInput = this.onInput.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
@@ -19,17 +19,17 @@ class ProfileAvatar extends React.Component {
|
||||
this.fileInput.current.click();
|
||||
}
|
||||
|
||||
onInput(e) { // eslint-disable-line no-unused-vars
|
||||
// console.log('input', e)
|
||||
this.form.current.submit();
|
||||
onClickDelete() {
|
||||
this.props.onDelete();
|
||||
}
|
||||
|
||||
onChange(e) { // eslint-disable-line no-unused-vars
|
||||
// console.log('change', e)
|
||||
onInput() {
|
||||
this.onSubmit();
|
||||
}
|
||||
|
||||
onSubmit(e) { // eslint-disable-line no-unused-vars
|
||||
// console.log('onsubmit', e);
|
||||
onSubmit(e) {
|
||||
if (e) e.preventDefault();
|
||||
this.props.onSave(new FormData(this.form.current));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -38,31 +38,52 @@ class ProfileAvatar extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="profile-avatar rounded-circle overflow-hidden">
|
||||
<button
|
||||
className="text-white profile-avatar-edit-button"
|
||||
onClick={this.onClick}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<img className="w-100" src={src} alt="profile avatar" />
|
||||
<form
|
||||
ref={this.form}
|
||||
onSubmit={this.onSubmit}
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
<Input
|
||||
className="d-none"
|
||||
innerRef={this.fileInput}
|
||||
type="file"
|
||||
name="file"
|
||||
id="exampleFile"
|
||||
onInput={this.onInput}
|
||||
onChange={this.onChange}
|
||||
accept=".jpg, .jpeg, .png"
|
||||
/>
|
||||
</form>
|
||||
<div className="profile-avatar-wrap position-relative">
|
||||
<div className="profile-avatar rounded-circle overflow-hidden bg-dark">
|
||||
{this.props.savePhotoState === 'pending' ? (
|
||||
<div
|
||||
className="p-absolute w-100 h-100 d-flex justify-content-center align-items-center"
|
||||
style={{ backgroundColor: 'rgba(255,255,255,.5)' }}
|
||||
>
|
||||
<Spinner color="primary" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="text-white profile-avatar-edit-button"
|
||||
onClick={this.onClick}
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
|
||||
<form
|
||||
ref={this.form}
|
||||
onSubmit={this.onSubmit}
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
<img className="w-100" src={src} alt="profile avatar" />
|
||||
|
||||
{/* The name of this input must be 'file' */}
|
||||
<Input
|
||||
className="d-none"
|
||||
innerRef={this.fileInput}
|
||||
type="file"
|
||||
name="file"
|
||||
id="exampleFile"
|
||||
onInput={this.onInput}
|
||||
accept=".jpg, .jpeg, .png"
|
||||
/>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{src ? (
|
||||
<button
|
||||
className="position-absolute btn btn-link w-100 btn-sm"
|
||||
onClick={this.onClickDelete}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -73,8 +94,12 @@ export default ProfileAvatar;
|
||||
|
||||
ProfileAvatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
};
|
||||
|
||||
ProfileAvatar.defaultProps = {
|
||||
src: null,
|
||||
savePhotoState: null,
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ class UserProfile extends React.Component {
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
this.onEdit = this.onEdit.bind(this);
|
||||
this.onSave = this.onSave.bind(this);
|
||||
this.onSaveProfilePhoto = this.onSaveProfilePhoto.bind(this);
|
||||
this.onDeleteProfilePhoto = this.onDeleteProfilePhoto.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onVisibilityChange = this.onVisibilityChange.bind(this);
|
||||
}
|
||||
@@ -46,6 +48,14 @@ class UserProfile extends React.Component {
|
||||
this.props.saveUserProfile(this.props.username, userAccountData, fieldName);
|
||||
}
|
||||
|
||||
onSaveProfilePhoto(formData) {
|
||||
this.props.saveUserProfilePhoto(this.props.username, formData);
|
||||
}
|
||||
|
||||
onDeleteProfilePhoto() {
|
||||
this.props.deleteUserProfilePhoto(this.props.username);
|
||||
}
|
||||
|
||||
onChange(fieldName, value) {
|
||||
this.setState({
|
||||
[fieldName]: {
|
||||
@@ -103,7 +113,9 @@ class UserProfile extends React.Component {
|
||||
<ProfileAvatar
|
||||
className="mb-md-3"
|
||||
src={profileImage}
|
||||
{...commonProps}
|
||||
onSave={this.onSaveProfilePhoto}
|
||||
onDelete={this.onDeleteProfilePhoto}
|
||||
savePhotoState={this.props.savePhotoState}
|
||||
/>
|
||||
<div>
|
||||
<h2 className="mb-0">{username}</h2>
|
||||
@@ -167,6 +179,7 @@ export default UserProfile;
|
||||
UserProfile.propTypes = {
|
||||
currentlyEditingField: PropTypes.string,
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
profileImage: PropTypes.string,
|
||||
fullName: PropTypes.string,
|
||||
@@ -183,6 +196,8 @@ UserProfile.propTypes = {
|
||||
title: PropTypes.string,
|
||||
})),
|
||||
saveUserProfile: PropTypes.func,
|
||||
saveUserProfilePhoto: PropTypes.func.isRequired,
|
||||
deleteUserProfilePhoto: PropTypes.func.isRequired,
|
||||
openEditableField: PropTypes.func.isRequired,
|
||||
closeEditableField: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -190,6 +205,7 @@ UserProfile.propTypes = {
|
||||
UserProfile.defaultProps = {
|
||||
currentlyEditingField: null,
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
error: null,
|
||||
profileImage: null,
|
||||
fullName: null,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { connect } from 'react-redux';
|
||||
import UserProfile from '../../components/UserProfile';
|
||||
import {
|
||||
saveUserProfile,
|
||||
saveUserProfilePhoto,
|
||||
deleteUserProfilePhoto,
|
||||
openEditableField,
|
||||
closeEditableField,
|
||||
} from '../../actions/profile';
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
const mapStateToProps = state => ({
|
||||
currentlyEditingField: state.profile.currentlyEditingField,
|
||||
saveState: state.profile.saveState,
|
||||
savePhotoState: state.profile.savePhotoState,
|
||||
error: state.profile.error,
|
||||
profileImage: state.userAccount.profileImage.imageUrlLarge,
|
||||
fullName: state.userAccount.name,
|
||||
@@ -23,6 +26,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
saveUserProfile,
|
||||
saveUserProfilePhoto,
|
||||
deleteUserProfilePhoto,
|
||||
openEditableField,
|
||||
closeEditableField,
|
||||
})(UserProfile);
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { SAVE_USER_PROFILE, EDITABLE_FIELD_CLOSE, EDITABLE_FIELD_OPEN } from '../../actions/profile';
|
||||
import {
|
||||
SAVE_USER_PROFILE,
|
||||
SAVE_USER_PROFILE_PHOTO,
|
||||
DELETE_USER_PROFILE_PHOTO,
|
||||
EDITABLE_FIELD_CLOSE,
|
||||
EDITABLE_FIELD_OPEN,
|
||||
} from '../../actions/profile';
|
||||
|
||||
|
||||
const initialState = {
|
||||
error: null,
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
currentlyEditingField: null,
|
||||
};
|
||||
|
||||
|
||||
const profile = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case EDITABLE_FIELD_OPEN:
|
||||
@@ -22,6 +31,7 @@ const profile = (state = initialState, action) => {
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
||||
case SAVE_USER_PROFILE.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
@@ -46,6 +56,57 @@ const profile = (state = initialState, action) => {
|
||||
saveState: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
case SAVE_USER_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'pending',
|
||||
error: null,
|
||||
};
|
||||
case SAVE_USER_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'complete',
|
||||
error: null,
|
||||
};
|
||||
case SAVE_USER_PROFILE_PHOTO.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
error: action.payload.error,
|
||||
};
|
||||
case SAVE_USER_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
case DELETE_USER_PROFILE_PHOTO.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'pending',
|
||||
error: null,
|
||||
};
|
||||
case DELETE_USER_PROFILE_PHOTO.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'complete',
|
||||
error: null,
|
||||
};
|
||||
case DELETE_USER_PROFILE_PHOTO.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: 'error',
|
||||
error: action.payload.error,
|
||||
};
|
||||
case DELETE_USER_PROFILE_PHOTO.RESET:
|
||||
return {
|
||||
...state,
|
||||
savePhotoState: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,16 @@ import {
|
||||
saveUserProfileFailure,
|
||||
saveUserProfileReset,
|
||||
closeEditableField,
|
||||
SAVE_USER_PROFILE_PHOTO,
|
||||
saveUserProfilePhotoBegin,
|
||||
saveUserProfilePhotoSuccess,
|
||||
saveUserProfilePhotoFailure,
|
||||
saveUserProfilePhotoReset,
|
||||
DELETE_USER_PROFILE_PHOTO,
|
||||
deleteUserProfilePhotoBegin,
|
||||
deleteUserProfilePhotoSuccess,
|
||||
deleteUserProfilePhotoFailure,
|
||||
deleteUserProfilePhotoReset,
|
||||
} from '../actions/profile';
|
||||
|
||||
let userAccountApiService = null;
|
||||
@@ -31,14 +41,18 @@ export const mapDataForRequest = (props) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
export function* saveUserProfile(action) {
|
||||
const { username, userAccountState } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(saveUserProfileBegin());
|
||||
|
||||
// Use call([context, fnName], ...args) for proper context on apiService
|
||||
const userAccount = yield call(
|
||||
// Because apiService is a class, 'this' needs to be bound.
|
||||
userAccountApiService.saveUserAccount,
|
||||
action.payload.username,
|
||||
mapDataForRequest(action.payload.userAccountState),
|
||||
[userAccountApiService, 'saveUserAccount'],
|
||||
username,
|
||||
mapDataForRequest(userAccountState),
|
||||
);
|
||||
|
||||
// Tells the profile form that
|
||||
@@ -58,7 +72,67 @@ export function* saveUserProfile(action) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* saveUserProfilePhoto(action) {
|
||||
const { username, formData } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(saveUserProfilePhotoBegin());
|
||||
|
||||
// Use call([context, fnName], ...args) for proper context on apiService
|
||||
yield call([userAccountApiService, 'saveUserProfilePhoto'], username, formData);
|
||||
|
||||
// Get the account data. Saving doesn't return anything on success.
|
||||
const userAccount = yield call(
|
||||
[userAccountApiService, 'getUserAccount'],
|
||||
username,
|
||||
);
|
||||
|
||||
yield put(saveUserProfilePhotoSuccess());
|
||||
// TODO: export the fetchUserAccountSuccess action from frontend-auth so we can
|
||||
// dry this up.
|
||||
yield put({
|
||||
type: 'FETCH_USER_ACCOUNT_SUCCESS',
|
||||
payload: { userAccount },
|
||||
});
|
||||
yield put(saveUserProfilePhotoReset());
|
||||
} catch (e) {
|
||||
yield put(saveUserProfilePhotoFailure(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* deleteUserProfilePhoto(action) {
|
||||
const { username } = action.payload;
|
||||
|
||||
try {
|
||||
yield put(deleteUserProfilePhotoBegin());
|
||||
|
||||
// Use call([context, fnName], ...args) for proper context on apiService
|
||||
yield call([userAccountApiService, 'deleteUserProfilePhoto'], username);
|
||||
|
||||
// Get the account data. Saving doesn't return anything on success.
|
||||
const userAccount = yield call(
|
||||
[userAccountApiService, 'getUserAccount'],
|
||||
username,
|
||||
);
|
||||
|
||||
yield put(deleteUserProfilePhotoSuccess());
|
||||
// TODO: export the fetchUserAccountSuccess action from frontend-auth so we can
|
||||
// dry this up.
|
||||
yield put({
|
||||
type: 'FETCH_USER_ACCOUNT_SUCCESS',
|
||||
payload: { userAccount },
|
||||
});
|
||||
yield put(deleteUserProfilePhotoReset());
|
||||
} catch (e) {
|
||||
yield put(deleteUserProfilePhotoFailure(e));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* rootSaga(apiService) {
|
||||
userAccountApiService = apiService;
|
||||
yield takeEvery(SAVE_USER_PROFILE.BASE, saveUserProfile);
|
||||
yield takeEvery(SAVE_USER_PROFILE_PHOTO.BASE, saveUserProfilePhoto);
|
||||
yield takeEvery(DELETE_USER_PROFILE_PHOTO.BASE, deleteUserProfilePhoto);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { takeEvery, put, call, delay } from 'redux-saga/effects';
|
||||
|
||||
import rootSaga, { saveUserProfile, mapDataForRequest } from './RootSaga';
|
||||
import rootSaga, { saveUserProfile, saveUserProfilePhoto, deleteUserProfilePhoto, mapDataForRequest } from './RootSaga';
|
||||
import * as profileActions from '../actions/profile';
|
||||
|
||||
class MockUserAccountApiService {
|
||||
@@ -16,6 +16,12 @@ describe('RootSaga', () => {
|
||||
// There is only one.
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.SAVE_USER_PROFILE.BASE, saveUserProfile));
|
||||
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.SAVE_USER_PROFILE_PHOTO.BASE, saveUserProfilePhoto));
|
||||
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.DELETE_USER_PROFILE_PHOTO.BASE, deleteUserProfilePhoto));
|
||||
// ... and done.
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
@@ -41,7 +47,7 @@ describe('RootSaga', () => {
|
||||
levelOfEducation: 'b',
|
||||
};
|
||||
expect(gen.next().value).toEqual(put(profileActions.saveUserProfileBegin()));
|
||||
expect(gen.next().value).toEqual(call(service.saveUserAccount, 'my username', userAccount));
|
||||
expect(gen.next().value).toEqual(call([service, 'saveUserAccount'], 'my username', userAccount));
|
||||
// The library would supply the result of the above call
|
||||
// as the parameter to the NEXT yield. Here:
|
||||
expect(gen.next(userAccount).value).toEqual(put(profileActions.saveUserProfileSuccess()));
|
||||
|
||||
Reference in New Issue
Block a user