From: Stas Vilchik Date: Mon, 8 Jun 2015 13:37:29 +0000 (+0200) Subject: SONAR-6341 make it possible to bulk delete provisioned projects X-Git-Tag: 5.2-RC1~1570 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=f1f836fa24922996df0906bd1336940de205d5b6;p=sonarqube.git SONAR-6341 make it possible to bulk delete provisioned projects --- diff --git a/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js b/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js new file mode 100644 index 00000000000..91d09a12ee7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js @@ -0,0 +1,31 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + return ModalForm.extend({ + template: Templates['provisioning-bulk-delete'], + + onFormSubmit: function (e) { + this._super(e); + this.sendRequest(); + }, + + sendRequest: function () { + var that = this, + selected = _.pluck(this.collection.where({ selected: true }), 'id'); + return this.collection.bulkDelete(selected, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.collection.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/provisioning/header-view.js b/server/sonar-web/src/main/js/apps/provisioning/header-view.js index 4be7c21196d..37165494eac 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/header-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/header-view.js @@ -1,13 +1,20 @@ define([ './create-view', + './bulk-delete-view', './templates' -], function (CreateView) { +], function (CreateView, BulkDeleteView) { return Marionette.ItemView.extend({ template: Templates['provisioning-header'], + collectionEvents: { + 'change:selected': 'toggleDeleteButton', + 'reset': 'toggleDeleteButton' + }, + events: { - 'click #provisioning-create': 'onCreateClick' + 'click #provisioning-create': 'onCreateClick', + 'click #provisioning-bulk-delete': 'onBulkDeleteClick' }, onCreateClick: function (e) { @@ -15,10 +22,27 @@ define([ this.createProject(); }, + onBulkDeleteClick: function (e) { + e.preventDefault(); + this.bulkDelete(); + }, + createProject: function () { new CreateView({ collection: this.collection }).render(); + }, + + bulkDelete: function () { + new BulkDeleteView({ + collection: this.collection + }).render(); + }, + + toggleDeleteButton: function () { + var selectedCount = this.collection.where({ selected: true }).length, + someSelected = selectedCount > 0; + this.$('#provisioning-bulk-delete').prop('disabled', !someSelected); } }); diff --git a/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js b/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js index 8e565d54981..2b03660b698 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/list-item-view.js @@ -8,7 +8,12 @@ define([ className: 'panel panel-vertical', template: Templates['provisioning-list-item'], + modelEvents: { + 'change:selected': 'onSelectedChange' + }, + events: { + 'click .js-toggle': 'onToggleClick', 'click .js-project-delete': 'onDeleteClick' }, @@ -21,11 +26,24 @@ define([ this.$('[data-toggle="tooltip"]').tooltip('destroy'); }, + onToggleClick: function (e) { + e.preventDefault(); + this.toggle(); + }, + onDeleteClick: function (e) { e.preventDefault(); this.deleteProject(); }, + onSelectedChange: function () { + this.$('.js-toggle').toggleClass('icon-checkbox-checked', this.model.get('selected')); + }, + + toggle: function () { + this.model.toggle(); + }, + deleteProject: function () { new DeleteView({ model: this.model }).render(); } diff --git a/server/sonar-web/src/main/js/apps/provisioning/project.js b/server/sonar-web/src/main/js/apps/provisioning/project.js index 00bc73dd150..fa34df605f2 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/project.js +++ b/server/sonar-web/src/main/js/apps/provisioning/project.js @@ -3,6 +3,10 @@ define(function () { return Backbone.Model.extend({ idAttribute: 'uuid', + defaults: { + selected: false + }, + urlRoot: function () { return baseUrl + '/api/projects'; }, @@ -24,6 +28,10 @@ define(function () { }); } return Backbone.ajax(opts); + }, + + toggle: function () { + this.set({ selected: !this.get('selected') }); } }); diff --git a/server/sonar-web/src/main/js/apps/provisioning/projects.js b/server/sonar-web/src/main/js/apps/provisioning/projects.js index a5c26347c59..05a59f822af 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/projects.js +++ b/server/sonar-web/src/main/js/apps/provisioning/projects.js @@ -33,6 +33,15 @@ define([ hasMore: function () { return this.total > this.p * this.ps; + }, + + bulkDelete: function (ids, options) { + var opts = _.extend({}, options, { + type: 'POST', + url: baseUrl + '/api/projects/bulk_delete', + data: { ids: ids.join() } + }); + return Backbone.ajax(opts); } }); diff --git a/server/sonar-web/src/main/js/apps/provisioning/search-view.js b/server/sonar-web/src/main/js/apps/provisioning/search-view.js index 519e3c44b2a..55c0fafc4fb 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/search-view.js +++ b/server/sonar-web/src/main/js/apps/provisioning/search-view.js @@ -5,7 +5,13 @@ define([ return Marionette.ItemView.extend({ template: Templates['provisioning-search'], + collectionEvents: { + 'change:selected': 'onSelectedChange', + 'reset': 'onSelectedChange' + }, + events: { + 'click .js-toggle-selection': 'onToggleSelectionClick', 'submit #provisioning-search-form': 'onFormSubmit', 'search #provisioning-search-query': 'debouncedOnKeyUp', 'keyup #provisioning-search-query': 'debouncedOnKeyUp' @@ -37,12 +43,58 @@ define([ this.searchRequest = this.search(q); }, + onSelectedChange: function () { + var projectsCount = this.collection.length, + selectedCount = this.collection.where({ selected: true }).length, + allSelected = projectsCount > 0 && projectsCount === selectedCount, + someSelected = !allSelected && selectedCount > 0; + this.$('.js-toggle-selection') + .toggleClass('icon-checkbox-checked', allSelected || someSelected) + .toggleClass('icon-checkbox-single', someSelected); + }, + + onToggleSelectionClick: function (e) { + e.preventDefault(); + this.toggleSelection(); + }, + + toggleSelection: function () { + var selectedCount = this.collection.where({ selected: true }).length, + someSelected = selectedCount > 0; + return someSelected ? this.selectNone() : this.selectAll(); + }, + + selectNone: function () { + this.collection.where({ selected: true }).forEach(function (project) { + project.set({ selected: false }); + }); + }, + + selectAll: function () { + this.collection.forEach(function (project) { + project.set({ selected: true }); + }); + }, + getQuery: function () { return this.$('#provisioning-search-query').val(); }, search: function (q) { + this.selectNone(); return this.collection.fetch({ reset: true, data: { q: q } }); + }, + + serializeData: function () { + var projectsCount = this.collection.length, + selectedCount = this.collection.where({ selected: true }).length, + allSelected = projectsCount > 0 && projectsCount === selectedCount, + someSelected = !allSelected && selectedCount > 0; + return _.extend(this._super(), { + selectedCount: selectedCount, + allSelected: allSelected, + someSelected: someSelected + }); } }); diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-bulk-delete.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-bulk-delete.hbs new file mode 100644 index 00000000000..571faee78cf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-bulk-delete.hbs @@ -0,0 +1,13 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs index 3daa8f061e3..bb4986cd9c0 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs +++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs @@ -3,6 +3,7 @@
+

{{t 'provisioning.page.description'}}

diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs index 6d2694ea8a6..9c852f2f0ff 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs @@ -2,6 +2,10 @@ +
+ +
+
{{name}} {{key}} diff --git a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs index 3911130a30e..d22c7c92f6d 100644 --- a/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs +++ b/server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs @@ -1,5 +1,9 @@
- diff --git a/server/sonar-web/src/main/less/init/icons.less b/server/sonar-web/src/main/less/init/icons.less index a06378e485c..0d8a7d68c27 100644 --- a/server/sonar-web/src/main/less/init/icons.less +++ b/server/sonar-web/src/main/less/init/icons.less @@ -194,6 +194,7 @@ a[class^="icon-"], a[class*=" icon-"] { .square(16px); background-size: 16px 16px; background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDxnPgogIDx0aXRsZT5MYXllciAxPC90aXRsZT4KICA8ZyBpZD0ic3ZnXzEiPgogICA8cGF0aCBpZD0ic3ZnXzIiIGQ9Im03NDkuNzE1MDI3LDE4Mi44NTY5OTVsLTQ3NS40Mjg5ODYsMGMtMjUuMTQzMDA1LDAgLTQ2LjY2Niw4Ljk1MTk5NiAtNjQuNTcwOTk5LDI2Ljg1Njk5NXMtMjYuODU2OTk1LDM5LjQyNzk5NCAtMjYuODU2OTk1LDY0LjU3MDAwN2wwLDQ3NS40Mjg5NTVjMCwyNS4xNDMwMDUgOC45NTE5OTYsNDYuNjY3MDUzIDI2Ljg1Njk5NSw2NC41NzEwNDVzMzkuNDI3OTk0LDI2Ljg1Njk5NSA2NC41NzA5OTksMjYuODU2OTk1bDQ3NS40Mjg5ODYsMGMyNS4xNDMwMDUsMCA0Ni42NjYwMTYsLTguOTUzMDAzIDY0LjU3MjAyMSwtMjYuODU2OTk1YzE3LjkwMzk5MiwtMTcuOTAzOTkyIDI2Ljg1NDk4LC0zOS40MjkwMTYgMjYuODU0OTgsLTY0LjU3MTA0NWwwLC00NzUuNDI3OTc5YzAsLTI1LjE0MzAyMSAtOC45NTA5ODksLTQ2LjY2NiAtMjYuODU0OTgsLTY0LjU3MDAwN2MtMTcuOTA2MDA2LC0xNy45MDQ5OTkgLTM5LjQyOTk5MywtMjYuODU4MDAyIC02NC41NzIwMjEsLTI2Ljg1ODAwMmwwLDBsMCwwLjAwMDAzMXptMTY0LjU3MTk2LDkxLjQyODAwOWwwLDQ3NS40Mjg5ODZjMCw0NS4zMzM5ODQgLTE2LjA5NjAwOCw4NC4wOTYwMDggLTQ4LjI4Njk4NywxMTYuMjg2MDExYy0zMi4xODkwMjYsMzIuMTkwOTc5IC03MC45NTMwMDMsNDguMjg2MDExIC0xMTYuMjg1MDM0LDQ4LjI4NjAxMWwtNDc1LjQyOTk2MiwwYy00NS4zMzMwMDgsMCAtODQuMDk1MDE2LC0xNi4wOTUwMzIgLTExNi4yODYwMTEsLTQ4LjI4NjAxMXMtNDguMjg1OTk1LC03MC45NTIwMjYgLTQ4LjI4NTk5NSwtMTE2LjI4NjAxMWwwLC00NzUuNDI4OTg2YzAsLTQ1LjMzMzAwOCAxNi4wOTUwMDEsLTg0LjA5NTAwMSA0OC4yODU5OTUsLTExNi4yODUwMDRjMzIuMTkwMDAyLC0zMi4xOTEwMDIgNzAuOTUzMDAzLC00OC4yODYwMDMgMTE2LjI4NjAxMSwtNDguMjg2MDAzbDQ3NS40Mjk5NjIsMGM0NS4zMzIwMzEsMCA4NC4wOTYwMDgsMTYuMDk1MDAxIDExNi4yODUwMzQsNDguMjg2MDAzYzMyLjE5MDk3OSwzMi4xOTAwMDIgNDguMjg2OTg3LDcwLjk1MTk5NiA0OC4yODY5ODcsMTE2LjI4NTAwNGwwLDB6IiBmaWxsPSIjNzc3Nzc3Ii8+CiAgPC9nPgogPC9nPgo8L3N2Zz4=); + .trans(none); } .icon-checkbox-checked { diff --git a/server/sonar-web/src/test/js/provisioning-spec.js b/server/sonar-web/src/test/js/provisioning-spec.js index 6a1b6b6abb2..8c197cd8c20 100644 --- a/server/sonar-web/src/test/js/provisioning-spec.js +++ b/server/sonar-web/src/test/js/provisioning-spec.js @@ -254,3 +254,130 @@ casper.test.begin(testName('Delete'), 1, function (test) { }); }); + +casper.test.begin(testName('Selection'), 22, function (test) { + casper + .start(lib.buildUrl('provisioning'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/projects/provisioned', 'search.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/provisioning/app'], function (App) { + App.start({ el: '#provisioning' }); + }); + }); + }) + + .then(function () { + casper.waitForSelector('#provisioning-list li'); + }) + + .then(function () { + test.assertExists('.js-toggle-selection'); + test.assertDoesntExist('.js-toggle-selection.icon-checkbox-checked'); + test.assertElementCount('.js-toggle', 3); + test.assertDoesntExist('.js-toggle.icon-checkbox-checked'); + test.assertExists('#provisioning-bulk-delete[disabled]'); + }) + + .then(function () { + casper.click('#provisioning-list [data-id="id-sonarqube"] .js-toggle'); + + test.assertExists('.js-toggle-selection.icon-checkbox-checked.icon-checkbox-single'); + test.assertExists('#provisioning-list [data-id="id-sonarqube"] .js-toggle.icon-checkbox-checked'); + test.assertExists('#provisioning-bulk-delete'); + test.assertDoesntExist('#provisioning-bulk-delete[disabled]'); + }) + + .then(function () { + casper.click('#provisioning-list [data-id="id-javascript"] .js-toggle'); + casper.click('#provisioning-list [data-id="id-sonarqube-release"] .js-toggle'); + + test.assertDoesntExist('.js-toggle-selection.icon-checkbox-checked.icon-checkbox-single'); + test.assertExists('.js-toggle-selection.icon-checkbox-checked'); + test.assertExists('#provisioning-bulk-delete'); + test.assertDoesntExist('#provisioning-bulk-delete[disabled]'); + }) + + .then(function () { + casper.click('.js-toggle-selection'); + + test.assertDoesntExist('.js-toggle-selection.icon-checkbox-checked'); + test.assertElementCount('.js-toggle', 3); + test.assertDoesntExist('.js-toggle.icon-checkbox-checked'); + test.assertExists('#provisioning-bulk-delete[disabled]'); + }) + + .then(function () { + casper.click('.js-toggle-selection'); + + test.assertDoesntExist('.js-toggle-selection.icon-checkbox-checked.icon-checkbox-single'); + test.assertExists('.js-toggle-selection.icon-checkbox-checked'); + test.assertElementCount('.js-toggle.icon-checkbox-checked', 3); + test.assertExists('#provisioning-bulk-delete'); + test.assertDoesntExist('#provisioning-bulk-delete[disabled]'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Bulk Delete'), 1, function (test) { + casper + .start(lib.buildUrl('provisioning'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/projects/provisioned', 'search.json'); + lib.mockRequestFromFile('/api/projects/bulk_delete', 'delete-error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/provisioning/app'], function (App) { + App.start({ el: '#provisioning' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForSelector('#provisioning-list li'); + }) + + .then(function () { + casper.click('#provisioning-list [data-id="id-sonarqube"] .js-toggle'); + casper.click('#provisioning-list [data-id="id-sonarqube-release"] .js-toggle'); + casper.click('#provisioning-bulk-delete'); + casper.waitForSelector('#bulk-delete-projects-form'); + }) + + .then(function () { + casper.click('#bulk-delete-projects-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMocks(); + lib.mockRequestFromFile('/api/projects/provisioned', 'search-deleted.json'); + lib.mockRequest('/api/projects/bulk_delete', '{}', { data: { ids: 'id-sonarqube,id-sonarqube-release' } }); + casper.click('#bulk-delete-projects-submit'); + casper.waitWhileSelector('[data-id="id-sonarqube"]'); + }) + + .then(function () { + test.assert(true); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); diff --git a/server/sonar-web/src/test/json/provisioning-spec/search-deleted.json b/server/sonar-web/src/test/json/provisioning-spec/search-deleted.json new file mode 100644 index 00000000000..85e5bca139e --- /dev/null +++ b/server/sonar-web/src/test/json/provisioning-spec/search-deleted.json @@ -0,0 +1,13 @@ +{ + "projects": [ + { + "uuid": "id-javascript", + "key": "javascript", + "name": "JavaScript", + "creationDate": "2015-05-25T16:49:41+0200" + } + ], + "total": 1, + "p": 1, + "ps": 100 +}