Save background image on dashcard automatically

closes CNVS-30697

Test Plan:
- Enable course images feature flag

- Go to the course settings

- Upload an image in the change image modal via
  drag and drop, by browsing through local files,
  and via flickr

- In all cases, on upload the modal should close
  automatically and you should see the preview of
  the image on the course settings page

- Previously, in order for the image to persist there,
  you had to scroll down the page and select the
  'update course settings' button

- Now, if you refresh the page, the image should
  automatically persist without updating the
  course settings

- From the cog dropdown on the image, select the
  option to remove image.

- Previously for image removal to persist, you had
  to update the course settings

- Now, if you refresh, the image removal should
  persist without updating the course settings

Change-Id: Icc91146ad29f936fdcfd8c0f55c54deb85e1dde5
Reviewed-on: https://gerrit.instructure.com/86671
Reviewed-by: Matt Zabriskie <mzabriskie@instructure.com>
Tested-by: Jenkins
QA-Review: Pierce Arner <pierce@instructure.com>
Product-Review: Kyle Follett <kfollett@instructure.com>
This commit is contained in:
Stephen Jensen 2016-08-01 15:27:34 -06:00
parent e9951e5434
commit 6e8285d72e
7 changed files with 63 additions and 73 deletions

View File

@ -76,9 +76,37 @@ define ([
};
},
prepareSetImage (imageUrl, imageId, ajaxLib = axios) {
putImageData(courseId, imageUrl, imageId = null, ajaxLib = axios) {
const data = imageId ? {"course[image_id]": imageId} :
{"course[image_url]": imageUrl};
return (dispatch, getState) => {
this.ajaxPutFormData(`/api/v1/courses/${courseId}`, data, ajaxLib)
.then((response)=> {
dispatch(imageId ? this.setCourseImageId(imageUrl, imageId) :
this.setCourseImageUrl(imageUrl));
})
.catch((response) => {
this.errorUploadingImage();
})
}
},
putRemoveImage(courseId, ajaxLib = axios) {
return (dispatch, getState) => {
this.ajaxPutFormData(`/api/v1/courses/${courseId}`, {"course[remove_image]": true}, ajaxLib)
.then((response)=> {
dispatch(this.removeImage());
})
.catch((response) => {
$.flashError(I18n.t("Error removing image"));
})
}
},
prepareSetImage (imageUrl, imageId, courseId, ajaxLib = axios) {
if (imageUrl) {
return this.setCourseImageId(imageUrl, imageId);
return this.putImageData(courseId, imageUrl, imageId, ajaxLib);
} else {
// In this case the url field was blank so we could either
// recreate it or hit the API to get it. We hit the api
@ -86,7 +114,7 @@ define ([
return (dispatch, getState) => {
ajaxLib.get(`/api/v1/files/${imageId}`)
.then((response) => {
dispatch(this.setCourseImageId(response.data.url, imageId));
dispatch(this.putImageData(courseId, response.data.url, imageId, ajaxLib));
})
.catch((response) => {
this.errorUploadingImage();
@ -119,7 +147,7 @@ define ([
formData.append('file', file);
ajaxLib.post(response.data.upload_url, formData)
.then((response) => {
dispatch(this.prepareSetImage(response.data.url, response.data.id));
dispatch(this.prepareSetImage(response.data.url, response.data.id, courseId, ajaxLib));
})
.catch((response) => {
this.errorUploadingImage();
@ -133,6 +161,22 @@ define ([
$.flashWarning(I18n.t("'%{type}' is not a valid image type (try jpg, png, or gif)", {type}));
}
};
},
ajaxPutFormData(path, data, ajaxLib = axios) {
return (
ajaxLib.put(path, data,
{
// TODO: this is a naive implementation,
// upgrading to axios@0.12.0 will make it unnecessary
// by using URLSearchParams.
transformRequest: function (data, headers) {
return Object.keys(data).reduce((prev, key) => {
return prev + (prev ? '&' : '') + `${key}=${data[key]}`;
}, '');
}
})
);
}
};

View File

@ -52,7 +52,7 @@ define([
}
removeImage() {
this.props.store.dispatch(Actions.removeImage());
this.props.store.dispatch(Actions.putRemoveImage(this.props.courseId));
}
imageControls () {
@ -130,16 +130,8 @@ define([
backgroundImage: `url(${this.state.imageUrl})`
};
var value = this.state.removeImage ? true : this.state.courseImage;
return (
<div>
<input
ref="hiddenInput"
type="hidden"
name={this.state.hiddenInputName}
value={value}
/>
<div
className="CourseImageSelector"
style={(this.state.imageUrl) ? styles : {}}
@ -155,7 +147,7 @@ define([
courseId={this.props.courseId}
handleClose={this.handleModalClose}
handleFileUpload={(e, courseId) => this.props.store.dispatch(Actions.uploadFile(e, courseId))}
handleFlickrUrlUpload={(flickrUrl) => this.props.store.dispatch(Actions.setCourseImageUrl(flickrUrl))}
handleFlickrUrlUpload={(flickrUrl) => this.props.store.dispatch(Actions.putImageData(this.props.courseId, flickrUrl))}
/>
</Modal>
</div>

View File

@ -19,23 +19,17 @@ define([
state.imageUrl = action.payload.imageUrl;
state.courseImage = action.payload.imageId;
state.showModal = false;
state.removeImage = false;
state.hiddenInputName = "course[image_id]"
return state;
},
SET_COURSE_IMAGE_URL (state, action) {
state.imageUrl = action.payload.imageUrl;
state.courseImage = action.payload.imageUrl;
state.showModal = false;
state.removeImage = false;
state.hiddenInputName = "course[image_url]";
return state;
},
REMOVE_IMAGE (state) {
state.imageUrl = '';
state.courseImage = 'abc';
state.removeImage = true;
state.hiddenInputName = "course[remove_image]";
return state;
}
};

View File

@ -5,8 +5,6 @@ define([], () => {
imageUrl: '',
showModal: false,
gettingImage: false,
removeImage: false,
hiddenInputName: ''
};
return initialState;

View File

@ -77,10 +77,11 @@ define([
deepEqual(actual, expected, 'the objects match');
});
test('prepareSetImage with a imageUrl calls setCourseImageId', () => {
sinon.spy(Actions, 'setCourseImageId');
Actions.prepareSetImage('http://imageUrl', 12);
ok(Actions.setCourseImageId.called, 'setCourseImageId was called');
test('prepareSetImage with a imageUrl calls putImageData', () => {
sinon.spy(Actions, 'putImageData');
Actions.prepareSetImage('http://imageUrl', 12, 0);
ok(Actions.putImageData.called, 'putImageData was called');
Actions.putImageData.restore();
});
asyncTest('prepareSetImage without a imageUrl calls the API to get the url', () => {
@ -98,17 +99,12 @@ define([
}
};
const expectedAction = {
type: 'SET_COURSE_IMAGE_ID',
payload: {
imageUrl: 'http://imageUrl',
imageId: 1
}
};
sinon.spy(Actions, 'putImageData');
Actions.prepareSetImage(null, 1, fakeAjaxLib)((dispatched) => {
Actions.prepareSetImage(null, 1, 0, fakeAjaxLib)((dispatched) => {
start();
deepEqual(dispatched, expectedAction, 'the proper action was dispatched');
ok(Actions.putImageData.called, 'putImageData was called indicating successfully hit API');
Actions.putImageData.restore();
});
});
@ -177,17 +173,12 @@ define([
preventDefault: () => {}
};
const expectedAction = {
type: 'SET_COURSE_IMAGE_ID',
payload: {
imageUrl: 'http://fileDownloadUrl',
imageId: 1
}
};
sinon.spy(Actions, 'prepareSetImage');
Actions.uploadFile(fakeDragonDropEvent, 1, fakeAjaxLib)((dispatched) => {
start();
deepEqual(dispatched, expectedAction, 'the SET_COURSE_IMAGE_ID action was fired');
ok(Actions.prepareSetImage.called, 'prepareSetImage was called');
Actions.prepareSetImage.restore();
});
})

View File

@ -23,13 +23,6 @@ define([
ok(component);
});
test('the hidden input reflects the state value of the selector', () => {
const component = TestUtils.renderIntoDocument(
<CourseImageSelector store={fakeStore} />
);
equal(React.findDOMNode(component.refs.hiddenInput).value, initialState.courseImage, 'the input matches');
});
test('it sets the background image style properly', () => {
const dispatchStub = sinon.stub(fakeStore, 'getState').returns(Object.assign(initialState, {
imageUrl: 'http://coolUrl'

View File

@ -64,34 +64,12 @@ define(['jsx/course_settings/reducer'], (reducer) => {
imageUrl: '',
courseImage: '',
showModal: true,
hiddenInputName: ''
};
const newState = reducer(initialState, action);
equal(newState.imageUrl, 'http://imageUrl', 'image url gets set');
equal(newState.courseImage, '42', 'image id gets set');
equal(newState.showModal, false, 'modal gets closed');
equal(newState.hiddenInputName, 'course[image_id]', 'input name is set properly');
});
test('REMOVE_IMAGE', () => {
const action = {
type: 'REMOVE_IMAGE'
};
const initialState = {
imageUrl: 'http://imageUrl',
courseImage: '42',
removeImage: false,
hiddenInputName: ''
};
const newState = reducer(initialState, action);
equal(newState.imageUrl, '', 'image url gets set');
equal(newState.courseImage, 'abc', 'image id gets set');
equal(newState.removeImage, true, 'remove image is set');
equal(newState.hiddenInputName, 'course[remove_image]', 'input name is set properly');
});
});