From c04875f418197895a512cc77026327c3da99d1d2 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 2 Jun 2015 13:53:17 +0200 Subject: [PATCH] SONAR-6602 refactor groups page --- server/sonar-web/Gruntfile.coffee | 7 + .../sonar-web/src/main/js/apps/groups/app.js | 47 +++ .../src/main/js/apps/groups/create-view.js | 30 ++ .../src/main/js/apps/groups/delete-view.js | 32 ++ .../src/main/js/apps/groups/form-view.js | 25 ++ .../src/main/js/apps/groups/group.js | 35 ++ .../src/main/js/apps/groups/groups.js | 40 ++ .../src/main/js/apps/groups/header-view.js | 25 ++ .../src/main/js/apps/groups/layout.js | 16 + .../main/js/apps/groups/list-footer-view.js | 34 ++ .../src/main/js/apps/groups/list-item-view.js | 59 +++ .../src/main/js/apps/groups/list-view.js | 11 + .../src/main/js/apps/groups/search-view.js | 49 +++ .../apps/groups/templates/groups-delete.hbs | 13 + .../js/apps/groups/templates/groups-form.hbs | 24 ++ .../apps/groups/templates/groups-header.hbs | 9 + .../apps/groups/templates/groups-layout.hbs | 6 + .../groups/templates/groups-list-footer.hbs | 6 + .../groups/templates/groups-list-item.hbs | 16 + .../apps/groups/templates/groups-search.hbs | 6 + .../js/apps/groups/templates/groups-users.hbs | 10 + .../src/main/js/apps/groups/update-view.js | 29 ++ .../src/main/js/apps/groups/users-view.js | 36 ++ .../src/main/js/apps/users/groups-view.js | 13 +- .../src/main/less/components/panels.less | 2 +- .../app/controllers/groups_controller.rb | 7 +- .../WEB-INF/app/views/groups/index.html.erb | 71 +--- server/sonar-web/src/test/js/groups-spec.js | 355 ++++++++++++++++++ .../src/test/json/groups-spec/error.json | 7 + .../test/json/groups-spec/search-big-1.json | 13 + .../test/json/groups-spec/search-big-2.json | 13 + .../test/json/groups-spec/search-created.json | 25 ++ .../json/groups-spec/search-filtered.json | 13 + .../test/json/groups-spec/search-updated.json | 19 + .../src/test/json/groups-spec/search.json | 19 + .../src/test/json/groups-spec/users.json | 17 + server/sonar-web/src/test/views/groups.jade | 5 + 37 files changed, 1066 insertions(+), 78 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/groups/app.js create mode 100644 server/sonar-web/src/main/js/apps/groups/create-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/delete-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/form-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/group.js create mode 100644 server/sonar-web/src/main/js/apps/groups/groups.js create mode 100644 server/sonar-web/src/main/js/apps/groups/header-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/layout.js create mode 100644 server/sonar-web/src/main/js/apps/groups/list-footer-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/list-item-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/list-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/search-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs create mode 100644 server/sonar-web/src/main/js/apps/groups/update-view.js create mode 100644 server/sonar-web/src/main/js/apps/groups/users-view.js create mode 100644 server/sonar-web/src/test/js/groups-spec.js create mode 100644 server/sonar-web/src/test/json/groups-spec/error.json create mode 100644 server/sonar-web/src/test/json/groups-spec/search-big-1.json create mode 100644 server/sonar-web/src/test/json/groups-spec/search-big-2.json create mode 100644 server/sonar-web/src/test/json/groups-spec/search-created.json create mode 100644 server/sonar-web/src/test/json/groups-spec/search-filtered.json create mode 100644 server/sonar-web/src/test/json/groups-spec/search-updated.json create mode 100644 server/sonar-web/src/test/json/groups-spec/search.json create mode 100644 server/sonar-web/src/test/json/groups-spec/users.json create mode 100644 server/sonar-web/src/test/views/groups.jade diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index ff402d13cb0..fc55da5fceb 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -123,6 +123,7 @@ module.exports = (grunt) -> 'build-app:coding-rules' 'build-app:computation' 'build-app:drilldown' + 'build-app:groups' 'build-app:markdown' 'build-app:measures' 'build-app:nav' @@ -156,6 +157,7 @@ module.exports = (grunt) -> 'casper:ui' 'casper:workspace' 'casper:users' + 'casper:groups' 'casper:provisioning' 'casper:computation' ] @@ -215,6 +217,9 @@ module.exports = (grunt) -> '<%= BUILD_PATH %>/js/apps/users/templates.js': [ '<%= SOURCE_PATH %>/js/apps/users/templates/**/*.hbs' ] + '<%= BUILD_PATH %>/js/apps/groups/templates.js': [ + '<%= SOURCE_PATH %>/js/apps/groups/templates/**/*.hbs' + ] '<%= BUILD_PATH %>/js/apps/provisioning/templates.js': [ '<%= SOURCE_PATH %>/js/apps/provisioning/templates/**/*.hbs' ] @@ -319,6 +324,8 @@ module.exports = (grunt) -> src: ['src/test/js/provisioning*.js'] computation: src: ['src/test/js/computation*.js'] + groups: + src: ['src/test/js/groups-spec.js'] uglify: build: diff --git a/server/sonar-web/src/main/js/apps/groups/app.js b/server/sonar-web/src/main/js/apps/groups/app.js new file mode 100644 index 00000000000..55c6dfef534 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/app.js @@ -0,0 +1,47 @@ +define([ + './layout', + './groups', + './header-view', + './search-view', + './list-view', + './list-footer-view' +], function (Layout, Groups, HeaderView, SearchView, ListView, ListFooterView) { + + var App = new Marionette.Application(), + init = function (options) { + // Layout + this.layout = new Layout({ el: options.el }); + this.layout.render(); + + // Collection + this.groups = new Groups(); + + // Header View + this.headerView = new HeaderView({ collection: this.groups }); + this.layout.headerRegion.show(this.headerView); + + // Search View + this.searchView = new SearchView({ collection: this.groups }); + this.layout.searchRegion.show(this.searchView); + + // List View + this.listView = new ListView({ collection: this.groups }); + this.layout.listRegion.show(this.listView); + + // List Footer View + this.listFooterView = new ListFooterView({ collection: this.groups }); + this.layout.listFooterRegion.show(this.listFooterView); + + // Go! + this.groups.fetch(); + }; + + App.on('start', function (options) { + window.requestMessages().done(function () { + init.call(App, options); + }); + }); + + return App; + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/create-view.js b/server/sonar-web/src/main/js/apps/groups/create-view.js new file mode 100644 index 00000000000..8d5cfce55aa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/create-view.js @@ -0,0 +1,30 @@ +define([ + './group', + './form-view' +], function (Group, FormView) { + + return FormView.extend({ + + sendRequest: function () { + var that = this, + group = new Group({ + name: this.$('#create-group-name').val(), + description: this.$('#create-group-description').val() + }); + this.disableForm(); + return group.save(null, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.collection.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.enableForm(); + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/delete-view.js b/server/sonar-web/src/main/js/apps/groups/delete-view.js new file mode 100644 index 00000000000..8fd83d34031 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/delete-view.js @@ -0,0 +1,32 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + return ModalForm.extend({ + template: Templates['groups-delete'], + + onFormSubmit: function () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.sendRequest(); + }, + + sendRequest: function () { + var that = this, + collection = this.model.collection; + return this.model.destroy({ + wait: true, + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + collection.total--; + that.close(); + }).fail(function (jqXHR) { + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/form-view.js b/server/sonar-web/src/main/js/apps/groups/form-view.js new file mode 100644 index 00000000000..aa872784e1e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/form-view.js @@ -0,0 +1,25 @@ +define([ + 'components/common/modal-form', + './templates' +], function (ModalForm) { + + return ModalForm.extend({ + template: Templates['groups-form'], + + onRender: function () { + ModalForm.prototype.onRender.apply(this, arguments); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onClose: function () { + ModalForm.prototype.onClose.apply(this, arguments); + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onFormSubmit: function () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + this.sendRequest(); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/group.js b/server/sonar-web/src/main/js/apps/groups/group.js new file mode 100644 index 00000000000..406f9ba3a3a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/group.js @@ -0,0 +1,35 @@ +define(function () { + + return Backbone.Model.extend({ + urlRoot: function () { + return baseUrl + '/api/usergroups'; + }, + + sync: function (method, model, options) { + var opts = options || {}; + if (method === 'create') { + _.defaults(opts, { + url: this.urlRoot() + '/create', + type: 'POST', + data: _.pick(model.toJSON(), 'name', 'description') + }); + } + if (method === 'update') { + _.defaults(opts, { + url: this.urlRoot() + '/update', + type: 'POST', + data: _.pick(model.toJSON(), 'id', 'name', 'description') + }); + } + if (method === 'delete') { + _.defaults(opts, { + url: this.urlRoot() + '/delete', + type: 'POST', + data: { id: this.id } + }); + } + return Backbone.ajax(opts); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/groups.js b/server/sonar-web/src/main/js/apps/groups/groups.js new file mode 100644 index 00000000000..9ddd6c9f9a1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/groups.js @@ -0,0 +1,40 @@ +define([ + './group' +], function (Group) { + + return Backbone.Collection.extend({ + model: Group, + + url: function () { + return baseUrl + '/api/usergroups/search'; + }, + + parse: function (r) { + this.total = +r.total; + this.p = +r.p; + this.ps = +r.ps; + return r.groups; + }, + + fetch: function (options) { + var d = (options && options.data) || {}; + this.q = d.q; + return Backbone.Collection.prototype.fetch.apply(this, arguments); + }, + + fetchMore: function () { + var p = this.p + 1; + return this.fetch({ add: true, remove: false, data: { p: p, ps: this.ps, q: this.q } }); + }, + + refresh: function () { + return this.fetch({ reset: true, data: { q: this.q } }); + }, + + hasMore: function () { + return this.total > this.p * this.ps; + } + + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/header-view.js b/server/sonar-web/src/main/js/apps/groups/header-view.js new file mode 100644 index 00000000000..da6f7f60919 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/header-view.js @@ -0,0 +1,25 @@ +define([ + './create-view', + './templates' +], function (CreateView) { + + return Marionette.ItemView.extend({ + template: Templates['groups-header'], + + events: { + 'click #groups-create': 'onCreateClick' + }, + + onCreateClick: function (e) { + e.preventDefault(); + this.createGroup(); + }, + + createGroup: function () { + new CreateView({ + collection: this.collection + }).render(); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/layout.js b/server/sonar-web/src/main/js/apps/groups/layout.js new file mode 100644 index 00000000000..a60fb06f35f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/layout.js @@ -0,0 +1,16 @@ +define([ + './templates' +], function () { + + return Marionette.Layout.extend({ + template: Templates['groups-layout'], + + regions: { + headerRegion: '#groups-header', + searchRegion: '#groups-search', + listRegion: '#groups-list', + listFooterRegion: '#groups-list-footer' + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/list-footer-view.js b/server/sonar-web/src/main/js/apps/groups/list-footer-view.js new file mode 100644 index 00000000000..cdad034f24a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/list-footer-view.js @@ -0,0 +1,34 @@ +define([ + './templates' +], function () { + + return Marionette.ItemView.extend({ + template: Templates['groups-list-footer'], + + collectionEvents: { + 'all': 'render' + }, + + events: { + 'click #groups-fetch-more': 'onMoreClick' + }, + + onMoreClick: function (e) { + e.preventDefault(); + this.fetchMore(); + }, + + fetchMore: function () { + this.collection.fetchMore(); + }, + + serializeData: function () { + return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), { + total: this.collection.total, + count: this.collection.length, + more: this.collection.hasMore() + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/list-item-view.js b/server/sonar-web/src/main/js/apps/groups/list-item-view.js new file mode 100644 index 00000000000..43eaa5b0d24 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/list-item-view.js @@ -0,0 +1,59 @@ +define([ + './update-view', + './delete-view', + './users-view', + './templates' +], function (UpdateView, DeleteView, UsersView) { + + return Marionette.ItemView.extend({ + tagName: 'li', + className: 'panel panel-vertical', + template: Templates['groups-list-item'], + + events: { + 'click .js-group-update': 'onUpdateClick', + 'click .js-group-delete': 'onDeleteClick', + 'click .js-group-users': 'onUsersClick' + }, + + onRender: function () { + this.$el.attr('data-id', this.model.id); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onClose: function () { + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onUpdateClick: function (e) { + e.preventDefault(); + this.updateGroup(); + }, + + onDeleteClick: function (e) { + e.preventDefault(); + this.deleteGroup(); + }, + + onUsersClick: function (e) { + e.preventDefault(); + this.showUsers(); + }, + + updateGroup: function () { + new UpdateView({ + model: this.model, + collection: this.model.collection + }).render(); + }, + + deleteGroup: function () { + new DeleteView({ model: this.model }).render(); + }, + + showUsers: function () { + new UsersView({ model: this.model }).render(); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/list-view.js b/server/sonar-web/src/main/js/apps/groups/list-view.js new file mode 100644 index 00000000000..138c36b7619 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/list-view.js @@ -0,0 +1,11 @@ +define([ + './list-item-view', + './templates' +], function (ListItemView) { + + return Marionette.CollectionView.extend({ + tagName: 'ul', + itemView: ListItemView + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/search-view.js b/server/sonar-web/src/main/js/apps/groups/search-view.js new file mode 100644 index 00000000000..1540d7eb36e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/search-view.js @@ -0,0 +1,49 @@ +define([ + './templates' +], function () { + + return Marionette.ItemView.extend({ + template: Templates['groups-search'], + + events: { + 'submit #groups-search-form': 'onFormSubmit', + 'search #groups-search-query': 'debouncedOnKeyUp', + 'keyup #groups-search-query': 'debouncedOnKeyUp' + }, + + initialize: function () { + this._bufferedValue = null; + this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 400); + }, + + onRender: function () { + this.delegateEvents(); + }, + + onFormSubmit: function (e) { + e.preventDefault(); + this.debouncedOnKeyUp(); + }, + + onKeyUp: function () { + var q = this.getQuery(); + if (q === this._bufferedValue) { + return; + } + this._bufferedValue = this.getQuery(); + if (this.searchRequest != null) { + this.searchRequest.abort(); + } + this.searchRequest = this.search(q); + }, + + getQuery: function () { + return this.$('#groups-search-query').val(); + }, + + search: function (q) { + return this.collection.fetch({ reset: true, data: { q: q } }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs new file mode 100644 index 00000000000..0644817633e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs @@ -0,0 +1,13 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs new file mode 100644 index 00000000000..a0927b33a73 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs @@ -0,0 +1,24 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs new file mode 100644 index 00000000000..19ba74febf8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs @@ -0,0 +1,9 @@ + diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs new file mode 100644 index 00000000000..4cad08c767e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs @@ -0,0 +1,6 @@ +
+
+ +
+ +
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs new file mode 100644 index 00000000000..841ab40ecd9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs @@ -0,0 +1,6 @@ + diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs new file mode 100644 index 00000000000..611cc382493 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs @@ -0,0 +1,16 @@ +
+ + +
+ +
+ {{name}} +
+ +
+ Members: {{membersCount}} +
+ +
+ {{description}} +
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs new file mode 100644 index 00000000000..5e81ec0b32a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs @@ -0,0 +1,6 @@ +
+ +
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs new file mode 100644 index 00000000000..eb346c0e31b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs @@ -0,0 +1,10 @@ + + + diff --git a/server/sonar-web/src/main/js/apps/groups/update-view.js b/server/sonar-web/src/main/js/apps/groups/update-view.js new file mode 100644 index 00000000000..71383a1793d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/update-view.js @@ -0,0 +1,29 @@ +define([ + './form-view' +], function (FormView) { + + return FormView.extend({ + + sendRequest: function () { + var that = this; + this.model.set({ + name: this.$('#create-group-name').val(), + description: this.$('#create-group-description').val() + }); + this.disableForm(); + return this.model.save(null, { + statusCode: { + // do not show global error + 400: null + } + }).done(function () { + that.collection.refresh(); + that.close(); + }).fail(function (jqXHR) { + that.enableForm(); + that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/groups/users-view.js b/server/sonar-web/src/main/js/apps/groups/users-view.js new file mode 100644 index 00000000000..59f84d74b08 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/groups/users-view.js @@ -0,0 +1,36 @@ +define([ + 'components/common/modals', + 'components/common/select-list', + './templates' +], function (Modal) { + + return Modal.extend({ + template: Templates['groups-users'], + + onRender: function () { + Modal.prototype.onRender.apply(this, arguments); + new window.SelectList({ + el: this.$('#groups-users'), + width: '100%', + readOnly: false, + focusSearch: false, + format: function (item) { + return item.name + '
' + item.login + ''; + }, + searchUrl: baseUrl + '/api/usergroups/users?ps=100&id=' + this.model.id, + selectUrl: baseUrl + '/api/usergroups/add_user', + deselectUrl: baseUrl + '/api/usergroups/remove_user', + extra: { + groupId: this.model.id + }, + selectParameter: 'userLogin', + selectParameterValue: 'login', + parse: function (r) { + this.more = false; + return r.users; + } + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/apps/users/groups-view.js b/server/sonar-web/src/main/js/apps/users/groups-view.js index b242f435773..ee817e66694 100644 --- a/server/sonar-web/src/main/js/apps/users/groups-view.js +++ b/server/sonar-web/src/main/js/apps/users/groups-view.js @@ -9,23 +9,24 @@ define([ itemTemplate: Templates['users-group'], onRender: function () { + console.log(this.model); Modal.prototype.onRender.apply(this, arguments); new window.SelectList({ el: this.$('#users-groups'), width: '100%', - readOnly: true, + readOnly: false, focusSearch: false, format: function (item) { return item.name + '
' + item.description + ''; }, searchUrl: baseUrl + '/api/users/groups?ps=100&login=' + this.model.id, - selectUrl: baseUrl + '/api/qualityprofiles/add_project', - deselectUrl: baseUrl + '/api/qualityprofiles/remove_project', + selectUrl: baseUrl + '/api/usergroups/add_user', + deselectUrl: baseUrl + '/api/usergroups/remove_user', extra: { - profileKey: key + userLogin: this.model.id }, - selectParameter: 'projectUuid', - selectParameterValue: 'uuid', + selectParameter: 'groupId', + selectParameterValue: 'id', parse: function (r) { this.more = false; return r.groups; diff --git a/server/sonar-web/src/main/less/components/panels.less b/server/sonar-web/src/main/less/components/panels.less index 44c2e82b69d..55c32ad5de1 100644 --- a/server/sonar-web/src/main/less/components/panels.less +++ b/server/sonar-web/src/main/less/components/panels.less @@ -25,7 +25,7 @@ } .panel + .panel { - margin-top: 20px; + margin-top: 10px; padding-top: 20px; border-top: 1px solid @barBorderColor; } diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/groups_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/groups_controller.rb index 51a6a8063ba..9ae565cc3e1 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/groups_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/groups_controller.rb @@ -23,12 +23,7 @@ class GroupsController < ApplicationController before_filter :admin_required def index - @groups = Group.find(:all, :order => 'name') - if params[:id] - @group = Group.find(params[:id]) - else - @group = Group.new - end + end def create_form diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/groups/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/groups/index.html.erb index fbf1a00fa7e..fedf3bd424c 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/groups/index.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/groups/index.html.erb @@ -1,65 +1,6 @@ -<% content_for :script do %> - -<% end %> - -
- - - - - - - -
- - - - - - - - - - - - - - - - - - <% @groups.each do |group| %> - - - - - - - <% end %> - -
NameDescriptionMembersOperations
<%= h 'Anyone' -%><%= message('user_groups.anyone.description') -%>  
<%= h group.name -%><%= h group.description -%> - <%= group.users.count -%> - (select) - - Edit -   - <%= link_to_action message('delete'), "#{ApplicationController.root_context}/groups/delete/#{group.id}", - :class => 'link-action link-red', - :id => "delete-#{group.name.parameterize}", - :confirm_button => message('delete'), - :confirm_title => 'Delete group: ' + group.name, - :confirm_msg => 'Are you sure that you want to delete this group? Members will not be deleted.', - :confirm_msg_params => [group.name] - -%> -
- -
-
+
+ diff --git a/server/sonar-web/src/test/js/groups-spec.js b/server/sonar-web/src/test/js/groups-spec.js new file mode 100644 index 00000000000..ef21e8a8fea --- /dev/null +++ b/server/sonar-web/src/test/js/groups-spec.js @@ -0,0 +1,355 @@ +/* globals casper: false */ +var lib = require('../lib'), + testName = lib.testName('Groups'); + +lib.initMessages(); +lib.changeWorkingDirectory('groups-spec'); +lib.configureCasper(); + +casper.test.begin(testName('List'), 7, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + test.assertExists('#groups-list ul'); + test.assertElementCount('#groups-list li[data-id]', 2); + test.assertSelectorContains('#groups-list .js-group-name', 'sonar-users'); + test.assertSelectorContains('#groups-list .js-group-description', + 'Any new users created will automatically join this group'); + test.assertElementCount('#groups-list .js-group-update', 2); + test.assertElementCount('#groups-list .js-group-delete', 2); + test.assertSelectorContains('#groups-list-footer', '2/2'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Search'), 4, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 2); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search-filtered.json', + { data: { q: 'adm' } }); + casper.evaluate(function () { + jQuery('#groups-search-query').val('adm'); + }); + casper.click('#groups-search-submit'); + casper.waitForSelectorTextChange('#groups-list-footer'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 1); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + casper.evaluate(function () { + jQuery('#groups-search-query').val(''); + }); + casper.click('#groups-search-submit'); + casper.waitForSelectorTextChange('#groups-list-footer'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 2); + test.assert(casper.evaluate(function () { + return jQuery('#groups-search-query').val() === ''; + })); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Show More'), 4, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search-big-1.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 1); + test.assertSelectorContains('#groups-list-footer', '1/2'); + lib.clearRequestMock(this.searchMock); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search-big-2.json', { data: { p: '2' } }); + casper.click('#groups-fetch-more'); + casper.waitForSelectorTextChange('#groups-list-footer'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 2); + test.assertSelectorContains('#groups-list-footer', '2/2'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Show Users'), 2, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/users*', 'users.json'); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + test.assertTextDoesntExist('Bob'); + casper.click('[data-id="1"] .js-group-users'); + casper.waitForText('Bob'); + }) + + .then(function () { + test.assertTextExist('John'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Create'), 4, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + this.createMock = lib.mockRequestFromFile('/api/usergroups/create', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 2); + casper.click('#groups-create'); + casper.waitForSelector('#create-group-form'); + }) + + .then(function () { + casper.click('#create-group-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.searchMock); + lib.mockRequestFromFile('/api/usergroups/search', 'search-created.json'); + lib.clearRequestMock(this.createMock); + lib.mockRequest('/api/usergroups/create', '{}', + { data: { name: 'name', description: 'description' } }); + casper.evaluate(function () { + jQuery('#create-group-name').val('name'); + jQuery('#create-group-description').val('description'); + }); + casper.click('#create-group-submit'); + casper.waitForSelectorTextChange('#groups-list-footer'); + }) + + .then(function () { + test.assertElementCount('#groups-list li[data-id]', 3); + test.assertSelectorContains('#groups-list .js-group-name', 'name'); + test.assertSelectorContains('#groups-list .js-group-description', 'description'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Update'), 2, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/usergroups/update', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + casper.click('[data-id="2"] .js-group-update'); + casper.waitForSelector('#create-group-form'); + }) + + .then(function () { + casper.click('#create-group-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.searchMock); + lib.mockRequestFromFile('/api/usergroups/search', 'search-updated.json'); + lib.clearRequestMock(this.updateMock); + lib.mockRequest('/api/usergroups/update', '{}', + { data: { id: '2', name: 'guys', description: 'cool guys' } }); + casper.evaluate(function () { + jQuery('#create-group-name').val('guys'); + jQuery('#create-group-description').val('cool guys'); + }); + casper.click('#create-group-submit'); + casper.waitForText('guys'); + }) + + .then(function () { + test.assertSelectorContains('[data-id="2"] .js-group-name', 'guys'); + test.assertSelectorContains('[data-id="2"] .js-group-description', 'cool guys'); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); + + +casper.test.begin(testName('Delete'), 1, function (test) { + casper + .start(lib.buildUrl('groups'), function () { + lib.setDefaultViewport(); + this.searchMock = lib.mockRequestFromFile('/api/usergroups/search', 'search.json'); + this.updateMock = lib.mockRequestFromFile('/api/usergroups/delete', 'error.json', { status: 400 }); + }) + + .then(function () { + casper.evaluate(function () { + require(['apps/groups/app'], function (App) { + App.start({ el: '#groups' }); + }); + jQuery.ajaxSetup({ dataType: 'json' }); + }); + }) + + .then(function () { + casper.waitForText('sonar-users'); + }) + + .then(function () { + casper.click('[data-id="1"] .js-group-delete'); + casper.waitForSelector('#delete-group-form'); + }) + + .then(function () { + casper.click('#delete-group-submit'); + casper.waitForSelector('.alert.alert-danger'); + }) + + .then(function () { + lib.clearRequestMock(this.updateMock); + lib.mockRequest('/api/usergroups/delete', '{}', { data: { id: '1'} }); + casper.click('#delete-group-submit'); + casper.waitWhileSelector('[data-id="1"]'); + }) + + .then(function () { + test.assert(true); + }) + + .then(function () { + lib.sendCoverage(); + }) + .run(function () { + test.done(); + }); +}); diff --git a/server/sonar-web/src/test/json/groups-spec/error.json b/server/sonar-web/src/test/json/groups-spec/error.json new file mode 100644 index 00000000000..dc1b261128c --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/error.json @@ -0,0 +1,7 @@ +{ + "errors": [ + { + "msg": "Some error message" + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/search-big-1.json b/server/sonar-web/src/test/json/groups-spec/search-big-1.json new file mode 100644 index 00000000000..654742be995 --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/search-big-1.json @@ -0,0 +1,13 @@ +{ + "total": 2, + "p": 1, + "ps": 1, + "groups": [ + { + "id": "1", + "name": "sonar-users", + "description": "Any new users created will automatically join this group", + "membersCount": 3 + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/search-big-2.json b/server/sonar-web/src/test/json/groups-spec/search-big-2.json new file mode 100644 index 00000000000..9d8a35bd3b6 --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/search-big-2.json @@ -0,0 +1,13 @@ +{ + "total": 2, + "p": 2, + "ps": 1, + "groups": [ + { + "id": "2", + "name": "sonar-administrators", + "description": "System administrators", + "membersCount": 1 + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/search-created.json b/server/sonar-web/src/test/json/groups-spec/search-created.json new file mode 100644 index 00000000000..1222b0dc009 --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/search-created.json @@ -0,0 +1,25 @@ +{ + "total": 3, + "p": 1, + "ps": 50, + "groups": [ + { + "id": "1", + "name": "sonar-users", + "description": "Any new users created will automatically join this group", + "membersCount": 3 + }, + { + "id": "2", + "name": "sonar-administrators", + "description": "System administrators", + "membersCount": 1 + }, + { + "id": "3", + "name": "name", + "description": "description", + "membersCount": 0 + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/search-filtered.json b/server/sonar-web/src/test/json/groups-spec/search-filtered.json new file mode 100644 index 00000000000..58a0ead4a40 --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/search-filtered.json @@ -0,0 +1,13 @@ +{ + "total": 1, + "p": 1, + "ps": 50, + "groups": [ + { + "id": "2", + "name": "sonar-administrators", + "description": "System administrators", + "membersCount": 1 + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/search-updated.json b/server/sonar-web/src/test/json/groups-spec/search-updated.json new file mode 100644 index 00000000000..ed1484f245d --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/search-updated.json @@ -0,0 +1,19 @@ +{ + "total": 2, + "p": 1, + "ps": 50, + "groups": [ + { + "id": "1", + "name": "sonar-users", + "description": "Any new users created will automatically join this group", + "membersCount": 3 + }, + { + "id": "2", + "name": "guys", + "description": "cool guys", + "membersCount": 1 + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/search.json b/server/sonar-web/src/test/json/groups-spec/search.json new file mode 100644 index 00000000000..a347403c5ae --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/search.json @@ -0,0 +1,19 @@ +{ + "total": 2, + "p": 1, + "ps": 50, + "groups": [ + { + "id": "1", + "name": "sonar-users", + "description": "Any new users created will automatically join this group", + "membersCount": 3 + }, + { + "id": "2", + "name": "sonar-administrators", + "description": "System administrators", + "membersCount": 1 + } + ] +} diff --git a/server/sonar-web/src/test/json/groups-spec/users.json b/server/sonar-web/src/test/json/groups-spec/users.json new file mode 100644 index 00000000000..dfb28ec4555 --- /dev/null +++ b/server/sonar-web/src/test/json/groups-spec/users.json @@ -0,0 +1,17 @@ +{ + "users": [ + { + "login": "bob", + "name": "Bob", + "selected": true + }, + { + "login": "john", + "name": "John", + "selected": true + } + ], + "p": 1, + "ps": 100, + "total": 2 +} diff --git a/server/sonar-web/src/test/views/groups.jade b/server/sonar-web/src/test/views/groups.jade new file mode 100644 index 00000000000..ddf0a29c6a3 --- /dev/null +++ b/server/sonar-web/src/test/views/groups.jade @@ -0,0 +1,5 @@ +extends layouts/main + +block body + #content + #groups -- 2.39.5