]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6341 make it possible to bulk delete provisioned projects
authorStas Vilchik <vilchiks@gmail.com>
Mon, 8 Jun 2015 13:37:29 +0000 (15:37 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 8 Jun 2015 13:59:25 +0000 (15:59 +0200)
13 files changed:
server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/header-view.js
server/sonar-web/src/main/js/apps/provisioning/list-item-view.js
server/sonar-web/src/main/js/apps/provisioning/project.js
server/sonar-web/src/main/js/apps/provisioning/projects.js
server/sonar-web/src/main/js/apps/provisioning/search-view.js
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-bulk-delete.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs
server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs
server/sonar-web/src/main/less/init/icons.less
server/sonar-web/src/test/js/provisioning-spec.js
server/sonar-web/src/test/json/provisioning-spec/search-deleted.json [new file with mode: 0644]

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 (file)
index 0000000..91d09a1
--- /dev/null
@@ -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);
+      });
+    }
+  });
+
+});
index 4be7c21196dd55848b25fbcae9c113ceccf89375..37165494eac0b3d2d91ce785ddc69b30478143e8 100644 (file)
@@ -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);
     }
   });
 
index 8e565d54981b320ce7ad89139c28ad2798e47ede..2b03660b698c24c61e486587aa3dc8e868a73e7e 100644 (file)
@@ -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();
     }
index 00bc73dd1507fe485429158d5dc4a93be287de90..fa34df605f27e73473ae50bd74fdb082a6284c2c 100644 (file)
@@ -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') });
     }
   });
 
index a5c26347c59b8ca76f496500a6cd59308f9a588f..05a59f822affb48687cc07ed0c8882e553ee829f 100644 (file)
@@ -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);
     }
 
   });
index 519e3c44b2a8eaaed163d4902a52317fcd141f0e..55c0fafc4fb3076c39c4bd82d90dd3d0c1b797f3 100644 (file)
@@ -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 (file)
index 0000000..571faee
--- /dev/null
@@ -0,0 +1,13 @@
+<form id="bulk-delete-projects-form">
+  <div class="modal-head">
+    <h2>Delete Projects</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    Are you sure you want to delete selected projects?
+  </div>
+  <div class="modal-foot">
+    <button id="bulk-delete-projects-submit" class="button-red">Delete</button>
+    <a href="#" class="js-modal-close" id="bulk-delete-projects-cancel">Cancel</a>
+  </div>
+</form>
index 3daa8f061e3efcc4ded39675d36034cbefdcf9e2..bb4986cd9c04d861ca811f4db145b4c29bd2b692 100644 (file)
@@ -3,6 +3,7 @@
   <div class="page-actions">
     <div class="button-group">
       <button id="provisioning-create">Create Project</button>
+      <button class="button-red" id="provisioning-bulk-delete" disabled>Delete Projects</button>
     </div>
   </div>
   <p class="page-description">{{t 'provisioning.page.description'}}</p>
index 6d2694ea8a6219e28bac9a74dd9cdfb847aaf88e..9c852f2f0ff2d033a119d7d7040daf6ca958c8a2 100644 (file)
@@ -2,6 +2,10 @@
   <a class="js-project-delete icon-delete" title="Delete" data-toggle="tooltip" href="#"></a>
 </div>
 
+<div class="pull-left big-spacer-right">
+  <a class="js-toggle icon-checkbox {{#if selected}}icon-checkbox-checked{{/if}}" href="#"></a>
+</div>
+
 <div class="display-inline-block text-top width-30">
   <a class="js-project-name" href="{{dashboardUrl key}}">{{name}}</a>
   <span class="js-project-key note little-spacer-left">{{key}}</span>
index 3911130a30e7ff5dcb9eaecd1181a982c3e4c11b..d22c7c92f6d2a8bcc00922f6f11445dc15aebf0d 100644 (file)
@@ -1,5 +1,9 @@
 <div class="panel panel-vertical bordered-bottom spacer-bottom">
-  <form id="provisioning-search-form" class="search-box">
+  <span class="big-spacer-right text-middle">
+    <a class="js-toggle-selection icon-checkbox {{#if allSelected}}icon-checkbox-checked{{/if}} {{#if someSelected}}icon-checkbox-checked icon-checkbox-single{{/if}}" href="#"></a>
+  </span>
+
+  <form id="provisioning-search-form" class="search-box display-inline-block text-middle">
     <button id="provisioning-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
     <input id="provisioning-search-query" class="search-box-input" type="search" name="q" placeholder="Search" maxlength="100">
   </form>
index a06378e485ce74986f06f95edc040aaa3272387b..0d8a7d68c27676f4bef8ca369c324bfe0900583f 100644 (file)
@@ -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 {
index 6a1b6b6abb2d3a9025ffb10f95ac045901235264..8c197cd8c20aeff2b90d0a93a85ac90e227c36a5 100644 (file)
@@ -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 (file)
index 0000000..85e5bca
--- /dev/null
@@ -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
+}