Add timeout to user and group deletion notification
Added timeout in DeleteHandler to auto-delete after a delay. Fixed issue where OC.Notification.hide() was called twice in a row when deleting multiple entries, causing the second notification to disappear. Fixed issue where "undo" click event handler was registered multiple times when calling setNotifications() twice. Added JS unit tests for the DeleteHandler class. Refix undo users, groups feature Timeout is now cleared in cancel(). Fixed click handler name for "undo" to be able to work with multiple DeleteHandler instances (in our case one for users and one for groups) so that there is no conflict.
This commit is contained in:
parent
31898aa635
commit
0d9f24a0ef
3 changed files with 237 additions and 5 deletions
|
@ -34,6 +34,16 @@ function DeleteHandler(endpoint, paramID, markCallback, removeCallback) {
|
|||
this.notificationPlaceholder = '%oid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of milliseconds after which the operation is performed.
|
||||
*/
|
||||
DeleteHandler.TIMEOUT_MS = 7000;
|
||||
|
||||
/**
|
||||
* Timer after which the action will be performed anyway.
|
||||
*/
|
||||
DeleteHandler.prototype._timeout = null;
|
||||
|
||||
/**
|
||||
* The function to be called after successfully marking the object for deletion
|
||||
* @callback markCallback
|
||||
|
@ -72,7 +82,9 @@ DeleteHandler.prototype.setNotification = function(notifier, dataID, message, un
|
|||
|
||||
var dh = this;
|
||||
|
||||
$('#notification').on('click', '.undo', function () {
|
||||
$('#notification')
|
||||
.off('click.deleteHandler_' + dataID)
|
||||
.on('click.deleteHandler_' + dataID, '.undo', function () {
|
||||
if ($('#notification').data(dh.notificationDataID)) {
|
||||
var oid = dh.oidToDelete;
|
||||
dh.cancel();
|
||||
|
@ -116,18 +128,36 @@ DeleteHandler.prototype.hideNotification = function() {
|
|||
*/
|
||||
DeleteHandler.prototype.mark = function(oid) {
|
||||
if(this.oidToDelete !== false) {
|
||||
this.deleteEntry();
|
||||
// passing true to avoid hiding the notification
|
||||
// twice and causing the second notification
|
||||
// to disappear immediately
|
||||
this.deleteEntry(true);
|
||||
}
|
||||
this.oidToDelete = oid;
|
||||
this.canceled = false;
|
||||
this.markCallback(oid);
|
||||
this.showNotification();
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
if (DeleteHandler.TIMEOUT_MS > 0) {
|
||||
this._timeout = window.setTimeout(
|
||||
_.bind(this.deleteEntry, this),
|
||||
DeleteHandler.TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* cancels a delete operation
|
||||
*/
|
||||
DeleteHandler.prototype.cancel = function() {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
this.canceled = true;
|
||||
this.oidToDelete = false;
|
||||
};
|
||||
|
@ -137,22 +167,31 @@ DeleteHandler.prototype.cancel = function() {
|
|||
* initialized by mark(). On error, it will show a message via
|
||||
* OC.dialogs.alert. On success, a callback is fired so that the client can
|
||||
* update the web interface accordingly.
|
||||
*
|
||||
* @param {boolean} [keepNotification] true to keep the notification, false to hide
|
||||
* it, defaults to false
|
||||
*/
|
||||
DeleteHandler.prototype.deleteEntry = function() {
|
||||
DeleteHandler.prototype.deleteEntry = function(keepNotification) {
|
||||
if(this.canceled || this.oidToDelete === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var dh = this;
|
||||
if($('#notification').data(this.notificationDataID) === true) {
|
||||
if(!keepNotification && $('#notification').data(this.notificationDataID) === true) {
|
||||
dh.hideNotification();
|
||||
}
|
||||
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
payload[dh.ajaxParamID] = dh.oidToDelete;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: OC.filePath('settings', 'ajax', dh.ajaxEndpoint),
|
||||
// FIXME: do not use synchronous ajax calls as they block the browser !
|
||||
async: false,
|
||||
data: payload,
|
||||
success: function (result) {
|
||||
|
|
185
settings/tests/js/users/deleteHandlerSpec.js
Normal file
185
settings/tests/js/users/deleteHandlerSpec.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* ownCloud
|
||||
*
|
||||
* @author Vincent Petry
|
||||
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public
|
||||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('DeleteHandler tests', function() {
|
||||
var showNotificationSpy;
|
||||
var hideNotificationSpy;
|
||||
var clock;
|
||||
var removeCallback;
|
||||
var markCallback;
|
||||
var undoCallback;
|
||||
|
||||
function init(markCallback, removeCallback, undoCallback) {
|
||||
var handler = new DeleteHandler('dummyendpoint.php', 'paramid', markCallback, removeCallback);
|
||||
handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry', undoCallback);
|
||||
return handler;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
showNotificationSpy = sinon.spy(OC.Notification, 'showHtml');
|
||||
hideNotificationSpy = sinon.spy(OC.Notification, 'hide');
|
||||
clock = sinon.useFakeTimers();
|
||||
removeCallback = sinon.stub();
|
||||
markCallback = sinon.stub();
|
||||
undoCallback = sinon.stub();
|
||||
|
||||
$('#testArea').append('<div id="notification"></div>');
|
||||
});
|
||||
afterEach(function() {
|
||||
showNotificationSpy.restore();
|
||||
hideNotificationSpy.restore();
|
||||
clock.restore();
|
||||
});
|
||||
it('shows a notification when marking for delete', function() {
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.mark('some_uid');
|
||||
|
||||
expect(showNotificationSpy.calledOnce).toEqual(true);
|
||||
expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_uid entry');
|
||||
|
||||
expect(markCallback.calledOnce).toEqual(true);
|
||||
expect(markCallback.getCall(0).args[0]).toEqual('some_uid');
|
||||
expect(removeCallback.notCalled).toEqual(true);
|
||||
expect(undoCallback.notCalled).toEqual(true);
|
||||
|
||||
expect(fakeServer.requests.length).toEqual(0);
|
||||
});
|
||||
it('deletes first entry and reshows notification on second delete', function() {
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.mark('some_uid');
|
||||
|
||||
expect(showNotificationSpy.calledOnce).toEqual(true);
|
||||
expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_uid entry');
|
||||
showNotificationSpy.reset();
|
||||
|
||||
handler.mark('some_other_uid');
|
||||
|
||||
expect(hideNotificationSpy.calledOnce).toEqual(true);
|
||||
expect(showNotificationSpy.calledOnce).toEqual(true);
|
||||
expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_other_uid entry');
|
||||
|
||||
expect(markCallback.calledTwice).toEqual(true);
|
||||
expect(markCallback.getCall(0).args[0]).toEqual('some_uid');
|
||||
expect(markCallback.getCall(1).args[0]).toEqual('some_other_uid');
|
||||
expect(removeCallback.notCalled).toEqual(true);
|
||||
expect(undoCallback.notCalled).toEqual(true);
|
||||
|
||||
// previous one was delete
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var request = fakeServer.requests[0];
|
||||
expect(request.url).toEqual(OC.webroot + '/index.php/settings/ajax/dummyendpoint.php');
|
||||
});
|
||||
it('automatically deletes after timeout', function() {
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.mark('some_uid');
|
||||
|
||||
clock.tick(5000);
|
||||
// nothing happens yet
|
||||
expect(fakeServer.requests.length).toEqual(0);
|
||||
|
||||
clock.tick(3000);
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var request = fakeServer.requests[0];
|
||||
expect(request.url).toEqual(OC.webroot + '/index.php/settings/ajax/dummyendpoint.php');
|
||||
});
|
||||
it('deletes when deleteEntry is called', function() {
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.mark('some_uid');
|
||||
|
||||
handler.deleteEntry();
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var request = fakeServer.requests[0];
|
||||
expect(request.url).toEqual(OC.webroot + '/index.php/settings/ajax/dummyendpoint.php');
|
||||
});
|
||||
it('cancels deletion when undo is clicked', function() {
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry <span class="undo">Undo</span>', undoCallback);
|
||||
handler.mark('some_uid');
|
||||
$('#notification .undo').click();
|
||||
|
||||
expect(undoCallback.calledOnce).toEqual(true);
|
||||
|
||||
// timer was cancelled
|
||||
clock.tick(10000);
|
||||
expect(fakeServer.requests.length).toEqual(0);
|
||||
});
|
||||
it('cancels deletion when cancel method is called', function() {
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry <span class="undo">Undo</span>', undoCallback);
|
||||
handler.mark('some_uid');
|
||||
handler.cancel();
|
||||
|
||||
// not sure why, seems to be by design
|
||||
expect(undoCallback.notCalled).toEqual(true);
|
||||
|
||||
// timer was cancelled
|
||||
clock.tick(10000);
|
||||
expect(fakeServer.requests.length).toEqual(0);
|
||||
});
|
||||
it('calls removeCallback after successful server side deletion', function() {
|
||||
fakeServer.respondWith(/\/index\.php\/settings\/ajax\/dummyendpoint.php/, [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({status: 'success'})
|
||||
]);
|
||||
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.mark('some_uid');
|
||||
handler.deleteEntry();
|
||||
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var request = fakeServer.requests[0];
|
||||
var query = OC.parseQueryString(request.requestBody);
|
||||
expect(query.paramid).toEqual('some_uid');
|
||||
|
||||
expect(removeCallback.calledOnce).toEqual(true);
|
||||
expect(undoCallback.notCalled).toEqual(true);
|
||||
expect(removeCallback.getCall(0).args[0]).toEqual('some_uid');
|
||||
});
|
||||
it('calls undoCallback and shows alert after failed server side deletion', function() {
|
||||
// stub t to avoid extra calls
|
||||
var tStub = sinon.stub(window, 't').returns('text');
|
||||
fakeServer.respondWith(/\/index\.php\/settings\/ajax\/dummyendpoint.php/, [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({status: 'error', data: {message: 'test error'}})
|
||||
]);
|
||||
|
||||
var alertDialogStub = sinon.stub(OC.dialogs, 'alert');
|
||||
var handler = init(markCallback, removeCallback, undoCallback);
|
||||
handler.mark('some_uid');
|
||||
handler.deleteEntry();
|
||||
|
||||
expect(fakeServer.requests.length).toEqual(1);
|
||||
var request = fakeServer.requests[0];
|
||||
var query = OC.parseQueryString(request.requestBody);
|
||||
expect(query.paramid).toEqual('some_uid');
|
||||
|
||||
expect(removeCallback.notCalled).toEqual(true);
|
||||
expect(undoCallback.calledOnce).toEqual(true);
|
||||
expect(undoCallback.getCall(0).args[0]).toEqual('some_uid');
|
||||
|
||||
expect(alertDialogStub.calledOnce);
|
||||
|
||||
alertDialogStub.restore();
|
||||
tStub.restore();
|
||||
});
|
||||
});
|
|
@ -66,7 +66,15 @@ module.exports = function(config) {
|
|||
'apps/files_external/js/mountsfilelist.js'
|
||||
],
|
||||
testFiles: ['apps/files_external/tests/js/*.js']
|
||||
}];
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
srcFiles: [
|
||||
'settings/js/users/deleteHandler.js'
|
||||
],
|
||||
testFiles: ['settings/tests/js/users/deleteHandlerSpec.js']
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// respect NOCOVERAGE env variable
|
||||
|
|
Loading…
Reference in a new issue