]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6602 refactor groups page
authorStas Vilchik <vilchiks@gmail.com>
Tue, 2 Jun 2015 11:53:17 +0000 (13:53 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Tue, 2 Jun 2015 13:08:42 +0000 (15:08 +0200)
37 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/groups/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/create-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/delete-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/form-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/group.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/groups.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/header-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/layout.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/list-footer-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/list-item-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/list-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/search-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-delete.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-form.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-layout.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-list-footer.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-list-item.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/templates/groups-users.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/update-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/groups/users-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/groups-view.js
server/sonar-web/src/main/less/components/panels.less
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/groups_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/groups/index.html.erb
server/sonar-web/src/test/js/groups-spec.js [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/error.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/search-big-1.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/search-big-2.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/search-created.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/search-filtered.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/search-updated.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/search.json [new file with mode: 0644]
server/sonar-web/src/test/json/groups-spec/users.json [new file with mode: 0644]
server/sonar-web/src/test/views/groups.jade [new file with mode: 0644]

index ff402d13cb06b7d30e534fcc1e3fdb695400cfff..fc55da5fceb8d8a7ac20a2483abb0c94df448e2a 100644 (file)
@@ -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 (file)
index 0000000..55c6dfe
--- /dev/null
@@ -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 (file)
index 0000000..8d5cfce
--- /dev/null
@@ -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 (file)
index 0000000..8fd83d3
--- /dev/null
@@ -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 (file)
index 0000000..aa87278
--- /dev/null
@@ -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 (file)
index 0000000..406f9ba
--- /dev/null
@@ -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 (file)
index 0000000..9ddd6c9
--- /dev/null
@@ -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 (file)
index 0000000..da6f7f6
--- /dev/null
@@ -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 (file)
index 0000000..a60fb06
--- /dev/null
@@ -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 (file)
index 0000000..cdad034
--- /dev/null
@@ -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 (file)
index 0000000..43eaa5b
--- /dev/null
@@ -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 (file)
index 0000000..138c36b
--- /dev/null
@@ -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 (file)
index 0000000..1540d7e
--- /dev/null
@@ -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 (file)
index 0000000..0644817
--- /dev/null
@@ -0,0 +1,13 @@
+<form id="delete-group-form" autocomplete="off">
+  <div class="modal-head">
+    <h2>Delete Group</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    Are you sure you want to delete "{{name}}"?
+  </div>
+  <div class="modal-foot">
+    <button id="delete-group-submit">Delete</button>
+    <a href="#" class="js-modal-close" id="delete-group-cancel">Cancel</a>
+  </div>
+</form>
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 (file)
index 0000000..a0927b3
--- /dev/null
@@ -0,0 +1,24 @@
+<form id="create-group-form" autocomplete="off">
+  <div class="modal-head">
+    <h2>{{#if id}}Update{{else}}Create{{/if}} Group</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    <div class="modal-field">
+      <label for="create-group-name">Name<em class="mandatory">*</em></label>
+      {{! keep this fake field to hack browser autofill }}
+      <input id="create-group-name-fake" name="name-fake" type="text" class="hidden">
+      <input id="create-group-name" name="name" type="text" size="50" maxlength="255" required value="{{name}}">
+    </div>
+    <div class="modal-field">
+      <label for="create-group-description">Description</label>
+      {{! keep this fake field to hack browser autofill }}
+      <textarea id="create-group-description-fake" name="description-fake" class="hidden"></textarea>
+      <textarea id="create-group-description" name="description">{{description}}</textarea>
+    </div>
+  </div>
+  <div class="modal-foot">
+    <button id="create-group-submit">{{#if id}}Update{{else}}Create{{/if}}</button>
+    <a href="#" class="js-modal-close" id="create-group-cancel">Cancel</a>
+  </div>
+</form>
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 (file)
index 0000000..19ba74f
--- /dev/null
@@ -0,0 +1,9 @@
+<header class="page-header">
+  <h1 class="page-title">{{t 'user_groups.page'}}</h1>
+  <div class="page-actions">
+    <div class="button-group">
+      <button id="groups-create">Create Group</button>
+    </div>
+  </div>
+  <p class="page-description">{{t 'user_groups.page.description'}}</p>
+</header>
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 (file)
index 0000000..4cad08c
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="page">
+  <div id="groups-header"></div>
+  <div id="groups-search"></div>
+  <div id="groups-list"></div>
+  <div id="groups-list-footer"></div>
+</div>
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 (file)
index 0000000..841ab40
--- /dev/null
@@ -0,0 +1,6 @@
+<footer class="spacer-top note text-center">
+  {{count}}/{{total}} shown
+  {{#if more}}
+    <a id="groups-fetch-more" class="spacer-left" href="#">show more</a>
+  {{/if}}
+</footer>
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 (file)
index 0000000..611cc38
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="pull-right big-spacer-left nowrap">
+  <a class="js-group-update icon-edit little-spacer-right" title="Update Details" data-toggle="tooltip" href="#"></a>
+  <a class="js-group-delete icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a>
+</div>
+
+<div class="display-inline-block text-top width-20">
+  <strong class="js-group-name">{{name}}</strong>
+</div>
+
+<div class="display-inline-block text-top big-spacer-left width-10">
+  Members: <a class="js-group-users" href="#">{{membersCount}}</a>
+</div>
+
+<div class="display-inline-block text-top big-spacer-left width-50">
+  <span class="js-group-description">{{description}}</span>
+</div>
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 (file)
index 0000000..5e81ec0
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="panel panel-vertical bordered-bottom spacer-bottom">
+  <form id="groups-search-form" class="search-box">
+    <button id="groups-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
+    <input id="groups-search-query" class="search-box-input" type="search" name="q" placeholder="Search" maxlength="100">
+  </form>
+</div>
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 (file)
index 0000000..eb346c0
--- /dev/null
@@ -0,0 +1,10 @@
+<div class="modal-head">
+  <h2>Update users</h2>
+</div>
+<div class="modal-body">
+  <div class="js-modal-messages"></div>
+  <div id="groups-users"></div>
+</div>
+<div class="modal-foot">
+  <a href="#" class="js-modal-close" id="groups-users-done">Done</a>
+</div>
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 (file)
index 0000000..71383a1
--- /dev/null
@@ -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 (file)
index 0000000..59f84d7
--- /dev/null
@@ -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 + '<br><span class="note">' + item.login + '</span>';
+        },
+        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;
+        }
+      });
+    }
+  });
+
+});
index b242f435773b7a3a760659c32662f655c9e6c50b..ee817e666941a27a47444956fc767665534edc14 100644 (file)
@@ -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 + '<br><span class="note">' + item.description + '</span>';
         },
         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;
