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:
parent
e9951e5434
commit
6e8285d72e
|
@ -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]}`;
|
||||
}, '');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,8 +5,6 @@ define([], () => {
|
|||
imageUrl: '',
|
||||
showModal: false,
|
||||
gettingImage: false,
|
||||
removeImage: false,
|
||||
hiddenInputName: ''
|
||||
};
|
||||
|
||||
return initialState;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
})
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
||||
});
|
Loading…
Reference in New Issue