aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-06-08 15:37:29 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-06-08 15:59:25 +0200
commitf1f836fa24922996df0906bd1336940de205d5b6 (patch)
treedc359cfcabfa5a36b10d48320ad613e71bd1680e /server
parent56963334491068e70db0695c7fc36d9c40bfca9a (diff)
downloadsonarqube-f1f836fa24922996df0906bd1336940de205d5b6.tar.gz
sonarqube-f1f836fa24922996df0906bd1336940de205d5b6.zip
SONAR-6341 make it possible to bulk delete provisioned projects
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/bulk-delete-view.js31
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/header-view.js28
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/list-item-view.js18
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/project.js8
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/projects.js9
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/search-view.js52
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-bulk-delete.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-header.hbs1
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-list-item.hbs4
-rw-r--r--server/sonar-web/src/main/js/apps/provisioning/templates/provisioning-search.hbs6
-rw-r--r--server/sonar-web/src/main/less/init/icons.less1
-rw-r--r--server/sonar-web/src/test/js/provisioning-spec.js127
-rw-r--r--server/sonar-web/src/test/json/provisioning-spec/search-deleted.json13
13 files changed, 308 insertions, 3 deletions
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 @@
+<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>
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 @@
<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>
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 @@
<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>
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 @@
<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>
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
+}