diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2019-02-14 14:24:20 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-14 14:24:20 +0100 |
commit | 6a3f4e4957eb803ed4b764f01d50727403d7a3ba (patch) | |
tree | f3a7e8772e2f4e61b751fe645408b5f47de31462 | |
parent | e65f7f05de6443bd66b1c31325cbc3cbe149d1e5 (diff) | |
parent | 08919eb19335825dc1f2cd950f9529a2b9caf29b (diff) | |
download | nextcloud-server-6a3f4e4957eb803ed4b764f01d50727403d7a3ba.tar.gz nextcloud-server-6a3f4e4957eb803ed4b764f01d50727403d7a3ba.zip |
Merge pull request #12652 from tomasz-grobelny/operation_progress_improvements3
Operation progress improvements
-rw-r--r-- | apps/files/js/file-upload.js | 62 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 143 | ||||
-rw-r--r-- | apps/files/js/merged-index.json | 2 | ||||
-rw-r--r-- | apps/files/js/operationprogressbar.js | 79 | ||||
-rw-r--r-- | apps/files/js/semaphore.js | 37 | ||||
-rw-r--r-- | apps/files/js/templates.js | 16 | ||||
-rw-r--r-- | apps/files/js/templates/operationprogressbar.handlebars | 6 | ||||
-rw-r--r-- | apps/files/js/templates/operationprogressbarlabel.handlebars | 4 | ||||
-rw-r--r-- | apps/files/templates/list.php | 6 | ||||
-rw-r--r-- | apps/files/tests/js/fileUploadSpec.js | 5 | ||||
-rw-r--r-- | apps/files/tests/js/filelistSpec.js | 450 | ||||
-rw-r--r-- | core/js/files/client.js | 3 |
12 files changed, 525 insertions, 288 deletions
diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 79d266a300b..e0b274cdc76 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -429,6 +429,11 @@ OC.Uploader.prototype = _.extend({ fileList: null, /** + * @type OCA.Files.OperationProgressBar + */ + progressBar: null, + + /** * @type OC.Files.Client */ filesClient: null, @@ -759,14 +764,6 @@ OC.Uploader.prototype = _.extend({ callbacks.onNoConflicts(selection); }, - _hideProgressBar: function() { - var self = this; - $('#uploadprogresswrapper .stop').fadeOut(); - $('#uploadprogressbar').fadeOut(function() { - self.$uploadEl.trigger(new $.Event('resized')); - }); - }, - _updateProgressBarOnUploadStop: function() { if (this._pendingUploadDoneCount === 0) { // All the uploads ended and there is no pending operation, so hide @@ -780,17 +777,31 @@ OC.Uploader.prototype = _.extend({ return; } - $('#uploadprogressbar .label .mobile').text(t('core', '…')); - $('#uploadprogressbar .label .desktop').text(t('core', 'Processing files …')); + this._setProgressBarText(t('core', 'Processing files …'), t('core', '…')); // Nothing is being uploaded at this point, and the pending operations // can not be cancelled, so the cancel button should be hidden. - $('#uploadprogresswrapper .stop').fadeOut(); + this._hideCancelButton(); + }, + + _hideProgressBar: function() { + this.progressBar.hideProgressBar(); + }, + + _hideCancelButton: function() { + this.progressBar.hideCancelButton(); }, _showProgressBar: function() { - $('#uploadprogressbar').fadeIn(); - this.$uploadEl.trigger(new $.Event('resized')); + this.progressBar.showProgressBar(); + }, + + _setProgressBarValue: function(value) { + this.progressBar.setProgressBarValue(value); + }, + + _setProgressBarText: function(textDesktop, textMobile, title) { + this.progressBar.setProgressBarText(textDesktop, textMobile, title); }, /** @@ -826,6 +837,7 @@ OC.Uploader.prototype = _.extend({ options = options || {}; this.fileList = options.fileList; + this.progressBar = options.progressBar; this.filesClient = options.filesClient || OC.Files.getClient(); this.davClient = new OC.Files.Client({ host: this.filesClient.getHost(), @@ -839,7 +851,7 @@ OC.Uploader.prototype = _.extend({ this.$uploadEl = $uploadEl; if ($uploadEl.exists()) { - $('#uploadprogresswrapper .stop').on('click', function() { + this.progressBar.on('cancel', function() { self.cancelUploads(); }); @@ -1099,16 +1111,8 @@ OC.Uploader.prototype = _.extend({ // add progress handlers fileupload.on('fileuploadstart', function(e, data) { self.log('progress handle fileuploadstart', e, data); - $('#uploadprogresswrapper .stop').show(); - $('#uploadprogresswrapper .label').show(); - $('#uploadprogressbar').progressbar({value: 0}); - $('#uploadprogressbar .ui-progressbar-value'). - html('<em class="label inner"><span class="desktop">' - + t('files', 'Uploading …') - + '</span><span class="mobile">' - + t('files', '…') - + '</span></em>'); - $('#uploadprogressbar').tooltip({placement: 'bottom'}); + self._setProgressBarText(t('files', 'Uploading …'), t('files', '…')); + self._setProgressBarValue(0); self._showProgressBar(); // initial remaining time variables lastUpdate = new Date().getTime(); @@ -1158,16 +1162,12 @@ OC.Uploader.prototype = _.extend({ // show "Uploading ..." for durations longer than 4 hours h = t('files', 'Uploading …'); } - $('#uploadprogressbar .label .mobile').text(h); - $('#uploadprogressbar .label .desktop').text(h); - $('#uploadprogressbar').attr('original-title', - t('files', '{loadedSize} of {totalSize} ({bitrate})' , { + self._setProgressBarText(h, h, t('files', '{loadedSize} of {totalSize} ({bitrate})' , { loadedSize: humanFileSize(data.loaded), totalSize: humanFileSize(data.total), bitrate: humanFileSize(data.bitrate / 8) + '/s' - }) - ); - $('#uploadprogressbar').progressbar('value', progress); + })); + self._setProgressBarValue(progress); self.trigger('progressall', e, data); }); fileupload.on('fileuploadstop', function(e, data) { diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 363f81a1a73..868623d9a8a 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -386,11 +386,16 @@ }); } + this._operationProgressBar = new OCA.Files.OperationProgressBar(); + this._operationProgressBar.render(); + this.$el.find('#uploadprogresswrapper').replaceWith(this._operationProgressBar.$el); + if (options.enableUpload) { // TODO: auto-create this element var $uploadEl = this.$el.find('#file_upload_start'); if ($uploadEl.exists()) { this._uploader = new OC.Uploader($uploadEl, { + progressBar: this._operationProgressBar, fileList: this, filesClient: this.filesClient, dropZone: $('#content'), @@ -2206,21 +2211,39 @@ remove: function(name, options){ options = options || {}; var fileEl = this.findFileEl(name); - var fileId = fileEl.data('id'); - var index = fileEl.index(); - if (!fileEl.length) { - return null; + var fileData = _.findWhere(this.files, {name: name}); + if (!fileData) { + return; } + var fileId = fileData.id; if (this._selectedFiles[fileId]) { // remove from selection first this._selectFileEl(fileEl, false); this.updateSelectionSummary(); } + if (this._selectedFiles[fileId]) { + delete this._selectedFiles[fileId]; + this._selectionSummary.remove(fileData); + this.updateSelectionSummary(); + } + var index = this.files.findIndex(function(el){return el.name==name;}); + this.files.splice(index, 1); + + // TODO: improve performance on batch update + this.isEmpty = !this.files.length; + if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { + this.updateEmptyContent(); + this.fileSummary.remove({type: fileData.type, size: fileData.size}, true); + } + + if (!fileEl.length) { + return null; + } + if (this._dragOptions && (fileEl.data('permissions') & OC.PERMISSION_DELETE)) { // file is only draggable when delete permissions are set fileEl.find('td.filename').draggable('destroy'); } - this.files.splice(index, 1); if (this._currentFileModel && this._currentFileModel.get('id') === fileId) { // Note: in the future we should call destroy() directly on the model // and the model will take care of the deletion. @@ -2230,12 +2253,6 @@ this._updateDetailsView(null); } fileEl.remove(); - // TODO: improve performance on batch update - this.isEmpty = !this.files.length; - if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) { - this.updateEmptyContent(); - this.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}, true); - } var lastIndex = this.$fileList.children().length; // if there are less elements visible than one page @@ -2282,29 +2299,6 @@ fileNames = [fileNames]; } - function Semaphore(max) { - var counter = 0; - var waiting = []; - - this.acquire = function() { - if(counter < max) { - counter++; - return new Promise(function(resolve) { resolve(); }); - } else { - return new Promise(function(resolve) { waiting.push(resolve); }); - } - }; - - this.release = function() { - counter--; - if (waiting.length > 0 && counter < max) { - counter++; - var promise = waiting.shift(); - promise(); - } - }; - } - var moveFileFunction = function(fileName) { var $tr = self.findFileEl(fileName); self.showFileBusyState($tr, true); @@ -2316,7 +2310,7 @@ return self.filesClient.move(dir + fileName, targetPath + fileName) .done(function() { // if still viewing the same directory - if (OC.joinPaths(self.getCurrentDirectory(), '/') === dir) { + if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) { // recalculate folder size var oldFile = self.findFileEl(target); var newFile = self.findFileEl(fileName); @@ -2325,7 +2319,6 @@ oldFile.data('size', newSize); oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize)); - // TODO: also update entry in FileList.files self.remove(fileName); } }) @@ -2345,22 +2338,33 @@ self.showFileBusyState($tr, false); }); }; + return this.reportOperationProgress(fileNames, moveFileFunction, callback); + }, + + _reflect: function (promise){ + return promise.then(function(v){ return {};}, function(e){ return {};}); + }, - var mcSemaphore = new Semaphore(10); + reportOperationProgress: function (fileNames, operationFunction, callback){ + var self = this; + self._operationProgressBar.showProgressBar(false); + var mcSemaphore = new OCA.Files.Semaphore(5); var counter = 0; var promises = _.map(fileNames, function(arg) { return mcSemaphore.acquire().then(function(){ - moveFileFunction(arg).then(function(){ + return operationFunction(arg).always(function(){ mcSemaphore.release(); counter++; + self._operationProgressBar.setProgressBarValue(100.0*counter/fileNames.length); }); }); }); - return Promise.all(promises).then(function(){ + return Promise.all(_.map(promises, self._reflect)).then(function(){ if (callback) { callback(); } + self._operationProgressBar.hideProgressBar(); }); }, @@ -2385,7 +2389,7 @@ if (!_.isArray(fileNames)) { fileNames = [fileNames]; } - _.each(fileNames, function(fileName) { + var copyFileFunction = function(fileName) { var $tr = self.findFileEl(fileName); self.showFileBusyState($tr, true); if (targetPath.charAt(targetPath.length - 1) !== '/') { @@ -2441,12 +2445,12 @@ } } } - self.filesClient.copy(dir + fileName, targetPathAndName) + return self.filesClient.copy(dir + fileName, targetPathAndName) .done(function () { filesToNotify.push(fileName); // if still viewing the same directory - if (OC.joinPaths(self.getCurrentDirectory(), '/') === dir) { + if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) { // recalculate folder size var oldFile = self.findFileEl(target); var newFile = self.findFileEl(fileName); @@ -2513,11 +2517,8 @@ } } }); - }); - - if (callback) { - callback(); - } + }; + return this.reportOperationProgress(fileNames, copyFileFunction, callback); }, /** @@ -2939,9 +2940,6 @@ // delete all files in directory files = _.pluck(this.files, 'name'); } - if (files) { - this.showFileBusyState(files, true); - } // Finish any existing actions if (this.lastAction) { this.lastAction(); @@ -2949,43 +2947,38 @@ dir = dir || this.getCurrentDirectory(); - function removeFromList(file) { - var fileEl = self.remove(file, {updateSummary: false}); - // FIXME: not sure why we need this after the - // element isn't even in the DOM any more - fileEl.find('.selectCheckBox').prop('checked', false); - fileEl.removeClass('selected'); - self.fileSummary.remove({type: fileEl.attr('data-type'), size: fileEl.attr('data-size')}); - // TODO: this info should be returned by the ajax call! - self.updateEmptyContent(); - self.fileSummary.update(); - self.updateSelectionSummary(); - // FIXME: don't repeat this, do it once all files are done - self.updateStorageStatistics(); - self.updateStorageQuotas(); - } - - _.each(files, function(file) { - self.filesClient.remove(dir + '/' + file) + var removeFunction = function(fileName) { + var $tr = self.findFileEl(fileName); + self.showFileBusyState($tr, true); + return self.filesClient.remove(dir + '/' + fileName) .done(function() { - removeFromList(file); + if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) { + self.remove(fileName); + } }) .fail(function(status) { if (status === 404) { // the file already did not exist, remove it from the list - removeFromList(file); + if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) { + self.remove(fileName); + } } else { // only reset the spinner for that one file OC.Notification.show(t('files', 'Error deleting file "{fileName}".', - {fileName: file}), {type: 'error'} + {fileName: fileName}), {type: 'error'} ); - var deleteAction = self.findFileEl(file).find('.action.delete'); - deleteAction.removeClass('icon-loading-small').addClass('icon-delete'); - self.showFileBusyState(files, false); } + }) + .always(function() { + self.showFileBusyState($tr, false); }); - }); + }; + return this.reportOperationProgress(files, removeFunction).then(function(){ + self.updateStorageStatistics(); + self.updateStorageQuotas(); + }); }, + /** * Creates the file summary section */ diff --git a/apps/files/js/merged-index.json b/apps/files/js/merged-index.json index e891d10bdae..8d25daa6b3c 100644 --- a/apps/files/js/merged-index.json +++ b/apps/files/js/merged-index.json @@ -21,7 +21,9 @@ "sidebarpreviewmanager.js", "sidebarpreviewtext.js", "detailtabview.js", + "semaphore.js", "mainfileinfodetailview.js", + "operationprogressbar.js", "detailsview.js", "fileactions.js", "fileactionsmenu.js", diff --git a/apps/files/js/operationprogressbar.js b/apps/files/js/operationprogressbar.js new file mode 100644 index 00000000000..efeea4ad78f --- /dev/null +++ b/apps/files/js/operationprogressbar.js @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var OperationProgressBar = OC.Backbone.View.extend({ + tagName: 'div', + id: 'uploadprogresswrapper', + events: { + 'click button.stop': '_onClickCancel' + }, + + render: function() { + this.$el.html(OCA.Files.Templates['operationprogressbar']({ + textCancelButton: t('Cancel operation') + })); + this.setProgressBarText(t('Uploading …'), t('…')); + }, + + hideProgressBar: function() { + var self = this; + $('#uploadprogresswrapper .stop').fadeOut(); + $('#uploadprogressbar').fadeOut(function() { + self.$el.trigger(new $.Event('resized')); + }); + }, + + hideCancelButton: function() { + $('#uploadprogresswrapper .stop').fadeOut(function() { + this.$el.trigger(new $.Event('resized')); + }); + }, + + showProgressBar: function(showCancelButton) { + if (showCancelButton) { + showCancelButton = true; + } + $('#uploadprogressbar').progressbar({value: 0}); + if(showCancelButton) { + $('#uploadprogresswrapper .stop').show(); + } else { + $('#uploadprogresswrapper .stop').hide(); + } + $('#uploadprogresswrapper .label').show(); + $('#uploadprogressbar').fadeIn(); + this.$el.trigger(new $.Event('resized')); + }, + + setProgressBarValue: function(value) { + $('#uploadprogressbar').progressbar({value: value}); + }, + + setProgressBarText: function(textDesktop, textMobile, title) { + var labelHtml = OCA.Files.Templates['operationprogressbarlabel']({textDesktop: textDesktop, textMobile: textMobile}); + $('#uploadprogressbar .ui-progressbar-value').html(labelHtml); + $('#uploadprogressbar .ui-progressbar-value>em').addClass('inner'); + $('#uploadprogressbar>em').replaceWith(labelHtml); + $('#uploadprogressbar>em').addClass('outer'); + $('#uploadprogressbar').tooltip({placement: 'bottom'}); + if(title) { + $('#uploadprogressbar').attr('original-title', title); + } + $('#uploadprogresswrapper .stop').show(); + }, + + _onClickCancel: function (event) { + this.trigger('cancel'); + return false; + } + }); + + OCA.Files.OperationProgressBar = OperationProgressBar; +})(OC, OCA); diff --git a/apps/files/js/semaphore.js b/apps/files/js/semaphore.js new file mode 100644 index 00000000000..044f0af23f3 --- /dev/null +++ b/apps/files/js/semaphore.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(){ + var Semaphore = function(max) { + var counter = 0; + var waiting = []; + + this.acquire = function() { + if(counter < max) { + counter++; + return new Promise(function(resolve) { resolve(); }); + } else { + return new Promise(function(resolve) { waiting.push(resolve); }); + } + }; + + this.release = function() { + counter--; + if (waiting.length > 0 && counter < max) { + counter++; + var promise = waiting.shift(); + promise(); + } + }; + }; + + OCA.Files.Semaphore = Semaphore; + +})(); diff --git a/apps/files/js/templates.js b/apps/files/js/templates.js index a2bdae5b3c4..be6b255d594 100644 --- a/apps/files/js/templates.js +++ b/apps/files/js/templates.js @@ -245,6 +245,22 @@ templates['newfilemenu_filename_form'] = template({"compiler":[7,">= 4.0.0"],"ma + alias4(((helper = (helper = helpers.fileName || (depth0 != null ? depth0.fileName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileName","hash":{},"data":data}) : helper))) + "\" autocomplete=\"off\" autocapitalize=\"off\">\n <input type=\"submit\" value=\" \" class=\"icon-confirm\" />\n</form>\n"; },"useData":true}); +templates['operationprogressbar'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { + var helper; + + return "<div id=\"uploadprogressbar\">\n <em class=\"label outer\" style=\"display:none\"></em>\n</div>\n<button class=\"stop icon-close\" style=\"display:none\">\n <span class=\"hidden-visually\">" + + container.escapeExpression(((helper = (helper = helpers.textCancelButton || (depth0 != null ? depth0.textCancelButton : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"textCancelButton","hash":{},"data":data}) : helper))) + + "</span>\n</button>\n"; +},"useData":true}); +templates['operationprogressbarlabel'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { + var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + + return "<em class=\"label\">\n <span class=\"desktop\">" + + alias4(((helper = (helper = helpers.textDesktop || (depth0 != null ? depth0.textDesktop : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"textDesktop","hash":{},"data":data}) : helper))) + + "</span>\n <span class=\"mobile\">" + + alias4(((helper = (helper = helpers.textMobile || (depth0 != null ? depth0.textMobile : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"textMobile","hash":{},"data":data}) : helper))) + + "</span>\n</em>\n"; +},"useData":true}); templates['template_addbutton'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; diff --git a/apps/files/js/templates/operationprogressbar.handlebars b/apps/files/js/templates/operationprogressbar.handlebars new file mode 100644 index 00000000000..c02ac623fab --- /dev/null +++ b/apps/files/js/templates/operationprogressbar.handlebars @@ -0,0 +1,6 @@ +<div id="uploadprogressbar"> + <em class="label outer" style="display:none"></em> +</div> +<button class="stop icon-close" style="display:none"> + <span class="hidden-visually">{{textCancelButton}}</span> +</button> diff --git a/apps/files/js/templates/operationprogressbarlabel.handlebars b/apps/files/js/templates/operationprogressbarlabel.handlebars new file mode 100644 index 00000000000..24ac66994da --- /dev/null +++ b/apps/files/js/templates/operationprogressbarlabel.handlebars @@ -0,0 +1,4 @@ +<em class="label"> + <span class="desktop">{{textDesktop}}</span> + <span class="mobile">{{textMobile}}</span> +</em> diff --git a/apps/files/templates/list.php b/apps/files/templates/list.php index 75dc2bee26f..8b20c84e008 100644 --- a/apps/files/templates/list.php +++ b/apps/files/templates/list.php @@ -1,12 +1,6 @@ <div id="controls"> <div class="actions creatable hidden"> <div id="uploadprogresswrapper"> - <div id="uploadprogressbar"> - <em class="label outer" style="display:none"><span class="desktop"><?php p($l->t('Uploading …'));?></span><span class="mobile"><?php p($l->t('…'));?></span></em> - </div> - <button class="stop icon-close" style="display:none"> - <span class="hidden-visually"><?php p($l->t('Cancel upload')) ?></span> - </button> </div> </div> <div id="file_action_panel"></div> diff --git a/apps/files/tests/js/fileUploadSpec.js b/apps/files/tests/js/fileUploadSpec.js index ecf29f979d8..0d2c9eb0697 100644 --- a/apps/files/tests/js/fileUploadSpec.js +++ b/apps/files/tests/js/fileUploadSpec.js @@ -24,6 +24,7 @@ describe('OC.Upload tests', function() { var testFile; var uploader; var failStub; + var progressBarStub; beforeEach(function() { testFile = { @@ -45,7 +46,8 @@ describe('OC.Upload tests', function() { '</div>' ); $dummyUploader = $('#file_upload_start'); - uploader = new OC.Uploader($dummyUploader); + progressBarStub = {on: function(){}}; + uploader = new OC.Uploader($dummyUploader, {progressBar: progressBarStub}); failStub = sinon.stub(); uploader.on('fail', failStub); }); @@ -143,6 +145,7 @@ describe('OC.Upload tests', function() { conflictDialogStub = sinon.stub(OC.dialogs, 'fileexists'); uploader = new OC.Uploader($dummyUploader, { + progressBar: progressBarStub, fileList: fileList }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index eae72ff05dc..402cfaa1073 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -522,7 +522,7 @@ describe('OCA.Files.FileList tests', function() { beforeEach(function() { deferredDelete = $.Deferred(); - deleteStub = sinon.stub(filesClient, 'remove').returns(deferredDelete.promise()); + deleteStub = sinon.stub(filesClient, 'remove'); }); afterEach(function() { deleteStub.restore(); @@ -530,92 +530,153 @@ describe('OCA.Files.FileList tests', function() { function doDelete() { // note: normally called from FileActions - fileList.do_delete(['One.txt', 'Two.jpg']); + return fileList.do_delete(['One.txt', 'Two.jpg']).then(function(){ - expect(deleteStub.calledTwice).toEqual(true); - expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); + expect(deleteStub.calledTwice).toEqual(true); + expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); + }); } - it('calls delete.php, removes the deleted entries and updates summary', function() { + it('calls delete.php, removes the deleted entries and updates summary', function(done) { var $summary; fileList.setFiles(testFiles); - doDelete(); + deferredDelete1 = $.Deferred(); + deferredDelete2 = $.Deferred(); + deleteStub.onCall(0).callsFake(function(src){ + expect(deleteStub.calledOnce).toEqual(true); + expect(src).toEqual('/subdir/One.txt'); + return deferredDelete1.promise(); + }); + deleteStub.onCall(1).callsFake(function(src){ + expect(deleteStub.calledTwice).toEqual(true); + expect(src).toEqual('/subdir/Two.jpg'); + return deferredDelete2.promise(); + }); - deferredDelete.resolve(200); + var promise = fileList.do_delete(['One.txt', 'Two.jpg']); + deferredDelete1.resolve(200); + deferredDelete2.resolve(200); + return promise.then(function(){ - expect(fileList.findFileEl('One.txt').length).toEqual(0); - expect(fileList.findFileEl('Two.jpg').length).toEqual(0); - expect(fileList.findFileEl('Three.pdf').length).toEqual(1); - expect(fileList.$fileList.find('tr').length).toEqual(2); - - $summary = $('#filestable .summary'); - expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.dirinfo').text()).toEqual('1 folder'); - expect($summary.find('.fileinfo').text()).toEqual('1 file'); - expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false); - expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); - expect($summary.find('.filesize').text()).toEqual('57 KB'); - expect(fileList.isEmpty).toEqual(false); - expect($('#filestable thead th').hasClass('hidden')).toEqual(false); - expect($('#emptycontent').hasClass('hidden')).toEqual(true); + expect(fileList.findFileEl('One.txt').length).toEqual(0); + expect(fileList.findFileEl('Two.jpg').length).toEqual(0); + expect(fileList.findFileEl('Three.pdf').length).toEqual(1); + expect(fileList.$fileList.find('tr').length).toEqual(2); + + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(false); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false); + expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); + expect($summary.find('.filesize').text()).toEqual('57 KB'); + expect(fileList.isEmpty).toEqual(false); + expect($('#filestable thead th').hasClass('hidden')).toEqual(false); + expect($('#emptycontent').hasClass('hidden')).toEqual(true); - expect(notificationStub.notCalled).toEqual(true); + expect(notificationStub.notCalled).toEqual(true); + done(); + }); }); - it('shows busy state on files to be deleted', function() { + it('shows busy state on files to be deleted', function(done) { fileList.setFiles(testFiles); - doDelete(); - - expect(fileList.findFileEl('One.txt').hasClass('busy')).toEqual(true); - expect(fileList.findFileEl('Three.pdf').hasClass('busy')).toEqual(false); + deferredDelete1 = $.Deferred(); + deferredDelete2 = $.Deferred(); + deleteStub.onCall(0).callsFake(function(src){ + expect(fileList.findFileEl('One.txt').hasClass('busy')).toEqual(true); + expect(fileList.findFileEl('Three.pdf').hasClass('busy')).toEqual(false); + + expect(deleteStub.calledOnce).toEqual(true); + expect(src).toEqual('/subdir/One.txt'); + return deferredDelete1.promise(); + }); + deleteStub.onCall(1).callsFake(function(src){ + expect(fileList.findFileEl('Two.jpg').hasClass('busy')).toEqual(true); + expect(fileList.findFileEl('Three.pdf').hasClass('busy')).toEqual(false); + + expect(deleteStub.calledTwice).toEqual(true); + expect(src).toEqual('/subdir/Two.jpg'); + return deferredDelete2.promise(); + }); + var promise = fileList.do_delete(['One.txt', 'Two.jpg']).then(function(){ + expect(deleteStub.calledTwice).toEqual(true); + }); + deferredDelete1.resolve(200); + deferredDelete2.resolve(200); + return promise.then(function(){ + expect(fileList.findFileEl('One.txt').hasClass('busy')).toEqual(false); + expect(fileList.findFileEl('Two.jpg').hasClass('busy')).toEqual(false); + done(); + }); }); - it('shows busy state on all files when deleting all', function() { + it('shows busy state on all files when deleting all', function(done) { fileList.setFiles(testFiles); - - fileList.do_delete(); - - expect(fileList.$fileList.find('tr.busy').length).toEqual(4); + var deferredDeleteArray = []; + var count = 0; + for (var i = 0; i < 4; i++) { + (function(i, fn){ + deferredDeleteArray.push($.Deferred()); + deleteStub.onCall(i).callsFake(function(src){ + expect(fileList.findFileEl(fn).hasClass('busy')).toEqual(true); + count++; + return deferredDeleteArray[i].promise(); + }); + })(i, testFiles[i].name); + } + var promise = fileList.do_delete(); + for (var i = 0; i < 4; i++) { + deferredDeleteArray[i].resolve(200); + } + return promise.then(function(){ + expect(count).toEqual(4); + done(); + }); }); - it('updates summary when deleting last file', function() { + it('updates summary when deleting last file', function(done) { var $summary; fileList.setFiles([testFiles[0], testFiles[1]]); - doDelete(); - + deleteStub.returns(deferredDelete.promise()); deferredDelete.resolve(200); - expect(fileList.$fileList.find('tr').length).toEqual(0); - - $summary = $('#filestable .summary'); - expect($summary.hasClass('hidden')).toEqual(true); - expect(fileList.isEmpty).toEqual(true); - expect(fileList.files.length).toEqual(0); - expect($('#filestable thead th').hasClass('hidden')).toEqual(true); - expect($('#emptycontent').hasClass('hidden')).toEqual(false); + return doDelete().then(function(){ + expect(fileList.$fileList.find('tr').length).toEqual(0); + $summary = $('#filestable .summary'); + expect($summary.hasClass('hidden')).toEqual(true); + expect(fileList.isEmpty).toEqual(true); + expect(fileList.files.length).toEqual(0); + expect($('#filestable thead th').hasClass('hidden')).toEqual(true); + expect($('#emptycontent').hasClass('hidden')).toEqual(false); + done(); + }); }); - it('bring back deleted item when delete call failed', function() { + it('bring back deleted item when delete call failed', function(done) { fileList.setFiles(testFiles); - doDelete(); - + deleteStub.returns(deferredDelete.promise()); + var promise = doDelete(); deferredDelete.reject(403); + return promise.then(function(){ + // files are still in the list + expect(fileList.findFileEl('One.txt').length).toEqual(1); + expect(fileList.findFileEl('Two.jpg').length).toEqual(1); + expect(fileList.$fileList.find('tr').length).toEqual(4); - // files are still in the list - expect(fileList.findFileEl('One.txt').length).toEqual(1); - expect(fileList.findFileEl('Two.jpg').length).toEqual(1); - expect(fileList.$fileList.find('tr').length).toEqual(4); - - expect(notificationStub.calledTwice).toEqual(true); + expect(notificationStub.calledTwice).toEqual(true); + done(); + }); }); - it('remove file from list if delete call returned 404 not found', function() { + it('remove file from list if delete call returned 404 not found', function(done) { fileList.setFiles(testFiles); - doDelete(); - + deleteStub.returns(deferredDelete.promise()); + var promise = doDelete(); deferredDelete.reject(404); + return promise.then(function(){ + expect(fileList.findFileEl('One.txt').length).toEqual(0); + expect(fileList.findFileEl('Two.jpg').length).toEqual(0); + expect(fileList.$fileList.find('tr').length).toEqual(2); - // files are still in the list - expect(fileList.findFileEl('One.txt').length).toEqual(0); - expect(fileList.findFileEl('Two.jpg').length).toEqual(0); - expect(fileList.$fileList.find('tr').length).toEqual(2); - - expect(notificationStub.notCalled).toEqual(true); + expect(notificationStub.notCalled).toEqual(true); + done(); + }); }); }); describe('Renaming files', function() { @@ -831,7 +892,7 @@ describe('OCA.Files.FileList tests', function() { beforeEach(function() { deferredMove = $.Deferred(); - moveStub = sinon.stub(filesClient, 'move').returns(deferredMove.promise()); + moveStub = sinon.stub(filesClient, 'move'); fileList.setFiles(testFiles); }); @@ -840,14 +901,18 @@ describe('OCA.Files.FileList tests', function() { }); it('Moves single file to target folder', function(done) { - return fileList.move('One.txt', '/somedir').then(function(){ + var promise = fileList.move('One.txt', '/somedir'); + moveStub.callsFake(function(src, dst){ expect(moveStub.calledOnce).toEqual(true); - expect(moveStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(moveStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); + expect(src).toEqual('/subdir/One.txt'); + expect(dst).toEqual('/somedir/One.txt'); + return deferredMove.promise(); + }); - deferredMove.resolve(201); + deferredMove.resolve(201); + return promise.then(function(){ expect(fileList.findFileEl('One.txt').length).toEqual(0); // folder size has increased @@ -861,26 +926,29 @@ describe('OCA.Files.FileList tests', function() { it('Moves list of files to target folder', function(done) { var deferredMove1 = $.Deferred(); var deferredMove2 = $.Deferred(); - moveStub.onCall(0).returns(deferredMove1.promise()); - moveStub.onCall(1).returns(deferredMove2.promise()); - - return fileList.move(['One.txt', 'Two.jpg'], '/somedir').then(function(){ - - expect(moveStub.calledTwice).toEqual(true); - expect(moveStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(moveStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); - expect(moveStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); - expect(moveStub.getCall(1).args[1]).toEqual('/somedir/Two.jpg'); - - deferredMove1.resolve(201); - + moveStub.onCall(0).callsFake(function(src, dst){ + expect(moveStub.calledOnce).toEqual(true); + expect(src).toEqual('/subdir/One.txt'); + expect(dst).toEqual('/somedir/One.txt'); + return deferredMove1.promise(); + }); + moveStub.onCall(1).callsFake(function(src, dst){ expect(fileList.findFileEl('One.txt').length).toEqual(0); // folder size has increased during move expect(fileList.findFileEl('somedir').data('size')).toEqual(262); expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); - deferredMove2.resolve(201); + expect(src).toEqual('/subdir/Two.jpg'); + expect(dst).toEqual('/somedir/Two.jpg'); + return deferredMove2.promise(); + }); + + var promise = fileList.move(['One.txt', 'Two.jpg'], '/somedir'); + deferredMove1.resolve(201); + deferredMove2.resolve(201); + return promise.then(function(){ + expect(moveStub.calledTwice).toEqual(true); expect(fileList.findFileEl('Two.jpg').length).toEqual(0); @@ -893,34 +961,32 @@ describe('OCA.Files.FileList tests', function() { }); }); it('Shows notification if a file could not be moved', function(done) { - return fileList.move('One.txt', '/somedir').then(function(){ - + moveStub.callsFake(function(){ expect(moveStub.calledOnce).toEqual(true); - - deferredMove.reject(409); - + return deferredMove.promise(); + }); + var promise = fileList.move('One.txt', '/somedir'); + deferredMove.reject(409); + return promise.then(function(){ expect(fileList.findFileEl('One.txt').length).toEqual(1); - expect(notificationStub.calledOnce).toEqual(true); expect(notificationStub.getCall(0).args[0]).toEqual('Could not move "One.txt"'); done(); }); }); it('Restores thumbnail if a file could not be moved', function(done) { - return fileList.move('One.txt', '/somedir').then(function(){ - + moveStub.callsFake(function(){ expect(fileList.findFileEl('One.txt').find('.thumbnail').parent().attr('class')) .toContain('icon-loading-small'); - expect(moveStub.calledOnce).toEqual(true); - - deferredMove.reject(409); - + return deferredMove.promise(); + }); + var promise = fileList.move('One.txt', '/somedir'); + deferredMove.reject(409); + return promise.then(function(){ expect(fileList.findFileEl('One.txt').length).toEqual(1); - expect(notificationStub.calledOnce).toEqual(true); expect(notificationStub.getCall(0).args[0]).toEqual('Could not move "One.txt"'); - expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) .toEqual(OC.imagePath('core', 'filetypes/text.svg')); done(); @@ -934,7 +1000,7 @@ describe('OCA.Files.FileList tests', function() { beforeEach(function() { deferredCopy = $.Deferred(); - copyStub = sinon.stub(filesClient, 'copy').returns(deferredCopy.promise()); + copyStub = sinon.stub(filesClient, 'copy'); fileList.setFiles(testFiles); }); @@ -942,86 +1008,100 @@ describe('OCA.Files.FileList tests', function() { copyStub.restore(); }); - it('Copies single file to target folder', function() { - fileList.copy('One.txt', '/somedir'); + it('Copies single file to target folder', function(done) { + copyStub.callsFake(function(){ + expect(copyStub.calledOnce).toEqual(true); + expect(copyStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(copyStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); - expect(copyStub.calledOnce).toEqual(true); - expect(copyStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(copyStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); + return deferredCopy.promise(); + }); + var promise = fileList.copy('One.txt', '/somedir'); deferredCopy.resolve(201); + return promise.then(function(){ + // File is still here + expect(fileList.findFileEl('One.txt').length).toEqual(1); - // File is still here - expect(fileList.findFileEl('One.txt').length).toEqual(1); - - // folder size has increased - expect(fileList.findFileEl('somedir').data('size')).toEqual(262); - expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); + // folder size has increased + expect(fileList.findFileEl('somedir').data('size')).toEqual(262); + expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); - // Copying sents a notification to tell that we've successfully copied file - expect(notificationStub.notCalled).toEqual(false); + // Copying sents a notification to tell that we've successfully copied file + expect(notificationStub.notCalled).toEqual(false); + done(); + }); }); - it('Copies list of files to target folder', function() { + it('Copies list of files to target folder', function(done) { var deferredCopy1 = $.Deferred(); var deferredCopy2 = $.Deferred(); - copyStub.onCall(0).returns(deferredCopy1.promise()); - copyStub.onCall(1).returns(deferredCopy2.promise()); - - fileList.copy(['One.txt', 'Two.jpg'], '/somedir'); + copyStub.onCall(0).callsFake(function(src, dst){ + expect(src).toEqual('/subdir/One.txt'); + expect(dst).toEqual('/somedir/One.txt'); + return deferredCopy1.promise(); + }); + copyStub.onCall(1).callsFake(function(src, dst){ + // folder size has increased during copy + expect(fileList.findFileEl('somedir').data('size')).toEqual(262); + expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); - expect(copyStub.calledTwice).toEqual(true); - expect(copyStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(copyStub.getCall(0).args[1]).toEqual('/somedir/One.txt'); - expect(copyStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); - expect(copyStub.getCall(1).args[1]).toEqual('/somedir/Two.jpg'); + expect(src).toEqual('/subdir/Two.jpg'); + expect(dst).toEqual('/somedir/Two.jpg'); + return deferredCopy2.promise(); + }); + var promise = fileList.copy(['One.txt', 'Two.jpg'], '/somedir'); deferredCopy1.resolve(201); - - expect(fileList.findFileEl('One.txt').length).toEqual(1); - - // folder size has increased during copy - expect(fileList.findFileEl('somedir').data('size')).toEqual(262); - expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B'); - deferredCopy2.resolve(201); - expect(fileList.findFileEl('Two.jpg').length).toEqual(1); + return promise.then(function(){ + expect(copyStub.calledTwice).toEqual(true); + expect(fileList.findFileEl('Two.jpg').length).toEqual(1); + expect(fileList.findFileEl('One.txt').length).toEqual(1); - // folder size has increased - expect(fileList.findFileEl('somedir').data('size')).toEqual(12311); - expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('12 KB'); + // folder size has increased + expect(fileList.findFileEl('somedir').data('size')).toEqual(12311); + expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('12 KB'); - expect(notificationStub.notCalled).toEqual(false); + expect(notificationStub.notCalled).toEqual(false); + done(); + }); }); - it('Shows notification if a file could not be copied', function() { - fileList.copy('One.txt', '/somedir'); - - expect(copyStub.calledOnce).toEqual(true); + it('Shows notification if a file could not be copied', function(done) { + copyStub.callsFake(function(){ + expect(copyStub.calledOnce).toEqual(true); + return deferredCopy.promise(); + }); + var promise = fileList.copy('One.txt', '/somedir'); deferredCopy.reject(409); - - expect(fileList.findFileEl('One.txt').length).toEqual(1); - - expect(notificationStub.calledOnce).toEqual(true); - expect(notificationStub.getCall(0).args[0]).toEqual('Could not copy "One.txt"'); + return promise.then(function(){ + expect(fileList.findFileEl('One.txt').length).toEqual(1); + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('Could not copy "One.txt"'); + done(); + }); }); - it('Restores thumbnail if a file could not be copied', function() { - fileList.copy('One.txt', '/somedir'); - - expect(fileList.findFileEl('One.txt').find('.thumbnail').parent().attr('class')) - .toContain('icon-loading-small'); - - expect(copyStub.calledOnce).toEqual(true); + it('Restores thumbnail if a file could not be copied', function(done) { + copyStub.callsFake(function(){ + expect(fileList.findFileEl('One.txt').find('.thumbnail').parent().attr('class')) + .toContain('icon-loading-small'); + expect(copyStub.calledOnce).toEqual(true); + return deferredCopy.promise(); + }); + var promise = fileList.copy('One.txt', '/somedir'); deferredCopy.reject(409); + return promise.then(function(){ + expect(fileList.findFileEl('One.txt').length).toEqual(1); - expect(fileList.findFileEl('One.txt').length).toEqual(1); - - expect(notificationStub.calledOnce).toEqual(true); - expect(notificationStub.getCall(0).args[0]).toEqual('Could not copy "One.txt"'); + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('Could not copy "One.txt"'); - expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) - .toEqual(OC.imagePath('core', 'filetypes/text.svg')); + expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) + .toEqual(OC.imagePath('core', 'filetypes/text.svg')); + done(); + }); }); }); @@ -2283,7 +2363,7 @@ describe('OCA.Files.FileList tests', function() { var deleteStub, deferredDelete; beforeEach(function() { deferredDelete = $.Deferred(); - deleteStub = sinon.stub(filesClient, 'remove').returns(deferredDelete.promise()); + deleteStub = sinon.stub(filesClient, 'remove'); fileList.$el.find('.actions-selected').click(); }); @@ -2292,34 +2372,54 @@ describe('OCA.Files.FileList tests', function() { deleteStub.restore(); }); - it('Deletes selected files when "Delete" clicked', function() { + it('Deletes selected files when "Delete" clicked', function(done) { + var deferred = $.Deferred(); + + deleteStub.returns(deferredDelete.promise()); + deleteStub.onCall(2).callsFake(function(){ + expect(deleteStub.callCount).toEqual(3); + expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Three.pdf'); + expect(deleteStub.getCall(2).args[0]).toEqual('/subdir/somedir'); + return deferredDelete.promise(); + }); + + stub = sinon.stub(fileList._operationProgressBar, 'hideProgressBar').callsFake(function(){ + expect(fileList.findFileEl('One.txt').length).toEqual(0); + expect(fileList.findFileEl('Three.pdf').length).toEqual(0); + expect(fileList.findFileEl('somedir').length).toEqual(0); + expect(fileList.findFileEl('Two.jpg').length).toEqual(1); + done(); + deferred.resolve(); + }); $('.selectedActions .filesSelectMenu .delete').click(); - - expect(deleteStub.callCount).toEqual(3); - expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Three.pdf'); - expect(deleteStub.getCall(2).args[0]).toEqual('/subdir/somedir'); - deferredDelete.resolve(204); + return deferred.promise(); - expect(fileList.findFileEl('One.txt').length).toEqual(0); - expect(fileList.findFileEl('Three.pdf').length).toEqual(0); - expect(fileList.findFileEl('somedir').length).toEqual(0); - expect(fileList.findFileEl('Two.jpg').length).toEqual(1); }); - it('Deletes all files when all selected when "Delete" clicked', function() { + it('Deletes all files when all selected when "Delete" clicked', function(done) { + var deferred = $.Deferred(); + + deleteStub.returns(deferredDelete.promise()); + deleteStub.onCall(3).callsFake(function(){ + expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); + expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); + expect(deleteStub.getCall(2).args[0]).toEqual('/subdir/Three.pdf'); + expect(deleteStub.getCall(3).args[0]).toEqual('/subdir/somedir'); + return deferredDelete.promise(); + }); + + stub = sinon.stub(fileList._operationProgressBar, 'hideProgressBar').callsFake(function(){ + expect(fileList.isEmpty).toEqual(true); + expect(deleteStub.callCount).toEqual(4); + done(); + deferred.resolve(); + }); $('.select-all').click(); $('.selectedActions .filesSelectMenu .delete').click(); - - expect(deleteStub.callCount).toEqual(4); - expect(deleteStub.getCall(0).args[0]).toEqual('/subdir/One.txt'); - expect(deleteStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg'); - expect(deleteStub.getCall(2).args[0]).toEqual('/subdir/Three.pdf'); - expect(deleteStub.getCall(3).args[0]).toEqual('/subdir/somedir'); - deferredDelete.resolve(204); - expect(fileList.isEmpty).toEqual(true); + return deferred.promise(); }); }); }); diff --git a/core/js/files/client.js b/core/js/files/client.js index 9de732b3bd4..2017becf87c 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -427,6 +427,9 @@ _getSabreException: function(response) { var result = {}; var xml = response.xhr.responseXML; + if (xml === null) { + return result; + } var messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message'); var exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception'); if (messages.length) { |