index 44c2e82b69d5793fb0c0bfe614754cd34375846e..55c32ad5de12e0113c8f0e78054ddc6855c200da 100644 (file)
@@ -25,7 +25,7 @@
 }
 
 .panel + .panel {
-  margin-top: 20px;
+  margin-top: 10px;
   padding-top: 20px;
   border-top: 1px solid @barBorderColor;
 }
index 51a6a8063baebb4ae83618867aedafa54335fa9e..9ae565cc3e142da7878274006ee18cc5cc84a682 100644 (file)
@@ -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
index fbf1a00fa7ef5c2236e8b6797ecdfc751918ec65..fedf3bd424cb6e293360b21809e35f9f0150a69c 100644 (file)
@@ -1,65 +1,6 @@
-<% content_for :script do %>
-  <script>require(['components/common/select-list']);</script>
-<% end %>
-
-<div class="page">
-  <header class="page-header">
-    <h1 class="page-title"><%= message('user_groups.page') -%></h1>
-    <% if is_admin? %>
-      <div class="page-actions">
-        <a id="link-create-group" href="<%= ApplicationController.root_context -%>/groups/create_form" class="open-modal link-action">Add new group</a>
-      </div>
-    <% end %>
-    <p class="page-description"><%= message('user_groups.page.description') -%> </p>
-  </header>
-
-
-  <table width="100%">
-    <tr>
-      <td valign="top">
-
-        <table class="data width100 sortable" id="groups">
-          <thead>
-          <tr>
-            <th class="text-left sortfirstasc"><a>Name</a></th>
-            <th class="text-left"><a>Description</a></th>
-            <th class="text-left"><a>Members</a></th>
-            <th class="text-right nosort" nowrap><a>Operations</a></th>
-          </tr>
-          </thead>
-          <tbody>
-          <tr id="group-anyone">
-            <td class="text-left"><%= h 'Anyone' -%></td>
-            <td class="text-left" style="word-break:break-all"><%= message('user_groups.anyone.description') -%></td>
-            <td class="text-left">&nbsp;</td>
-            <td>&nbsp;</td>
-          </tr>
-          <% @groups.each do |group| %>
-            <tr id="group-<%= group.name.parameterize -%>">
-              <td class="text-left"><%= h group.name -%></td>
-              <td class="text-left" style="word-break:break-all"><%= h group.description -%></td>
-              <td class="text-left">
-                <span id="count-<%= group.name.parameterize -%>"><%= group.users.count -%></span>
-                (<a id="select-<%= group.name.parameterize -%>" class="open-modal link-action" href="<%= ApplicationController.root_context -%>/groups/select_user/<%= group.id -%>">select</a>)
-              </td>
-              <td class="text-right">
-                <a id='edit-<%= group.name.parameterize -%>' class='open-modal link-action' href="<%= ApplicationController.root_context -%>/groups/edit_form/<%= group.id -%>">Edit</a>
-                &nbsp;
-                <%= 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]
-                -%>
-              </td>
-            </tr>
-          <% end %>
-          </tbody>
-        </table>
-        <script>jQuery('#groups').sortable();</script>
-      </td>
-    </tr>
-  </table>
-</div>
+<div id="groups"></div>
+<script>
+  require(['apps/groups/app'], function (App) {
+    App.start({ el: '#groups' });
+  });
+</script>
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 (file)
index 0000000..ef21e8a
--- /dev/null
@@ -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 (file)
index 0000000..dc1b261
--- /dev/null
@@ -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 (file)
index 0000000..654742b
--- /dev/null
@@ -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 (file)
index 0000000..9d8a35b
--- /dev/null
@@ -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 (file)
index 0000000..1222b0d
--- /dev/null
@@ -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 (file)
index 0000000..58a0ead
--- /dev/null
@@ -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 (file)
index 0000000..ed1484f
--- /dev/null
@@ -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 (file)
index 0000000..a347403
--- /dev/null
@@ -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 (file)
index 0000000..dfb28ec
--- /dev/null
@@ -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 (file)
index 0000000..ddf0a29
--- /dev/null
@@ -0,0 +1,5 @@
+extends layouts/main
+
+block body
+  #content
+    #groups