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) {
|
if (imageUrl) {
|
||||||
return this.setCourseImageId(imageUrl, imageId);
|
return this.putImageData(courseId, imageUrl, imageId, ajaxLib);
|
||||||
} else {
|
} else {
|
||||||
// In this case the url field was blank so we could either
|
// 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
|
// recreate it or hit the API to get it. We hit the api
|
||||||
|
@ -86,7 +114,7 @@ define ([
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
ajaxLib.get(`/api/v1/files/${imageId}`)
|
ajaxLib.get(`/api/v1/files/${imageId}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(this.setCourseImageId(response.data.url, imageId));
|
dispatch(this.putImageData(courseId, response.data.url, imageId, ajaxLib));
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
this.errorUploadingImage();
|
this.errorUploadingImage();
|
||||||
|
@ -119,7 +147,7 @@ define ([
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
ajaxLib.post(response.data.upload_url, formData)
|
ajaxLib.post(response.data.upload_url, formData)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(this.prepareSetImage(response.data.url, response.data.id));
|
dispatch(this.prepareSetImage(response.data.url, response.data.id, courseId, ajaxLib));
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
this.errorUploadingImage();
|
this.errorUploadingImage();
|
||||||
|
@ -133,6 +161,22 @@ define ([
|
||||||
$.flashWarning(I18n.t("'%{type}' is not a valid image type (try jpg, png, or gif)", {type}));
|
$.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() {
|
removeImage() {
|
||||||
this.props.store.dispatch(Actions.removeImage());
|
this.props.store.dispatch(Actions.putRemoveImage(this.props.courseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
imageControls () {
|
imageControls () {
|
||||||
|
@ -130,16 +130,8 @@ define([
|
||||||
backgroundImage: `url(${this.state.imageUrl})`
|
backgroundImage: `url(${this.state.imageUrl})`
|
||||||
};
|
};
|
||||||
|
|
||||||
var value = this.state.removeImage ? true : this.state.courseImage;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input
|
|
||||||
ref="hiddenInput"
|
|
||||||
type="hidden"
|
|
||||||
name={this.state.hiddenInputName}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
className="CourseImageSelector"
|
className="CourseImageSelector"
|
||||||
style={(this.state.imageUrl) ? styles : {}}
|
style={(this.state.imageUrl) ? styles : {}}
|
||||||
|
@ -155,7 +147,7 @@ define([
|
||||||
courseId={this.props.courseId}
|
courseId={this.props.courseId}
|
||||||
handleClose={this.handleModalClose}
|
handleClose={this.handleModalClose}
|
||||||
handleFileUpload={(e, courseId) => this.props.store.dispatch(Actions.uploadFile(e, courseId))}
|
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>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,23 +19,17 @@ define([
|
||||||
state.imageUrl = action.payload.imageUrl;
|
state.imageUrl = action.payload.imageUrl;
|
||||||
state.courseImage = action.payload.imageId;
|
state.courseImage = action.payload.imageId;
|
||||||
state.showModal = false;
|
state.showModal = false;
|
||||||
state.removeImage = false;
|
|
||||||
state.hiddenInputName = "course[image_id]"
|
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
SET_COURSE_IMAGE_URL (state, action) {
|
SET_COURSE_IMAGE_URL (state, action) {
|
||||||
state.imageUrl = action.payload.imageUrl;
|
state.imageUrl = action.payload.imageUrl;
|
||||||
state.courseImage = action.payload.imageUrl;
|
state.courseImage = action.payload.imageUrl;
|
||||||
state.showModal = false;
|
state.showModal = false;
|
||||||
state.removeImage = false;
|
|
||||||
state.hiddenInputName = "course[image_url]";
|
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
REMOVE_IMAGE (state) {
|
REMOVE_IMAGE (state) {
|
||||||
state.imageUrl = '';
|
state.imageUrl = '';
|
||||||
state.courseImage = 'abc';
|
state.courseImage = 'abc';
|
||||||
state.removeImage = true;
|
|
||||||
state.hiddenInputName = "course[remove_image]";
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,8 +5,6 @@ define([], () => {
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
showModal: false,
|
showModal: false,
|
||||||
gettingImage: false,
|
gettingImage: false,
|
||||||
removeImage: false,
|
|
||||||
hiddenInputName: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return initialState;
|
return initialState;
|
||||||
|
|
|
@ -77,10 +77,11 @@ define([
|
||||||
deepEqual(actual, expected, 'the objects match');
|
deepEqual(actual, expected, 'the objects match');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prepareSetImage with a imageUrl calls setCourseImageId', () => {
|
test('prepareSetImage with a imageUrl calls putImageData', () => {
|
||||||
sinon.spy(Actions, 'setCourseImageId');
|
sinon.spy(Actions, 'putImageData');
|
||||||
Actions.prepareSetImage('http://imageUrl', 12);
|
Actions.prepareSetImage('http://imageUrl', 12, 0);
|
||||||
ok(Actions.setCourseImageId.called, 'setCourseImageId was called');
|
ok(Actions.putImageData.called, 'putImageData was called');
|
||||||
|
Actions.putImageData.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
asyncTest('prepareSetImage without a imageUrl calls the API to get the url', () => {
|
asyncTest('prepareSetImage without a imageUrl calls the API to get the url', () => {
|
||||||
|
@ -98,17 +99,12 @@ define([
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedAction = {
|
sinon.spy(Actions, 'putImageData');
|
||||||
type: 'SET_COURSE_IMAGE_ID',
|
|
||||||
payload: {
|
|
||||||
imageUrl: 'http://imageUrl',
|
|
||||||
imageId: 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Actions.prepareSetImage(null, 1, fakeAjaxLib)((dispatched) => {
|
Actions.prepareSetImage(null, 1, 0, fakeAjaxLib)((dispatched) => {
|
||||||
start();
|
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: () => {}
|
preventDefault: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedAction = {
|
sinon.spy(Actions, 'prepareSetImage');
|
||||||
type: 'SET_COURSE_IMAGE_ID',
|
|
||||||
payload: {
|
|
||||||
imageUrl: 'http://fileDownloadUrl',
|
|
||||||
imageId: 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Actions.uploadFile(fakeDragonDropEvent, 1, fakeAjaxLib)((dispatched) => {
|
Actions.uploadFile(fakeDragonDropEvent, 1, fakeAjaxLib)((dispatched) => {
|
||||||
start();
|
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);
|
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', () => {
|
test('it sets the background image style properly', () => {
|
||||||
const dispatchStub = sinon.stub(fakeStore, 'getState').returns(Object.assign(initialState, {
|
const dispatchStub = sinon.stub(fakeStore, 'getState').returns(Object.assign(initialState, {
|
||||||
imageUrl: 'http://coolUrl'
|
imageUrl: 'http://coolUrl'
|
||||||
|
|
|
@ -64,34 +64,12 @@ define(['jsx/course_settings/reducer'], (reducer) => {
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
courseImage: '',
|
courseImage: '',
|
||||||
showModal: true,
|
showModal: true,
|
||||||
hiddenInputName: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newState = reducer(initialState, action);
|
const newState = reducer(initialState, action);
|
||||||
equal(newState.imageUrl, 'http://imageUrl', 'image url gets set');
|
equal(newState.imageUrl, 'http://imageUrl', 'image url gets set');
|
||||||
equal(newState.courseImage, '42', 'image id gets set');
|
equal(newState.courseImage, '42', 'image id gets set');
|
||||||
equal(newState.showModal, false, 'modal gets closed');
|
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