Fauxton: Implement bulk deletion for all-docs-listing Introduce a collection which keeps track of documents that will deleted using the CouchDB Bulk-update API.
The collection fires events, so the view is noticed. Closes COUCHDB-2153 Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/6a466086 Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/6a466086 Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/6a466086 Branch: refs/heads/1.6.x Commit: 6a46608604812346628e21fa75c99b2b2a095cc3 Parents: f5a25ea Author: Robert Kowalski <[email protected]> Authored: Fri May 2 21:39:21 2014 +0200 Committer: Robert Kowalski <[email protected]> Committed: Sat May 31 22:57:25 2014 +0200 ---------------------------------------------------------------------- src/fauxton/app/addons/documents/resources.js | 90 +++++++++- src/fauxton/app/addons/documents/routes.js | 3 +- .../documents/templates/all_docs_item.html | 4 +- .../documents/templates/all_docs_list.html | 6 +- .../app/addons/documents/tests/resourcesSpec.js | 83 ++++++++- src/fauxton/app/addons/documents/views.js | 178 ++++++++++++------- 6 files changed, 290 insertions(+), 74 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a466086/src/fauxton/app/addons/documents/resources.js ---------------------------------------------------------------------- diff --git a/src/fauxton/app/addons/documents/resources.js b/src/fauxton/app/addons/documents/resources.js index 21ee55f..21cdfdd 100644 --- a/src/fauxton/app/addons/documents/resources.js +++ b/src/fauxton/app/addons/documents/resources.js @@ -44,7 +44,7 @@ function(app, FauxtonAPI, PagingCollection) { }; })(); - + Documents.Doc = FauxtonAPI.Model.extend({ idAttribute: "_id", documentation: function(){ @@ -302,6 +302,94 @@ function(app, FauxtonAPI, PagingCollection) { }); + Documents.BulkDeleteDoc = FauxtonAPI.Model.extend({ + idAttribute: "_id" + }); + + Documents.BulkDeleteDocCollection = FauxtonAPI.Collection.extend({ + + model: Documents.BulkDeleteDoc, + + sync: function ()Â { + + }, + + initialize: function (models, options) { + this.databaseId = options.databaseId; + }, + + bulkDelete: function () { + var payload = this.createPayload(this.toJSON()), + that = this; + + $.ajax({ + type: 'POST', + url: app.host + '/' + this.databaseId + '/_bulk_docs', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(payload), + }) + .then(function (res) { + that.handleResponse(res); + }) + .fail(function () { + var ids = _.reduce(that.toArray(), function (acc, doc) { + acc.push(doc.id); + return acc; + }, []); + that.trigger('error', ids); + }); + }, + + handleResponse: function (res) { + var ids = _.reduce(res, function (ids, doc) { + if (doc.error) { + ids.errorIds.push(doc.id); + } + + if (doc.ok === true) { + ids.successIds.push(doc.id); + } + + return ids; + }, {errorIds: [], successIds: []}); + + this.removeDocuments(ids.successIds); + + if (ids.errorIds.length) { + this.trigger('error', ids.errorIds); + } + + this.trigger('updated'); + }, + + removeDocuments: function (ids) { + var reloadDesignDocs = false; + _.each(ids, function (id) { + if (/_design/.test(id)) { + reloadDesignDocs = true; + } + + this.remove(this.get(id)); + }, this); + + if (reloadDesignDocs) { + FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); + } + + this.trigger('removed', ids); + }, + + createPayload: function (documents) { + var documentList = documents; + + return { + docs: documentList + }; + } + }); + + Documents.AllDocs = PagingCollection.extend({ model: Documents.Doc, documentation: function(){ http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a466086/src/fauxton/app/addons/documents/routes.js ---------------------------------------------------------------------- diff --git a/src/fauxton/app/addons/documents/routes.js b/src/fauxton/app/addons/documents/routes.js index a24a3bd..6d67dae 100644 --- a/src/fauxton/app/addons/documents/routes.js +++ b/src/fauxton/app/addons/documents/routes.js @@ -224,7 +224,8 @@ function(app, FauxtonAPI, Documents, Databases) { database: this.data.database, collection: this.data.database.allDocs, docParams: docParams, - params: urlParams + params: urlParams, + bulkDeleteDocsCollection: new Documents.BulkDeleteDocCollection([], {databaseId: this.data.database.get('id')}) })); this.crumbs = [ http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a466086/src/fauxton/app/addons/documents/templates/all_docs_item.html ---------------------------------------------------------------------- diff --git a/src/fauxton/app/addons/documents/templates/all_docs_item.html b/src/fauxton/app/addons/documents/templates/all_docs_item.html index bfedaaa..a8ef20f 100644 --- a/src/fauxton/app/addons/documents/templates/all_docs_item.html +++ b/src/fauxton/app/addons/documents/templates/all_docs_item.html @@ -12,13 +12,13 @@ License for the specific language governing permissions and limitations under the License. --> -<td class="select"><input type="checkbox" class="row-select"></td> +<td class="select"><input <%- checked ? 'checked="checked"' : '' %> type="checkbox" class="js-row-select"></td> <td> <div> <pre class="prettyprint"><%- doc.prettyJSON() %></pre> <% if (doc.isEditable()) { %> <div class="btn-group"> - <a href="#<%= doc.url('web-index') %>" class="btn btn-small edits">Edit <%= doc.docType() %></a> + <a href="#<%= doc.url('web-index') %>" class="btn btn-small edits">Edit <%- doc.docType() %></a> <button href="#" class="btn btn-small btn-danger delete" title="Delete this document."><i class="icon icon-trash"></i></button> </div> <% } %> http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a466086/src/fauxton/app/addons/documents/templates/all_docs_list.html ---------------------------------------------------------------------- diff --git a/src/fauxton/app/addons/documents/templates/all_docs_list.html b/src/fauxton/app/addons/documents/templates/all_docs_list.html index a521ff9..a643427 100644 --- a/src/fauxton/app/addons/documents/templates/all_docs_list.html +++ b/src/fauxton/app/addons/documents/templates/all_docs_list.html @@ -17,7 +17,7 @@ the License. <div class="row"> <div class="btn-toolbar span6"> <button type="button" class="btn btn-small all" data-toggle="button">â All</button> - <button class="btn btn-small disabled bulk-delete"><i class="icon-trash"></i></button> + <button class="btn btn-small disabled js-bulk-delete"><i class="icon-trash"></i></button> <% if (expandDocs) { %> <button id="collapse" class="btn btn-small"><i class="icon-minus"></i> Collapse</button> <% } else { %> @@ -32,8 +32,8 @@ the License. <table class="all-docs table table-striped table-condensed"> <tbody></tbody> </table> - - <% if (endOfResults) { %> + + <% if (endOfResults) { %> <div class="text-center well"> <p class="muted"> End of results - <a id="js-end-results" href="#query" data-bypass="true" data-toggle="tab">edit query</a> http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a466086/src/fauxton/app/addons/documents/tests/resourcesSpec.js ---------------------------------------------------------------------- diff --git a/src/fauxton/app/addons/documents/tests/resourcesSpec.js b/src/fauxton/app/addons/documents/tests/resourcesSpec.js index e120582..1159360 100644 --- a/src/fauxton/app/addons/documents/tests/resourcesSpec.js +++ b/src/fauxton/app/addons/documents/tests/resourcesSpec.js @@ -10,8 +10,8 @@ // License for the specific language governing permissions and limitations under // the License. define([ - 'addons/documents/resources', - 'testUtils' + 'addons/documents/resources', + 'testUtils' ], function (Models, testUtils) { var assert = testUtils.assert; @@ -50,7 +50,6 @@ define([ }); }); - }); describe('QueryParams', function() { @@ -148,5 +147,81 @@ define([ }); }); }); -}); + describe('Bulk Delete', function () { + var databaseId = 'ente', + collection, + values; + + values = [{ + _id: '1', + _rev: '1234561', + _deleted: true + }, + { + _id: '2', + _rev: '1234562', + _deleted: true + }, + { + _id: '3', + _rev: '1234563', + _deleted: true + }]; + + beforeEach(function () { + collection = new Models.BulkDeleteDocCollection(values, { + databaseId: databaseId + }); + }); + + it("contains the models", function () { + collection = new Models.BulkDeleteDocCollection(values, { + databaseId: databaseId + }); + + assert.equal(collection.length, 3); + }); + + it("clears the memory if no errors happened", function () { + collection.handleResponse([ + {"ok":true,"id":"1","rev":"10-72cd2edbcc0d197ce96188a229a7af01"}, + {"ok":true,"id":"2","rev":"6-da537822b9672a4b2f42adb1be04a5b1"} + ]); + + assert.equal(collection.length, 1); + }); + + it("triggers a removed event with all ids", function () { + collection.listenToOnce(collection, 'removed', function (ids) { + assert.deepEqual(ids, ['Deferred', 'DeskSet']); + }); + + collection.handleResponse([ + {"ok":true,"id":"Deferred","rev":"10-72cd2edbcc0d197ce96188a229a7af01"}, + {"ok":true,"id":"DeskSet","rev":"6-da537822b9672a4b2f42adb1be04a5b1"} + ]); + }); + + it("triggers a error event with all errored ids", function () { + collection.listenToOnce(collection, 'error', function (ids) { + assert.deepEqual(ids, ['Deferred']); + }); + collection.handleResponse([ + {"error":"confclict","id":"Deferred","rev":"10-72cd2edbcc0d197ce96188a229a7af01"}, + {"ok":true,"id":"DeskSet","rev":"6-da537822b9672a4b2f42adb1be04a5b1"} + ]); + }); + + it("removes successfull deleted from the collection but keeps one with errors", function () { + collection.handleResponse([ + {"error":"confclict","id":"1","rev":"10-72cd2edbcc0d197ce96188a229a7af01"}, + {"ok":true,"id":"2","rev":"6-da537822b9672a4b2f42adb1be04a5b1"}, + {"error":"conflict","id":"3","rev":"6-da537822b9672a4b2f42adb1be04a5b1"} + ]); + assert.ok(collection.get('1')); + assert.ok(collection.get('3')); + assert.notOk(collection.get('2')); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb/blob/6a466086/src/fauxton/app/addons/documents/views.js ---------------------------------------------------------------------- diff --git a/src/fauxton/app/addons/documents/views.js b/src/fauxton/app/addons/documents/views.js index 87bc7ae..97f82b4 100644 --- a/src/fauxton/app/addons/documents/views.js +++ b/src/fauxton/app/addons/documents/views.js @@ -33,6 +33,15 @@ define([ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColumns, beautify, prettify, ZeroClipboard) { + + function showError (msg) { + FauxtonAPI.addNotification({ + msg: msg, + type: 'error', + clear: true + }); + } + var Views = {}; Views.SearchBox = FauxtonAPI.View.extend({ @@ -243,6 +252,10 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, tagName: "tr", className: "all-docs-item", + initialize: function (options) { + this.checked = options.checked; + }, + events: { "click button.delete": "destroy", "dblclick pre.prettyprint": "edit" @@ -256,7 +269,8 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, serialize: function() { return { - doc: this.model + doc: this.model, + checked: this.checked }; }, @@ -279,7 +293,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, this.model.destroy().then(function(resp) { FauxtonAPI.addNotification({ - msg: "Succesfully destroyed your doc", + msg: "Succesfully deleted your doc", clear: true }); that.$el.fadeOut(function () { @@ -292,7 +306,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, } }, function(resp) { FauxtonAPI.addNotification({ - msg: "Failed to destroy your doc!", + msg: "Failed to deleted your doc!", type: "error", clear: true }); @@ -499,29 +513,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, template: "addons/documents/templates/all_docs_list", events: { "click button.all": "selectAll", - "click button.bulk-delete": "bulkDelete", + "click button.js-bulk-delete": "bulkDelete", "click #collapse": "collapse", - "change .row-select":"toggleTrash", + "click .all-docs-item": "toggleDocument", "click #js-end-results": "scrollToQuery" }, - toggleTrash: function () { - if (this.$('.row-select:checked').length > 0) { - this.$('.bulk-delete').removeClass('disabled'); - } else { - this.$('.bulk-delete').addClass('disabled'); - } - }, - - scrollToQuery: function () { - $('#dashboard-content').animate({ scrollTop: 0 }, 'slow'); - }, - - initialize: function(options){ + initialize: function (options) { this.nestedView = options.nestedView || Views.Document; this.rows = {}; - this.viewList = !! options.viewList; - this.database = options.database; + this.viewList = !!options.viewList; if (options.ddocInfo) { this.designDocs = options.ddocInfo.designDocs; @@ -532,6 +533,74 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, this.params = options.params || {}; this.expandDocs = true; this.perPageDefault = this.docParams.limit || 20; + + // some doclists don't have an option to delete + if (!this.viewList) { + this.bulkDeleteDocsCollection = options.bulkDeleteDocsCollection; + } + }, + + removeDocuments: function (ids) { + _.each(ids, function (id) { + this.removeDocument(id); + }, this); + + this.pagination.updatePerPage(parseInt(this.$('#select-per-page :selected').val(), 10)); + FauxtonAPI.triggerRouteEvent('perPageChange', this.pagination.documentsLeftToFetch()); + }, + + removeDocument: function (id) { + var that = this; + + if (!this.rows[id]) { + return; + } + + this.rows[id].$el.fadeOut('slow', function () { + that.rows[id].remove(); + }); + }, + + showError: function (ids) { + if (ids) { + showError('Failed to delete: ' + ids.join(', ')); + return; + } + + showError('Failed to delete your doc!'); + }, + + toggleDocument: function (event) { + var $row = this.$(event.target).closest('tr'), + docId = $row.attr('data-id'), + db = this.database.get('id'), + rev = this.collection.get(docId).get('_rev'), + data = {_id: docId, _rev: rev, _deleted: true}; + + if (!$row.hasClass('js-to-delete'))Â { + this.bulkDeleteDocsCollection.add(data); + } else { + this.bulkDeleteDocsCollection.remove(this.bulkDeleteDocsCollection.get(docId)); + } + + $row.find('.js-row-select').prop('checked', !$row.hasClass('js-to-delete')); + $row.toggleClass('js-to-delete'); + + this.toggleTrash(); + }, + + toggleTrash: function () { + var $bulkdDeleteButton = this.$('.js-bulk-delete'); + + if (this.bulkDeleteDocsCollection.length > 0) { + $bulkdDeleteButton.removeClass('disabled'); + } else { + $bulkdDeleteButton.addClass('disabled'); + } + }, + + scrollToQuery: function () { + $('#dashboard-content').animate({ scrollTop: 0 }, 'slow'); }, establish: function() { @@ -551,7 +620,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, //now redirect back to alldocs FauxtonAPI.navigate(model.database.url("index") + "?limit=100"); - console.log("ERROR: ", arguments); } }); }, @@ -580,48 +648,17 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, this.render(); }, - /* - * TODO: this should be reconsidered - * This currently performs delete operations on the model level, - * when we could be using bulk docs with _deleted = true. Using - * individual models is cleaner from a backbone standpoint, but - * not from the couchdb api. - * Also, the delete method is naive and leaves the body intact, - * when we should switch the doc to only having id/rev/deleted. - */ bulkDelete: function() { - var that = this; - // yuck, data binding ftw? - var eles = this.$el.find("input.row-select:checked") - .parents("tr.all-docs-item") - .map(function(e) { return $(this).attr("data-id"); }) - .get(); + var that = this, + documentsLength = this.bulkDeleteDocsCollection.length, + msg; - if (eles.length === 0 || !window.confirm("Are you sure you want to delete these " + eles.length + " docs?")) { + msg = "Are you sure you want to delete these " + documentsLength + " docs?"; + if (documentsLength === 0 || !window.confirm(msg)) { return false; } - _.each(eles, function(ele) { - var model = this.collection.get(ele); - - model.destroy().then(function(resp) { - that.rows[ele].$el.fadeOut(function () { - $(this).remove(); - }); - - model.collection.remove(model.id); - if (!!model.id.match('_design')) { - FauxtonAPI.triggerRouteEvent('reloadDesignDocs'); - } - that.$('.bulk-delete').addClass('disabled'); - }, function(resp) { - FauxtonAPI.addNotification({ - msg: "Failed to destroy your doc!", - type: "error", - clear: true - }); - }); - }, this); + this.bulkDeleteDocsCollection.bulkDelete(); }, addPagination: function () { @@ -640,6 +677,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, }, beforeRender: function() { + var docs; if (!this.pagination) { this.addPagination(); @@ -658,11 +696,16 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, this.setView('#item-numbers', this.allDocsNumber); - var docs = this.expandDocs ? this.collection : this.collection.simple(); + docs = this.expandDocs ? this.collection : this.collection.simple(); docs.each(function(doc) { + var isChecked; + if (this.bulkDeleteDocsCollection) { + isChecked = this.bulkDeleteDocsCollection.get(doc.id); + } this.rows[doc.id] = this.insertView("table.all-docs tbody", new this.nestedView({ - model: doc + model: doc, + checked: isChecked })); }, this); }, @@ -683,8 +726,17 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, } }, - afterRender: function(){ + afterRender: function () { prettyPrint(); + + if (this.bulkDeleteDocsCollection) { + this.stopListening(this.bulkDeleteDocsCollection); + this.listenTo(this.bulkDeleteDocsCollection, 'error', this.showError); + this.listenTo(this.bulkDeleteDocsCollection, 'removed', this.removeDocuments); + this.listenTo(this.bulkDeleteDocsCollection, 'updated', this.toggleTrash); + } + + this.toggleTrash(); }, perPage: function () { @@ -731,13 +783,13 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, this.model.destroy().then(function(resp) { FauxtonAPI.addNotification({ - msg: "Succesfully destroyed your doc", + msg: "Succesfully deleted your doc", clear: true }); FauxtonAPI.navigate(database.url("index")); }, function(resp) { FauxtonAPI.addNotification({ - msg: "Failed to destroy your doc!", + msg: "Failed to delete your doc!", type: "error", clear: true });
