]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6565 refactor users page
authorStas Vilchik <vilchiks@gmail.com>
Wed, 20 May 2015 08:54:59 +0000 (10:54 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Fri, 22 May 2015 13:53:42 +0000 (15:53 +0200)
45 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/js/apps/users/app.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/change-password-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/create-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/deactivate-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/form-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/header-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/layout.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/list-footer-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/list-item-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/list-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/search-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-change-password.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-form.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-header.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-layout.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-list-footer.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/templates/users-search.hbs [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/update-view.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/user.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/users.js [new file with mode: 0644]
server/sonar-web/src/main/less/components.less
server/sonar-web/src/main/less/components/modals.less
server/sonar-web/src/main/less/components/search.less [new file with mode: 0644]
server/sonar-web/src/main/less/init/icons.less
server/sonar-web/src/main/less/init/misc.less
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/users_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/users/index.html.erb
server/sonar-web/src/main/webapp/WEB-INF/config/routes.rb
server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot
server/sonar-web/src/main/webapp/fonts/sonar-5.2.svg
server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf
server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff
server/sonar-web/src/test/js/users-spec.js [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/error.json [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/search-big-1.json [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/search-big-2.json [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/search-created.json [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/search-filtered.json [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/search-updated.json [new file with mode: 0644]
server/sonar-web/src/test/json/users-spec/search.json [new file with mode: 0644]
server/sonar-web/src/test/server-coverage.js
server/sonar-web/src/test/views/users.jade [new file with mode: 0644]

index b9431a16328f81cc9c67ce7491120d7cf213d94a..82a5708b8f9931914578bc98b8e8d1defd27c021 100644 (file)
@@ -158,6 +158,10 @@ module.exports = (grunt) ->
         name: 'apps/markdown/app'
         out: '<%= ASSETS_PATH %>/js/apps/markdown/app.js'
 
+      users: options:
+        name: 'apps/users/app'
+        out: '<%= ASSETS_PATH %>/js/apps/users/app.js'
+
 
     parallel:
       build:
@@ -178,6 +182,7 @@ module.exports = (grunt) ->
           'requirejs:nav'
           'requirejs:issueFilterWidget'
           'requirejs:markdown'
+          'requirejs:users'
         ]
       casper:
         options: grunt: true
@@ -197,6 +202,7 @@ module.exports = (grunt) ->
           'casper:treemap'
           'casper:ui'
           'casper:workspace'
+          'casper:users'
         ]
 
 
@@ -254,6 +260,9 @@ module.exports = (grunt) ->
           '<%= BUILD_PATH %>/js/apps/markdown/templates.js': [
             '<%= SOURCE_PATH %>/js/apps/markdown/templates/**/*.hbs'
           ]
+          '<%= BUILD_PATH %>/js/apps/users/templates.js': [
+            '<%= SOURCE_PATH %>/js/apps/users/templates/**/*.hbs'
+          ]
 
 
     clean:
@@ -299,15 +308,21 @@ module.exports = (grunt) ->
         port: expressPort
       testCoverageLight:
         options:
+          concise: false
           verbose: true
+          'no-colors': false
         src: ['src/test/js/**/*<%= grunt.option("spec") %>*.js']
       single:
         options:
+          concise: false
           verbose: true
+          'no-colors': false
         src: ['src/test/js/<%= grunt.option("spec") %>-spec.js']
       testfile:
         options:
+          concise: false
           verbose: true
+          'no-colors': false
         src: ['<%= grunt.option("file") %>']
 
       apiDocumentation:
@@ -340,6 +355,8 @@ module.exports = (grunt) ->
         src: ['src/test/js/ui*.js']
       workspace:
         src: ['src/test/js/workspace*.js']
+      users:
+        src: ['src/test/js/users*.js']
 
     uglify:
       build:
@@ -402,7 +419,7 @@ module.exports = (grunt) ->
         tasks: ['copy:js', 'concat:build', 'copy:assets-all-js']
 
       handlebars:
-        files: '<%= SOURCE_PATH %>/hbs/**/*.hbs'
+        files: '<%= SOURCE_PATH %>/**/*.hbs'
         tasks: ['handlebars:build', 'copy:assets-all-js']
 
 
diff --git a/server/sonar-web/src/main/js/apps/users/app.js b/server/sonar-web/src/main/js/apps/users/app.js
new file mode 100644 (file)
index 0000000..9fa9df3
--- /dev/null
@@ -0,0 +1,47 @@
+define([
+  './layout',
+  './users',
+  './header-view',
+  './search-view',
+  './list-view',
+  './list-footer-view'
+], function (Layout, Users, 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.users = new Users();
+
+        // Header View
+        this.headerView = new HeaderView({ collection: this.users });
+        this.layout.headerRegion.show(this.headerView);
+
+        // Search View
+        this.searchView = new SearchView({ collection: this.users });
+        this.layout.searchRegion.show(this.searchView);
+
+        // List View
+        this.listView = new ListView({ collection: this.users });
+        this.layout.listRegion.show(this.listView);
+
+        // List Footer View
+        this.listFooterView = new ListFooterView({ collection: this.users });
+        this.layout.listFooterRegion.show(this.listFooterView);
+
+        // Go!
+        this.users.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/users/change-password-view.js b/server/sonar-web/src/main/js/apps/users/change-password-view.js
new file mode 100644 (file)
index 0000000..cd2a892
--- /dev/null
@@ -0,0 +1,37 @@
+define([
+  'components/common/modal-form',
+  './templates'
+], function (ModalForm) {
+
+  return ModalForm.extend({
+    template: Templates['users-change-password'],
+
+    onFormSubmit: function () {
+      ModalForm.prototype.onFormSubmit.apply(this, arguments);
+      this.sendRequest();
+    },
+
+    sendRequest: function () {
+      var that = this,
+          password = this.$('#change-user-password-password').val(),
+          confirmation = this.$('#change-user-password-password-confirmation').val();
+      if (password !== confirmation) {
+        that.showErrors([{ msg: 'New password and its confirmation do not match' }]);
+        return;
+      }
+      this.disableForm();
+      return this.model.changePassword(password, {
+        statusCode: {
+          // do not show global error
+          400: null
+        }
+      }).done(function () {
+        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/users/create-view.js b/server/sonar-web/src/main/js/apps/users/create-view.js
new file mode 100644 (file)
index 0000000..026f809
--- /dev/null
@@ -0,0 +1,33 @@
+define([
+  './user',
+  './form-view'
+], function (User, FormView) {
+
+  return FormView.extend({
+
+    sendRequest: function () {
+      var that = this,
+          user = new User({
+            login: this.$('#create-user-login').val(),
+            name: this.$('#create-user-name').val(),
+            email: this.$('#create-user-email').val(),
+            password: this.$('#create-user-password').val(),
+            scmAccounts: this.getScmAccounts()
+          });
+      this.disableForm();
+      return user.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/users/deactivate-view.js b/server/sonar-web/src/main/js/apps/users/deactivate-view.js
new file mode 100644 (file)
index 0000000..cf4c465
--- /dev/null
@@ -0,0 +1,32 @@
+define([
+  'components/common/modal-form',
+  './templates'
+], function (ModalForm) {
+
+  return ModalForm.extend({
+    template: Templates['users-deactivate'],
+
+    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/users/form-view.js b/server/sonar-web/src/main/js/apps/users/form-view.js
new file mode 100644 (file)
index 0000000..2cf2e1a
--- /dev/null
@@ -0,0 +1,52 @@
+define([
+  'components/common/modal-form',
+  './templates'
+], function (ModalForm) {
+
+  var $ = jQuery;
+
+  return ModalForm.extend({
+    template: Templates['users-form'],
+
+    events: function () {
+      return _.extend(ModalForm.prototype.events.apply(this, arguments), {
+        'click #create-user-add-scm-account': 'onAddScmAccountClick'
+      });
+    },
+
+    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();
+    },
+
+    onAddScmAccountClick: function (e) {
+      e.preventDefault();
+      this.addScmAccount();
+    },
+
+    getScmAccounts: function () {
+      var scmAccounts = this.$('[name="scmAccounts"]').map(function () {
+        return $(this).val();
+      }).toArray();
+      return scmAccounts.filter(function (value) {
+        return !!value;
+      });
+    },
+
+    addScmAccount: function () {
+      var fields = this.$('[name="scmAccounts"]');
+      fields.first().clone().val('').insertAfter(fields.last());
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/users/header-view.js b/server/sonar-web/src/main/js/apps/users/header-view.js
new file mode 100644 (file)
index 0000000..c8b7619
--- /dev/null
@@ -0,0 +1,25 @@
+define([
+  './create-view',
+  './templates'
+], function (CreateView) {
+
+  return Marionette.ItemView.extend({
+    template: Templates['users-header'],
+
+    events: {
+      'click #users-create': 'onCreateClick'
+    },
+
+    onCreateClick: function (e) {
+      e.preventDefault();
+      this.createUser();
+    },
+
+    createUser: function () {
+      new CreateView({
+        collection: this.collection
+      }).render();
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/users/layout.js b/server/sonar-web/src/main/js/apps/users/layout.js
new file mode 100644 (file)
index 0000000..d2b6251
--- /dev/null
@@ -0,0 +1,16 @@
+define([
+  './templates'
+], function () {
+
+  return Marionette.Layout.extend({
+    template: Templates['users-layout'],
+
+    regions: {
+      headerRegion: '#users-header',
+      searchRegion: '#users-search',
+      listRegion: '#users-list',
+      listFooterRegion: '#users-list-footer'
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/users/list-footer-view.js b/server/sonar-web/src/main/js/apps/users/list-footer-view.js
new file mode 100644 (file)
index 0000000..968ad09
--- /dev/null
@@ -0,0 +1,34 @@
+define([
+  './templates'
+], function () {
+
+  return Marionette.ItemView.extend({
+    template: Templates['users-list-footer'],
+
+    collectionEvents: {
+      'all': 'render'
+    },
+
+    events: {
+      'click #users-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/users/list-item-view.js b/server/sonar-web/src/main/js/apps/users/list-item-view.js
new file mode 100644 (file)
index 0000000..0a3e62a
--- /dev/null
@@ -0,0 +1,86 @@
+define([
+  './update-view',
+  './change-password-view',
+  './deactivate-view',
+  './templates'
+], function (UpdateView, ChangePasswordView, DeactivateView) {
+
+  return Marionette.ItemView.extend({
+    tagName: 'li',
+    className: 'panel panel-vertical',
+    template: Templates['users-list-item'],
+
+    events: {
+      'click .js-user-more-scm': 'onMoreScmClick',
+      'click .js-user-update': 'onUpdateClick',
+      'click .js-user-change-password': 'onChangePasswordClick',
+      'click .js-user-deactivate': 'onDeactivateClick'
+    },
+
+    initialize: function () {
+      this.scmLimit = 3;
+    },
+
+    onRender: function () {
+      this.$el.attr('data-login', this.model.id);
+      this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' });
+    },
+
+    onClose: function () {
+      this.$('[data-toggle="tooltip"]').tooltip('destroy');
+    },
+
+    onMoreScmClick: function (e) {
+      e.preventDefault();
+      this.showMoreScm();
+    },
+
+    onUpdateClick: function (e) {
+      e.preventDefault();
+      this.updateUser();
+    },
+
+    onChangePasswordClick: function (e) {
+      e.preventDefault();
+      this.changePassword();
+    },
+
+    onDeactivateClick: function (e) {
+      e.preventDefault();
+      this.deactivateUser();
+    },
+
+    showMoreScm: function () {
+      this.scmLimit = 10000;
+      this.render();
+    },
+
+    updateUser: function () {
+      new UpdateView({
+        model: this.model,
+        collection: this.model.collection
+      }).render();
+    },
+
+    changePassword: function () {
+      new ChangePasswordView({
+        model: this.model,
+        collection: this.model.collection
+      }).render();
+    },
+
+    deactivateUser: function () {
+      new DeactivateView({ model: this.model }).render();
+    },
+
+    serializeData: function () {
+      var scmAccounts = this.model.get('scmAccounts'),
+          scmAccountsLimit = scmAccounts.length > this.scmLimit ? this.scmLimit - 1 : this.scmLimit;
+      return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
+        firstScmAccounts: _.first(scmAccounts, scmAccountsLimit),
+        moreScmAccountsCount: scmAccounts.length - scmAccountsLimit
+      });
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/users/list-view.js b/server/sonar-web/src/main/js/apps/users/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/users/search-view.js b/server/sonar-web/src/main/js/apps/users/search-view.js
new file mode 100644 (file)
index 0000000..5129bf5
--- /dev/null
@@ -0,0 +1,49 @@
+define([
+  './templates'
+], function () {
+
+  return Marionette.ItemView.extend({
+    template: Templates['users-search'],
+
+    events: {
+      'submit #users-search-form': 'onFormSubmit',
+      'search #users-search-query': 'debouncedOnKeyUp',
+      'keyup #users-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.$('#users-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/users/templates/users-change-password.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-change-password.hbs
new file mode 100644 (file)
index 0000000..2268480
--- /dev/null
@@ -0,0 +1,24 @@
+<form id="change-user-password-form" autocomplete="off">
+  <div class="modal-head">
+    <h2>Change Password</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    <div class="modal-field">
+      <label for="change-user-password-password">New Password<em class="mandatory">*</em></label>
+      {{! keep this fake field to hack browser autofill }}
+      <input id="change-user-password-password-fake" name="password-fake" type="password" class="hidden">
+      <input id="change-user-password-password" name="password" type="password" size="50" maxlength="50" required>
+    </div>
+    <div class="modal-field">
+      <label for="change-user-password-password-confirmation">Confirm Password<em class="mandatory">*</em></label>
+      {{! keep this fake field to hack browser autofill }}
+      <input id="change-user-password-password-confirmation-fake" name="password-confirmation-fake" type="password" class="hidden">
+      <input id="change-user-password-password-confirmation" name="password-confirmation" type="password" size="50" maxlength="50" required>
+    </div>
+  </div>
+  <div class="modal-foot">
+    <button id="change-user-password-submit">Change</button>
+    <a href="#" class="js-modal-close" id="change-user-password-cancel">Cancel</a>
+  </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-deactivate.hbs
new file mode 100644 (file)
index 0000000..4d92cfd
--- /dev/null
@@ -0,0 +1,13 @@
+<form id="deactivate-user-form" autocomplete="off">
+  <div class="modal-head">
+    <h2>Deactivate User</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    Are you sure you want to deactivate "{{name}} ({{login}})"?
+  </div>
+  <div class="modal-foot">
+    <button id="deactivate-user-submit">Deactivate</button>
+    <a href="#" class="js-modal-close" id="deactivate-user-cancel">Cancel</a>
+  </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-form.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-form.hbs
new file mode 100644 (file)
index 0000000..f2637ce
--- /dev/null
@@ -0,0 +1,53 @@
+<form id="create-user-form" autocomplete="off">
+  <div class="modal-head">
+    <h2>{{#if login}}Update{{else}}Create{{/if}} User</h2>
+  </div>
+  <div class="modal-body">
+    <div class="js-modal-messages"></div>
+    {{#unless login}}
+      <div class="modal-field">
+        <label for="create-user-login">Login<em class="mandatory">*</em></label>
+        {{! keep this fake field to hack browser autofill }}
+        <input id="create-user-login-fake" name="login-fake" type="text" class="hidden">
+        <input id="create-user-login" name="login" type="text" size="50" minlength="2" maxlength="255" required
+            value="{{login}}">
+        <p class="note">Minimum 2 characters</p>
+      </div>
+    {{/unless}}
+    <div class="modal-field">
+      <label for="create-user-name">Name<em class="mandatory">*</em></label>
+      {{! keep this fake field to hack browser autofill }}
+      <input id="create-user-name-fake" name="name-fake" type="text" class="hidden">
+      <input id="create-user-name" name="name" type="text" size="50" maxlength="200" required value="{{name}}">
+    </div>
+    <div class="modal-field">
+      <label for="create-user-email">Email</label>
+      {{! keep this fake field to hack browser autofill }}
+      <input id="create-user-email-fake" name="email-fake" type="email" class="hidden">
+      <input id="create-user-email" name="email" type="email" size="50" maxlength="100" value="{{email}}">
+    </div>
+    {{#unless login}}
+      <div class="modal-field">
+        <label for="create-user-password">Password<em class="mandatory">*</em></label>
+        {{! keep this fake field to hack browser autofill }}
+        <input id="create-user-password-fake" name="password-fake" type="password" class="hidden">
+        <input id="create-user-password" name="password" type="password" size="50" maxlength="50" required>
+      </div>
+    {{/unless}}
+    <div class="modal-field">
+      <label>SCM Accounts</label>
+      {{#each scmAccounts}}
+        <input name="scmAccounts" type="text" size="50" maxlength="255" value="{{this}}">
+      {{else}}
+        <input name="scmAccounts" type="text" size="50" maxlength="255">
+      {{/each}}
+      <a id="create-user-add-scm-account" class="icon-plus" href="#" title="Add another SCM account"
+         data-toggle="tooltip"></a>
+      <p class="note">Note that login and email are automatically considered as SCM accounts</p>
+    </div>
+  </div>
+  <div class="modal-foot">
+    <button id="create-user-submit">{{#if login}}Update{{else}}Create{{/if}}</button>
+    <a href="#" class="js-modal-close" id="create-user-cancel">Cancel</a>
+  </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs
new file mode 100644 (file)
index 0000000..e356003
--- /dev/null
@@ -0,0 +1,9 @@
+<header class="page-header">
+  <h1 class="page-title">{{t 'users.page'}}</h1>
+  <div class="page-actions">
+    <div class="button-group">
+      <button id="users-create">Create User</button>
+    </div>
+  </div>
+  <p class="page-description">{{t 'users.page.description'}}</p>
+</header>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-layout.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-layout.hbs
new file mode 100644 (file)
index 0000000..e89130e
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="page">
+  <div id="users-header"></div>
+  <div id="users-search"></div>
+  <div id="users-list"></div>
+  <div id="users-list-footer"></div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-footer.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-footer.hbs
new file mode 100644 (file)
index 0000000..3cf34d7
--- /dev/null
@@ -0,0 +1,6 @@
+<footer class="spacer-top note text-center">
+  {{count}}/{{total}} shown
+  {{#if more}}
+    <a id="users-fetch-more" class="spacer-left" href="#">show more</a>
+  {{/if}}
+</footer>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
new file mode 100644 (file)
index 0000000..ce81603
--- /dev/null
@@ -0,0 +1,46 @@
+<div class="pull-right big-spacer-left nowrap">
+  <a class="js-user-update icon-edit little-spacer-right" title="Update Details" data-toggle="tooltip" href="#"></a>
+  <a class="js-user-change-password icon-lock little-spacer-right" title="Change Password" data-toggle="tooltip"
+     href="#"></a>
+  <a class="js-user-deactivate icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a>
+</div>
+
+<div class="display-inline-block text-top width-30">
+  <div>
+    <strong class="js-user-name">{{name}}</strong>
+    <span class="js-user-login note little-spacer-left">{{login}}</span>
+  </div>
+  <div class="js-user-email little-spacer-top">{{email}}</div>
+</div>
+
+<div class="display-inline-block text-top big-spacer-left width-25">
+  {{#notEmpty scmAccounts}}
+    <div class="pull-left spacer-right">
+      <strong>SCM</strong>
+    </div>
+    <ul class="overflow-hidden bordered-left">
+      {{#each firstScmAccounts}}
+        <li class="spacer-left little-spacer-bottom">{{this}}</li>
+      {{/each}}
+      {{#gt moreScmAccountsCount 0}}
+        <li class="spacer-left little-spacer-bottom">
+          <a class="js-user-more-scm" href="#">{{moreScmAccountsCount}} more</a>
+        </li>
+      {{/gt}}
+    </ul>
+  {{/notEmpty}}
+</div>
+
+<div class="display-inline-block text-top big-spacer-left width-25">
+  <div class="pull-left spacer-right">
+    <strong>Groups</strong>
+  </div>
+  <ul class="overflow-hidden bordered-left">
+    <li class="spacer-left little-spacer-bottom">sonar-users ?</li>
+    <li class="spacer-left little-spacer-bottom">sonar-administrators ?</li>
+    <li class="spacer-left little-spacer-bottom">
+      <a class="js-user-more-groups" href="#">? more</a>
+      <a class="icon-bullet-list spacer-left" title="Update Groups" data-toggle="tooltip" href="#"></a>
+    </li>
+  </ul>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs
new file mode 100644 (file)
index 0000000..3880505
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="panel panel-vertical bordered-bottom spacer-bottom">
+  <form id="users-search-form" class="search-box">
+    <button id="users-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
+    <input id="users-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/users/update-view.js b/server/sonar-web/src/main/js/apps/users/update-view.js
new file mode 100644 (file)
index 0000000..81497a3
--- /dev/null
@@ -0,0 +1,30 @@
+define([
+  './form-view'
+], function (FormView) {
+
+  return FormView.extend({
+
+    sendRequest: function () {
+      var that = this;
+      this.model.set({
+        name: this.$('#create-user-name').val(),
+        email: this.$('#create-user-email').val(),
+        scmAccounts: this.getScmAccounts()
+      });
+      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/users/user.js b/server/sonar-web/src/main/js/apps/users/user.js
new file mode 100644 (file)
index 0000000..119a27f
--- /dev/null
@@ -0,0 +1,71 @@
+define(function () {
+
+  return Backbone.Model.extend({
+    idAttribute: 'login',
+
+    urlRoot: function () {
+      return baseUrl + '/api/users';
+    },
+
+    defaults: function () {
+      return {
+        groupsCount: 0,
+        scmAccounts: []
+      };
+    },
+
+    toQuery: function () {
+      var q = this.toJSON();
+      _.each(q, function (value, key) {
+        if (_.isArray(value)) {
+          q[key] = value.join(',');
+        }
+      });
+      return q;
+    },
+
+    isNew: function() {
+      // server never sends a password
+      return this.has('password');
+    },
+
+    sync: function (method, model, options) {
+      var opts = options || {};
+      if (method === 'create') {
+        _.defaults(opts, {
+          url: this.urlRoot() + '/create',
+          type: 'POST',
+          data: _.pick(model.toQuery(), 'login', 'name', 'email', 'password', 'scmAccounts')
+        });
+      }
+      if (method === 'update') {
+        _.defaults(opts, {
+          url: this.urlRoot() + '/update',
+          type: 'POST',
+          data: _.pick(model.toQuery(), 'login', 'name', 'email', 'scmAccounts')
+        });
+      }
+      if (method === 'delete') {
+        _.defaults(opts, {
+          url: this.urlRoot() + '/deactivate',
+          type: 'POST',
+          data: { login: this.id }
+        });
+      }
+      return Backbone.ajax(opts);
+    },
+
+    changePassword: function (password, options) {
+      var opts = _.defaults(options || {}, {
+        url: this.urlRoot() + '/change_password',
+        type: 'POST',
+        data: {
+          login: this.id,
+          password: password
+        }
+      });
+      return Backbone.ajax(opts);
+    }
+  });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/users/users.js b/server/sonar-web/src/main/js/apps/users/users.js
new file mode 100644 (file)
index 0000000..adf80b6
--- /dev/null
@@ -0,0 +1,40 @@
+define([
+  './user'
+], function (User) {
+
+  return Backbone.Collection.extend({
+    model: User,
+
+    url: function () {
+      return baseUrl + '/api/users/search';
+    },
+
+    parse: function (r) {
+      this.total = +r.total;
+      this.p = +r.p;
+      this.ps = +r.ps;
+      return r.users;
+    },
+
+    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;
+    }
+
+  });
+
+});
index 58c47a924dee4b22110b4c4577585d4575ac5fa1..929acdc7e7479905f15351da50a76b8bf6da5531 100644 (file)
@@ -42,3 +42,4 @@
 @import "components/badges";
 @import "components/columns";
 @import "components/workspace";
+@import "components/search";
index 98fd4a4af1eb3073781ac2404718a5d382f86008..49863dbcfcc76131217c4564e966db96d4ff3d9b 100644 (file)
@@ -143,6 +143,7 @@ ul.modal-head-metadata li {
 }
 
 .modal-field input[type=text],
+.modal-field input[type=email],
 .modal-field input[type=password],
 .modal-field textarea {
   width: 250px;
diff --git a/server/sonar-web/src/main/less/components/search.less b/server/sonar-web/src/main/less/components/search.less
new file mode 100644 (file)
index 0000000..48be2ff
--- /dev/null
@@ -0,0 +1,24 @@
+@import (reference) "../variables";
+@import (reference) "../mixins";
+
+.search-box {
+  position: relative;
+  font-size: 0;
+}
+
+.search-box-input {
+  vertical-align: middle;
+  width: 250px;
+  border: none !important;
+  font-size: @baseFontSize;
+}
+
+.search-box-submit {
+  display: inline-block;
+  vertical-align: middle;
+
+  .icon-search:before {
+    color: @secondFontColor;
+    font-size: @iconSmallFontSize;
+  }
+}
index fe27ce965a9d1f5ea0e4396e3da806374263b5d7..e94718f0f4f72b4c5e79412962c6108c15fa6a9f 100644 (file)
@@ -310,6 +310,7 @@ a[class^="icon-"], a[class*=" icon-"] {
 }
 .icon-bullet-list:before {
   content: "\f03a";
+  font-size: @iconSmallFontSize;
 }
 .icon-settings:before {
   content: "\f015";
@@ -586,6 +587,10 @@ a[class^="icon-"], a[class*=" icon-"] {
   content: "\f0b0";
   font-size: @iconFontSize;
 }
+.icon-lock:before {
+  content: "\f023";
+  font-size: @iconFontSize;
+}
 .icon-issues {
   display: inline-block;
   .square(60px);
index b766304822a30289d311399619365d9dfbfc6f8b..3cc8526e54d2723d1488ba1e8a06e4552b6ceef0 100644 (file)
 .spacer-bottom { margin-bottom: 8px; }
 .spacer-top    { margin-top: 8px; }
 
+.big-spacer-left   { margin-left: 16px; }
+.big-spacer-right  { margin-right: 16px; }
+.big-spacer-bottom { margin-bottom: 16px; }
+.big-spacer-top    { margin-top: 16px; }
+
 .little-spacer-left   { margin-left: 4px; }
 .little-spacer-right  { margin-right: 4px; }
 .little-spacer-bottom { margin-bottom: 4px; }
@@ -62,12 +67,16 @@ td.spacer-top    { padding-top: 8px; }
 .bordered-bottom { border-bottom: 1px solid @barBorderColor; }
 .bordered-top    { border-top: 1px solid @barBorderColor; }
 
+.overflow-hidden { overflow: hidden; }
+
 .width-100 { width: 100%; }
 .width-80 { width: 80%; }
 .width-60 { width: 60%; }
 .width-55 { width: 55%; }
 .width-50 { width: 50%; }
 .width-40 { width: 40%; }
+.width-30 { width: 30%; }
+.width-25 { width: 25%; }
 .width-20 { width: 20%; }
 .width-15 { width: 15%; }
 
index 779a828b3294b3b2330df9ffedd7a95b49c000a0..ac643f9c0d4b744d77e720c9b6ab3722055f4ec4 100644 (file)
@@ -54,98 +54,15 @@ class UsersController < ApplicationController
   end
 
   def index
-    init_users_list
-    if params[:id]
-      @user = User.find(params[:id])
-    else
-      @user = User.new
-    end
-  end
 
-  def create_form
-    @user = User.new
-    render :partial => 'users/create_form'
   end
 
   def new
     render :action => 'new', :layout => 'nonav'
   end
 
-  def edit_form
-    call_backend do
-      @user = Internal.users_api.getByLogin(params[:id])
-      render :partial => 'users/edit_form', :status => 200
-    end
-  end
 
-
-  def change_password_form
-    @user = User.find(params[:id])
-    render :partial => 'users/change_password_form', :status => 200
-  end
-
-  def update_password
-    user = User.find(params[:id])
-    @user = user
-    if params[:user][:password].blank?
-      @errors = message('my_profile.password.empty')
-      render :partial => 'users/change_password_form', :status => 400
-    elsif user.update_attributes(:password => params[:user][:password], :password_confirmation => params[:user][:password_confirmation])
-      flash[:notice] = 'Password was successfully updated.'
-      render :text => 'ok', :status => 200
-    else
-      @errors = user.errors.full_messages.join("<br/>\n")
-      render :partial => 'users/change_password_form', :status => 400
-    end
-  end
-
-  def update
-    call_backend do
-      Internal.users_api.update(params[:user])
-      flash[:notice] = 'User was successfully updated.'
-      render :text => 'ok', :status => 200
-    end
-  end
-
-  def delete
-    begin
-      user = User.find(params[:id])
-      Api.users.deactivate(user.login)
-      flash[:notice] = 'User is deleted.'
-    rescue NativeException => exception
-      if exception.cause.java_kind_of? Java::OrgSonarServerExceptions::ServerException
-        error = exception.cause
-        flash[:error] = (error.getMessage ? error.getMessage : Api::Utils.message(error.l10nKey, :params => error.l10nParams.to_a))
-      else
-        flash[:error] = 'Error when deleting this user.'
-      end
-    end
-
-    redirect_to(:action => 'index', :id => nil)
-  end
-
-  def select_group
-    @user = User.find(params[:id])
-    render :partial => 'users/select_group'
-  end
-
-  def set_groups
-    @user = User.find(params[:id])
-
-    if  @user.set_groups(params[:groups])
-      flash[:notice] = 'User is updated.'
-    end
-
-    redirect_to(:action => 'index')
-  end
-
-  def to_index(errors, id)
-    if !errors.empty?
-      flash[:error] = errors.full_messages.join("<br/>\n")
-    end
-
-    redirect_to(:action => 'index', :id => id)
-  end
+  private
 
   def prepare_user
     user = User.new(params[:user])
@@ -155,11 +72,4 @@ class UsersController < ApplicationController
     user
   end
 
-
-  private
-
-  def init_users_list
-    @users = User.find(:all, :conditions => ["active=?", true], :include => 'groups')
-  end
-
 end
index 59ac4c82243542aef86309be47ecd0687e7ab67e..e3026f5208413fbb7870d4809b73824b1ac5c5a1 100644 (file)
@@ -1,63 +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('users.page') -%></h1>
-    <% if profiles_administrator? %>
-      <div class="page-actions">
-        <a id="create-link-user" href="<%= ApplicationController.root_context -%>/users/create_form" class="open-modal">
-          Add new user
-        </a>
-      </div>
-    <% end %>
-    <p class="page-description"><%= message('users.page.description') -%> </p>
-  </header>
-
-  <table width="100%">
-    <tr>
-      <td valign="top">
-        <table class="data width100 sortable" id="users">
-          <thead>
-          <tr>
-            <th class="text-left"><a>Login</a></th>
-            <th class="text-left sortfirstasc"><a>Name</a></th>
-            <th class="text-left"><a>Email</a></th>
-            <th class="text-left nosort"><a>Groups</a></th>
-            <th class="text-right nosort" nowrap><a>Operations</a></th>
-          </tr>
-          </thead>
-          <tbody>
-          <% @users.each do |user| %>
-            <tr id="user-<%= user.login.parameterize -%>">
-              <td class="text-left" valign="top"><%= h user.login -%></td>
-              <td class="text-left" valign="top"><%= h user.name -%></td>
-              <td class="text-left" valign="top"><%= h user.email -%></td>
-              <td class="text-left" valign="top">
-                <%= h user.groups.sort.map(&:name).join(', ') %>
-                (<%= link_to "select", {:action => 'select_group', :id => user.id}, {:id => "select-#{user.login.parameterize}", :class => 'open-modal link-action'} %>)
-              </td>
-              <td class="text-right" valign="top">
-                <a id="edit-<%= user.login -%>" class="open-modal link-action" href="<%= ApplicationController.root_context -%>/users/edit_form/<%= u user.login -%>">Edit</a>
-                &nbsp;
-                <%= link_to 'Change password', {:id => user.id, :action => 'change_password_form'}, {:id => "change-password-#{user.login.parameterize}", :class => 'open-modal link-action'} -%>
-                &nbsp;
-                <%= link_to_action message('delete'), "#{ApplicationController.root_context}/users/delete/#{user.id}",
-                                   :class => 'link-action link-red',
-                                   :id => "delete-#{user.login}",
-                                   :confirm_button => message('delete'),
-                                   :confirm_title => 'Delete user: '+user.login,
-                                   :confirm_msg => 'Warning : are you sure to delete the user "' + user.name+'"?',
-                                   :confirm_msg_params => [user.name]
-                -%>
-              </td>
-            </tr>
-          <% end %>
-          </tbody>
-        </table>
-        <script>jQuery('#users').sortable();</script>
-      </td>
-    </tr>
-  </table>
-</div>
+<div id="users"></div>
+<script>
+  require(['apps/users/app'], function (App) {
+    App.start({ el: '#users' });
+  });
+</script>
index 6f17f4f61b0663ab28cc42a11ebaa446b69f7099..2a60362d1120e18718b57752c3ae51f4d340cdc7 100644 (file)
@@ -1,10 +1,4 @@
 ActionController::Routing::Routes.draw do |map|
-  map.connect 'users/select_group', :controller => 'users', :action => 'select_group'
-  map.connect 'users/set_groups', :controller => 'users', :action => 'set_groups'
-  map.connect 'users/create_form', :controller => 'users', :action => 'create_form'
-  map.connect 'users/edit_form', :controller => 'users', :action => 'edit_form'
-  map.resources :users
-
   map.namespace :api do |api|
     api.resources :events, :only => [:index, :show, :create, :destroy]
     api.resources :user_properties, :only => [:index, :show, :create, :destroy], :requirements => { :id => /.*/ }
index 2b2f832e8c809f68fe5c903af01cf5cc4ff4a55a..fbc4d06cfa34b193036e7fb46e34060178158118 100755 (executable)
Binary files a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot and b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.eot differ
index 972c3f3ee0c95da9f0c41a5047386dba7c7ac39a..1d648b7b456b5ba9a9a44419b3cefccb02b720de 100755 (executable)
@@ -40,6 +40,7 @@
 <glyph unicode="&#xf017;" d="M73.143 82.286h585.143v438.857h-237.714q-22.857 0-38.857 16t-16 38.857v237.714h-292.571v-731.429zM438.857 594.286h214.857q-5.714 16.571-12.571 23.429l-178.857 178.857q-6.857 6.857-23.429 12.571v-214.857zM731.429 576v-512q0-22.857-16-38.857t-38.857-16h-621.714q-22.857 0-38.857 16t-16 38.857v768q0 22.857 16 38.857t38.857 16h365.714q22.857 0 50.286-11.429t43.429-27.429l178.286-178.286q16-16 27.429-43.429t11.429-50.286z" horiz-adv-x="731" />
 <glyph unicode="&#xf018;" d="M512 649.143v-256q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v36.571q0 8 5.143 13.143t13.143 5.143h128v201.143q0 8 5.143 13.143t13.143 5.143h36.571q8 0 13.143-5.143t5.143-13.143zM749.714 448q0 84.571-41.714 156t-113.143 113.143-156 41.714-156-41.714-113.143-113.143-41.714-156 41.714-156 113.143-113.143 156-41.714 156 41.714 113.143 113.143 41.714 156zM877.714 448q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
 <glyph unicode="&#xf021;" d="M863.429 356.571q0-2.857-0.571-4-36.571-153.143-153.143-248.286t-273.143-95.143q-83.429 0-161.429 31.429t-139.143 89.714l-73.714-73.714q-10.857-10.857-25.714-10.857t-25.714 10.857-10.857 25.714v256q0 14.857 10.857 25.714t25.714 10.857h256q14.857 0 25.714-10.857t10.857-25.714-10.857-25.714l-78.286-78.286q40.571-37.714 92-58.286t106.857-20.571q76.571 0 142.857 37.143t106.286 102.286q6.286 9.714 30.286 66.857 4.571 13.143 17.143 13.143h109.714q7.429 0 12.857-5.429t5.429-12.857zM877.714 813.714v-256q0-14.857-10.857-25.714t-25.714-10.857h-256q-14.857 0-25.714 10.857t-10.857 25.714 10.857 25.714l78.857 78.857q-84.571 78.286-199.429 78.286-76.571 0-142.857-37.143t-106.286-102.286q-6.286-9.714-30.286-66.857-4.571-13.143-17.143-13.143h-113.714q-7.429 0-12.857 5.429t-5.429 12.857v4q37.143 153.143 154.286 248.286t274.286 95.143q83.429 0 162.286-31.714t140-89.429l74.286 73.714q10.857 10.857 25.714 10.857t25.714-10.857 10.857-25.714z" />
+<glyph unicode="&#xf023;" d="M182.857 512h292.571v109.714q0 60.571-42.857 103.429t-103.429 42.857-103.429-42.857-42.857-103.429v-109.714zM658.286 457.143v-329.143q0-22.857-16-38.857t-38.857-16h-548.571q-22.857 0-38.857 16t-16 38.857v329.143q0 22.857 16 38.857t38.857 16h18.286v109.714q0 105.143 75.429 180.571t180.571 75.429 180.571-75.429 75.429-180.571v-109.714h18.286q22.857 0 38.857-16t16-38.857z" horiz-adv-x="658" />
 <glyph unicode="&#xf02c;" d="M256 704q0 30.286-21.429 51.714t-51.714 21.429-51.714-21.429-21.429-51.714 21.429-51.714 51.714-21.429 51.714 21.429 21.429 51.714zM865.714 374.857q0-30.286-21.143-51.429l-280.571-281.143q-22.286-21.143-52-21.143-30.286 0-51.429 21.143l-408.571 409.143q-21.714 21.143-36.857 57.714t-15.143 66.857v237.714q0 29.714 21.714 51.429t51.429 21.714h237.714q30.286 0 66.857-15.143t58.286-36.857l408.571-408q21.143-22.286 21.143-52zM1085.143 374.857q0-30.286-21.143-51.429l-280.571-281.143q-22.286-21.143-52-21.143-20.571 0-33.714 8t-30.286 25.714l268.571 268.571q21.143 21.143 21.143 51.429 0 29.714-21.143 52l-408.571 408q-21.714 21.714-58.286 36.857t-66.857 15.143h128q30.286 0 66.857-15.143t58.286-36.857l408.571-408q21.143-22.286 21.143-52z" horiz-adv-x="1097" />
 <glyph unicode="&#xf039;" d="M1024 192v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 411.429v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 630.857v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714zM1024 850.286v-73.143q0-14.857-10.857-25.714t-25.714-10.857h-950.857q-14.857 0-25.714 10.857t-10.857 25.714v73.143q0 14.857 10.857 25.714t25.714 10.857h950.857q14.857 0 25.714-10.857t10.857-25.714z" />
 <glyph unicode="&#xf03a;" d="M146.286 210.286v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM146.286 429.714v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM146.286 649.143v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM1024 210.286v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857zM146.286 868.571v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-109.714q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h109.714q7.429 0 12.857-5.429t5.429-12.857zM1024 429.714v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857zM1024 649.143v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857zM1024 868.571v-109.714q0-7.429-5.429-12.857t-12.857-5.429h-768q-7.429 0-12.857 5.429t-5.429 12.857v109.714q0 7.429 5.429 12.857t12.857 5.429h768q7.429 0 12.857-5.429t5.429-12.857z" />
@@ -62,6 +63,7 @@
 <glyph unicode="&#xf069;" d="M846.857 360q26.286-14.857 34-44.286t-7.143-55.714l-36.571-62.857q-14.857-26.286-44.286-34t-55.714 7.143l-152 87.429v-175.429q0-29.714-21.714-51.429t-51.429-21.714h-73.143q-29.714 0-51.429 21.714t-21.714 51.429v175.429l-152-87.429q-26.286-14.857-55.714-7.143t-44.286 34l-36.571 62.857q-14.857 26.286-7.143 55.714t34 44.286l152 88-152 88q-26.286 14.857-34 44.286t7.143 55.714l36.571 62.857q14.857 26.286 44.286 34t55.714-7.143l152-87.429v175.429q0 29.714 21.714 51.429t51.429 21.714h73.143q29.714 0 51.429-21.714t21.714-51.429v-175.429l152 87.429q26.286 14.857 55.714 7.143t44.286-34l36.571-62.857q14.857-26.286 7.143-55.714t-34-44.286l-152-88z" horiz-adv-x="951" />
 <glyph unicode="&#xf073;" d="M73.143 9.143h164.571v164.571h-164.571v-164.571zM274.286 9.143h182.857v164.571h-182.857v-164.571zM73.143 210.286h164.571v182.857h-164.571v-182.857zM274.286 210.286h182.857v182.857h-182.857v-182.857zM73.143 429.714h164.571v164.571h-164.571v-164.571zM493.714 9.143h182.857v164.571h-182.857v-164.571zM274.286 429.714h182.857v164.571h-182.857v-164.571zM713.143 9.143h164.571v164.571h-164.571v-164.571zM493.714 210.286h182.857v182.857h-182.857v-182.857zM292.571 704v164.571q0 7.429-5.429 12.857t-12.857 5.429h-36.571q-7.429 0-12.857-5.429t-5.429-12.857v-164.571q0-7.429 5.429-12.857t12.857-5.429h36.571q7.429 0 12.857 5.429t5.429 12.857zM713.143 210.286h164.571v182.857h-164.571v-182.857zM493.714 429.714h182.857v164.571h-182.857v-164.571zM713.143 429.714h164.571v164.571h-164.571v-164.571zM731.429 704v164.571q0 7.429-5.429 12.857t-12.857 5.429h-36.571q-7.429 0-12.857-5.429t-5.429-12.857v-164.571q0-7.429 5.429-12.857t12.857-5.429h36.571q7.429 0 12.857 5.429t5.429 12.857zM950.857 740.571v-731.429q0-29.714-21.714-51.429t-51.429-21.714h-804.571q-29.714 0-51.429 21.714t-21.714 51.429v731.429q0 29.714 21.714 51.429t51.429 21.714h73.143v54.857q0 37.714 26.857 64.571t64.571 26.857h36.571q37.714 0 64.571-26.857t26.857-64.571v-54.857h219.429v54.857q0 37.714 26.857 64.571t64.571 26.857h36.571q37.714 0 64.571-26.857t26.857-64.571v-54.857h73.143q29.714 0 51.429-21.714t21.714-51.429z" horiz-adv-x="951" />
 <glyph unicode="&#xf075;" d="M1024 448q0-99.429-68.571-183.714t-186.286-133.143-257.143-48.857q-40 0-82.857 4.571-113.143-100-262.857-138.286-28-8-65.143-12.571-9.714-1.143-17.429 5.143t-10 16.571v0.571q-1.714 2.286-0.286 6.857t1.143 5.714 2.571 5.429l3.429 5.143t4 4.857 4.571 5.143q4 4.571 17.714 19.714t19.714 21.714 17.714 22.571 18.571 29.143 15.429 33.714 14.857 43.429q-89.714 50.857-141.429 125.714t-51.714 160.571q0 74.286 40.571 142t109.143 116.857 163.429 78 198.857 28.857q139.429 0 257.143-48.857t186.286-133.143 68.571-183.714z" />
+<glyph unicode="&#xf080;" d="M365.714 438.857v-292.571h-146.286v292.571h146.286zM585.143 731.428v-585.143h-146.286v585.143h146.286zM1170.286 73.143v-73.143h-1170.286v877.714h73.143v-804.571h1097.143zM804.571 585.143v-438.857h-146.286v438.857h146.286zM1024 804.571v-658.286h-146.286v658.286h146.286z" horiz-adv-x="1170" />
 <glyph unicode="&#xf085;" d="M512 448q0 60.571-42.857 103.429t-103.429 42.857-103.429-42.857-42.857-103.429 42.857-103.429 103.429-42.857 103.429 42.857 42.857 103.429zM950.857 155.429q0 29.714-21.714 51.429t-51.429 21.714-51.429-21.714-21.714-51.429q0-30.286 21.429-51.714t51.714-21.429 51.714 21.429 21.429 51.714zM950.857 740.571q0 29.714-21.714 51.429t-51.429 21.714-51.429-21.714-21.714-51.429q0-30.286 21.429-51.714t51.714-21.429 51.714 21.429 21.429 51.714zM731.429 500v-105.714q0-5.714-4-11.143t-9.143-6l-88.571-13.714q-6.286-20-18.286-43.429 19.429-27.429 51.429-65.714 4-5.714 4-11.429 0-6.857-4-10.857-13.143-17.143-47.143-51.143t-44.857-34q-6.286 0-12 4l-65.714 51.429q-21.143-10.857-44-17.714-6.286-61.714-13.143-88.571-4-13.714-17.143-13.714h-106.286q-6.286 0-11.429 4.286t-5.714 10l-13.143 87.429q-19.429 5.714-42.857 17.714l-67.429-50.857q-4-4-11.429-4-6.286 0-12 4.571-82.286 76-82.286 91.429 0 5.143 4 10.857 5.714 8 23.429 30.286t26.857 34.857q-13.143 25.143-20 46.857l-86.857 13.714q-5.714 0.571-9.714 5.429t-4 11.143v105.714q0 5.714 4 11.143t9.143 6l88.571 13.714q6.286 20 18.286 43.429-19.429 27.429-51.429 65.714-4 6.286-4 11.429 0 6.857 4 11.429 12.571 17.143 46.857 50.857t45.143 33.714q6.286 0 12-4l65.714-51.429q19.429 10.286 44 18.286 6.286 61.714 13.143 88 4 13.714 17.143 13.714h106.286q6.286 0 11.429-4.286t5.714-10l13.143-87.429q19.429-5.714 42.857-17.714l67.429 50.857q4.571 4 11.429 4 6.286 0 12-4.571 82.286-76 82.286-91.429 0-5.143-4-10.857-6.857-9.143-24-30.857t-25.714-34.286q13.143-27.429 19.429-46.857l86.857-13.143q5.714-1.143 9.714-6t4-11.143zM1097.143 195.429v-80q0-9.143-85.143-17.714-6.857-15.429-17.143-29.714 29.143-64.571 29.143-78.857 0-2.286-2.286-4-69.714-40.571-70.857-40.571-4.571 0-26.286 26.857t-29.714 38.857q-11.429-1.143-17.143-1.143t-17.143 1.143q-8-12-29.714-38.857t-26.286-26.857q-1.143 0-70.857 40.571-2.286 1.714-2.286 4 0 14.286 29.143 78.857-10.286 14.286-17.143 29.714-85.143 8.571-85.143 17.714v80q0 9.143 85.143 17.714 7.429 16.571 17.143 29.714-29.143 64.571-29.143 78.857 0 2.286 2.286 4 2.286 1.143 20 11.429t33.714 19.429 17.143 9.143q4.571 0 26.286-26.571t29.714-38.571q11.429 1.143 17.143 1.143t17.143-1.143q29.143 40.571 52.571 64l3.429 1.143q2.286 0 70.857-40 2.286-1.714 2.286-4 0-14.286-29.143-78.857 9.714-13.143 17.143-29.714 85.143-8.571 85.143-17.714zM1097.143 780.571v-80q0-9.143-85.143-17.714-6.857-15.429-17.143-29.714 29.143-64.571 29.143-78.857 0-2.286-2.286-4-69.714-40.571-70.857-40.571-4.571 0-26.286 26.857t-29.714 38.857q-11.429-1.143-17.143-1.143t-17.143 1.143q-8-12-29.714-38.857t-26.286-26.857q-1.143 0-70.857 40.571-2.286 1.714-2.286 4 0 14.286 29.143 78.857-10.286 14.286-17.143 29.714-85.143 8.571-85.143 17.714v80q0 9.143 85.143 17.714 7.429 16.571 17.143 29.714-29.143 64.571-29.143 78.857 0 2.286 2.286 4 2.286 1.143 20 11.429t33.714 19.429 17.143 9.143q4.571 0 26.286-26.571t29.714-38.571q11.429 1.143 17.143 1.143t17.143-1.143q29.143 40.571 52.571 64l3.429 1.143q2.286 0 70.857-40 2.286-1.714 2.286-4 0-14.286-29.143-78.857 9.714-13.143 17.143-29.714 85.143-8.571 85.143-17.714z" horiz-adv-x="1097" />
 <glyph unicode="&#xf08e;" d="M804.571 429.714v-182.857q0-68-48.286-116.286t-116.286-48.286h-475.429q-68 0-116.286 48.286t-48.286 116.286v475.429q0 68 48.286 116.286t116.286 48.286h402.286q8 0 13.143-5.143t5.143-13.143v-36.571q0-8-5.143-13.143t-13.143-5.143h-402.286q-37.714 0-64.571-26.857t-26.857-64.571v-475.429q0-37.714 26.857-64.571t64.571-26.857h475.429q37.714 0 64.571 26.857t26.857 64.571v182.857q0 8 5.143 13.143t13.143 5.143h36.571q8 0 13.143-5.143t5.143-13.143zM1024 923.429v-292.571q0-14.857-10.857-25.714t-25.714-10.857-25.714 10.857l-100.571 100.571-372.571-372.571q-5.714-5.714-13.143-5.714t-13.143 5.714l-65.143 65.143q-5.714 5.714-5.714 13.143t5.714 13.143l372.571 372.571-100.571 100.571q-10.857 10.857-10.857 25.714t10.857 25.714 25.714 10.857h292.571q14.857 0 25.714-10.857t10.857-25.714z" />
 <glyph unicode="&#xf091;" d="M261.714 455.429q-42.286 92.571-42.286 212h-146.286v-54.857q0-44.571 54-92.571t134.571-64.571zM877.714 612.571v54.857h-146.286q0-119.429-42.286-212 80.571 16.571 134.571 64.571t54 92.571zM950.857 685.714v-73.143q0-40.571-23.714-81.714t-64-74.286-98.857-55.714-123.143-25.429q-24-30.857-54.286-54.286-21.714-19.429-30-41.429t-8.286-51.143q0-30.857 17.429-52t55.714-21.143q42.857 0 76.286-26t33.429-65.429v-36.571q0-8-5.143-13.143t-13.143-5.143h-475.429q-8 0-13.143 5.143t-5.143 13.143v36.571q0 39.429 33.429 65.429t76.286 26q38.286 0 55.714 21.143t17.429 52q0 29.143-8.286 51.143t-30 41.429q-30.286 23.429-54.286 54.286-64.571 2.857-123.143 25.429t-98.857 55.714-64 74.286-23.714 81.714v73.143q0 22.857 16 38.857t38.857 16h164.571v54.857q0 37.714 26.857 64.571t64.571 26.857h329.143q37.714 0 64.571-26.857t26.857-64.571v-54.857h164.571q22.857 0 38.857-16t16-38.857z" horiz-adv-x="951" />
index d4e65cacf9d053703305f179c8ef3bfdfb7a4ef2..fa26fd2b826ebab80955675327b79b8a34dbab1c 100755 (executable)
Binary files a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf and b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.ttf differ
index ae53de1e82b3246b4393941f66ad37250d3f0842..1e9e0d823fc272c1c13e3b4cb938f5e91d5c8a9e 100755 (executable)
Binary files a/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff and b/server/sonar-web/src/main/webapp/fonts/sonar-5.2.woff differ
diff --git a/server/sonar-web/src/test/js/users-spec.js b/server/sonar-web/src/test/js/users-spec.js
new file mode 100644 (file)
index 0000000..1b96301
--- /dev/null
@@ -0,0 +1,406 @@
+/* globals casper: false */
+var lib = require('../lib'),
+    testName = lib.testName('Users');
+
+lib.initMessages();
+lib.changeWorkingDirectory('users-spec');
+lib.configureCasper();
+
+casper.test.begin(testName('List'), 11, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        lib.mockRequestFromFile('/api/users/search', 'search.json');
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        test.assertExists('#users-list ul');
+        test.assertElementCount('#users-list li[data-login]', 3);
+        test.assertSelectorContains('#users-list .js-user-login', 'smith');
+        test.assertSelectorContains('#users-list .js-user-name', 'Bob');
+        test.assertSelectorContains('#users-list .js-user-email', 'bob@example.com');
+        test.assertElementCount('#users-list .js-user-update', 3);
+        test.assertElementCount('#users-list .js-user-change-password', 3);
+        test.assertElementCount('#users-list .js-user-deactivate', 3);
+        test.assertSelectorContains('#users-list-footer', '3/3');
+      })
+
+      .then(function () {
+        test.assertSelectorDoesntContain('[data-login="ryan"]', 'another@example.com');
+        casper.click('[data-login="ryan"] .js-user-more-scm');
+        test.assertSelectorContains('[data-login="ryan"]', 'another@example.com');
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
+
+
+casper.test.begin(testName('Search'), 4, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json');
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 3);
+        lib.clearRequestMock(this.searchMock);
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search-filtered.json', { data: { q: 'ryan' } });
+        casper.evaluate(function () {
+          jQuery('#users-search-query').val('ryan');
+        });
+        casper.click('#users-search-submit');
+        casper.waitForSelectorTextChange('#users-list-footer');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 1);
+        lib.clearRequestMock(this.searchMock);
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json');
+        casper.evaluate(function () {
+          jQuery('#users-search-query').val('');
+        });
+        casper.click('#users-search-submit');
+        casper.waitForSelectorTextChange('#users-list-footer');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 3);
+        test.assert(casper.evaluate(function () {
+          return jQuery('#users-search-query').val() === '';
+        }));
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
+
+
+casper.test.begin(testName('Show More'), 4, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search-big-1.json');
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 2);
+        test.assertSelectorContains('#users-list-footer', '2/3');
+        lib.clearRequestMock(this.searchMock);
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search-big-2.json', { data: { p: '2' } });
+        casper.click('#users-fetch-more');
+        casper.waitForSelectorTextChange('#users-list-footer');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 3);
+        test.assertSelectorContains('#users-list-footer', '3/3');
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
+
+
+casper.test.begin(testName('Create'), 5, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json');
+        this.createMock = lib.mockRequestFromFile('/api/users/create', 'error.json', { status: 400 });
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+          jQuery.ajaxSetup({ dataType: 'json' });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 3);
+        casper.click('#users-create');
+        casper.waitForSelector('#create-user-form');
+      })
+
+      .then(function () {
+        casper.click('#create-user-submit');
+        casper.waitForSelector('.alert.alert-danger');
+      })
+
+      .then(function () {
+        lib.clearRequestMock(this.searchMock);
+        lib.mockRequestFromFile('/api/users/search', 'search-created.json');
+        lib.clearRequestMock(this.createMock);
+        lib.mockRequest('/api/users/create', '{}',
+            { data: { login: 'login', name: 'name', email: 'email@example.com', scmAccounts: 'scm1,scm2' } });
+        casper.click('#create-user-add-scm-account');
+        casper.click('#create-user-add-scm-account');
+        casper.evaluate(function () {
+          jQuery('#create-user-login').val('login');
+          jQuery('#create-user-name').val('name');
+          jQuery('#create-user-email').val('email@example.com');
+          jQuery('[name="scmAccounts"]').first().val('scm1');
+          jQuery('[name="scmAccounts"]').last().val('scm2');
+        });
+        casper.click('#create-user-submit');
+        casper.waitForSelectorTextChange('#users-list-footer');
+      })
+
+      .then(function () {
+        test.assertElementCount('#users-list li[data-login]', 4);
+        test.assertSelectorContains('#users-list .js-user-login', 'login');
+        test.assertSelectorContains('#users-list .js-user-name', 'name');
+        test.assertSelectorContains('#users-list .js-user-email', 'email@example.com');
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
+
+
+casper.test.begin(testName('Update'), 3, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json');
+        this.updateMock = lib.mockRequestFromFile('/api/users/update', 'error.json', { status: 400 });
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+          jQuery.ajaxSetup({ dataType: 'json' });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        casper.click('[data-login="smith"] .js-user-update');
+        casper.waitForSelector('#create-user-form');
+      })
+
+      .then(function () {
+        casper.click('#create-user-submit');
+        casper.waitForSelector('.alert.alert-danger');
+      })
+
+      .then(function () {
+        lib.clearRequestMock(this.searchMock);
+        lib.mockRequestFromFile('/api/users/search', 'search-updated.json');
+        lib.clearRequestMock(this.updateMock);
+        lib.mockRequest('/api/users/update', '{}',
+            { data: { login: 'smith', name: 'Mike', email: 'mike@example.com', scmAccounts: 'scm5,scm6' } });
+        casper.click('#create-user-add-scm-account');
+        casper.evaluate(function () {
+          jQuery('#create-user-name').val('Mike');
+          jQuery('#create-user-email').val('mike@example.com');
+          jQuery('[name="scmAccounts"]').first().val('scm5');
+          jQuery('[name="scmAccounts"]').last().val('scm6');
+        });
+        casper.click('#create-user-submit');
+        casper.waitForText('Mike');
+      })
+
+      .then(function () {
+        test.assertSelectorContains('[data-login="smith"] .js-user-login', 'smith');
+        test.assertSelectorContains('[data-login="smith"] .js-user-name', 'Mike');
+        test.assertSelectorContains('[data-login="smith"] .js-user-email', 'mike@example.com');
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
+
+
+casper.test.begin(testName('Change Password'), 1, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json');
+        this.updateMock = lib.mockRequestFromFile('/api/users/change_password', 'error.json', { status: 400 });
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+          jQuery.ajaxSetup({ dataType: 'json' });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        casper.click('[data-login="smith"] .js-user-change-password');
+        casper.waitForSelector('#change-user-password-form');
+      })
+
+      .then(function () {
+        casper.click('#change-user-password-submit');
+        casper.waitForSelector('.alert.alert-danger');
+      })
+
+      .then(function () {
+        casper.click('#change-user-password-cancel');
+        casper.waitWhileSelector('#change-user-password-form');
+      })
+
+      .then(function () {
+        casper.click('[data-login="smith"] .js-user-change-password');
+        casper.waitForSelector('#change-user-password-form');
+      })
+
+      .then(function () {
+        lib.clearRequestMock(this.updateMock);
+        lib.mockRequest('/api/users/change_password', '{}', { data: { login: 'smith', password: 'secret' } });
+        casper.evaluate(function () {
+          jQuery('#change-user-password-password').val('secret');
+          jQuery('#change-user-password-password-confirmation').val('another');
+        });
+        casper.click('#change-user-password-submit');
+        casper.waitForSelector('.alert.alert-danger');
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          jQuery('#change-user-password-password').val('secret');
+          jQuery('#change-user-password-password-confirmation').val('secret');
+        });
+        casper.click('#change-user-password-submit');
+        casper.waitWhileSelector('#change-user-password-form');
+      })
+
+      .then(function () {
+        test.assert(true);
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
+
+
+casper.test.begin(testName('Deactivate'), 1, function (test) {
+  casper
+      .start(lib.buildUrl('users'), function () {
+        lib.setDefaultViewport();
+        this.searchMock = lib.mockRequestFromFile('/api/users/search', 'search.json');
+        this.updateMock = lib.mockRequestFromFile('/api/users/deactivate', 'error.json', { status: 400 });
+      })
+
+      .then(function () {
+        casper.evaluate(function () {
+          require(['apps/users/app'], function (App) {
+            App.start({ el: '#users' });
+          });
+          jQuery.ajaxSetup({ dataType: 'json' });
+        });
+      })
+
+      .then(function () {
+        casper.waitForText('Bob');
+      })
+
+      .then(function () {
+        casper.click('[data-login="smith"] .js-user-deactivate');
+        casper.waitForSelector('#deactivate-user-form');
+      })
+
+      .then(function () {
+        casper.click('#deactivate-user-submit');
+        casper.waitForSelector('.alert.alert-danger');
+      })
+
+      .then(function () {
+        lib.clearRequestMock(this.updateMock);
+        lib.mockRequest('/api/users/deactivate', '{}', { data: { login: 'smith'} });
+        casper.click('#deactivate-user-submit');
+        casper.waitWhileSelector('[data-login="smith"]');
+      })
+
+      .then(function () {
+        test.assert(true);
+      })
+
+      .then(function () {
+        lib.sendCoverage();
+      })
+      .run(function () {
+        test.done();
+      });
+});
diff --git a/server/sonar-web/src/test/json/users-spec/error.json b/server/sonar-web/src/test/json/users-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/users-spec/search-big-1.json b/server/sonar-web/src/test/json/users-spec/search-big-1.json
new file mode 100644 (file)
index 0000000..5e3f981
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "total": 3,
+  "p": 1,
+  "ps": 2,
+  "users": [
+    {
+      "login": "admin",
+      "name": "Administrator",
+      "email": "admin@example.com",
+      "scmAccounts": []
+    },
+    {
+      "login": "smith",
+      "name": "Bob",
+      "email": "bob@example.com",
+      "scmAccounts": []
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/users-spec/search-big-2.json b/server/sonar-web/src/test/json/users-spec/search-big-2.json
new file mode 100644 (file)
index 0000000..605d9c1
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "total": 3,
+  "p": 2,
+  "ps": 2,
+  "users": [
+    {
+      "login": "ryan",
+      "name": "John",
+      "email": "john@example.com",
+      "scmAccounts": []
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/users-spec/search-created.json b/server/sonar-web/src/test/json/users-spec/search-created.json
new file mode 100644 (file)
index 0000000..a4f1282
--- /dev/null
@@ -0,0 +1,34 @@
+{
+  "total": 4,
+  "p": 1,
+  "ps": 50,
+  "users": [
+    {
+      "login": "admin",
+      "name": "Administrator",
+      "email": "admin@example.com",
+      "scmAccounts": []
+    },
+    {
+      "login": "login",
+      "name": "name",
+      "email": "email@example.com",
+      "scmAccounts": [
+        "scm1",
+        "scm2"
+      ]
+    },
+    {
+      "login": "smith",
+      "name": "Bob",
+      "email": "bob@example.com",
+      "scmAccounts": []
+    },
+    {
+      "login": "ryan",
+      "name": "John",
+      "email": "john@example.com",
+      "scmAccounts": []
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/users-spec/search-filtered.json b/server/sonar-web/src/test/json/users-spec/search-filtered.json
new file mode 100644 (file)
index 0000000..ac74e4b
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "total": 1,
+  "p": 1,
+  "ps": 50,
+  "users": [
+    {
+      "login": "ryan",
+      "name": "John",
+      "email": "john@example.com",
+      "scmAccounts": []
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/users-spec/search-updated.json b/server/sonar-web/src/test/json/users-spec/search-updated.json
new file mode 100644 (file)
index 0000000..294f7ed
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "total": 3,
+  "p": 1,
+  "ps": 50,
+  "users": [
+    {
+      "login": "admin",
+      "name": "Administrator",
+      "email": "admin@example.com",
+      "scmAccounts": []
+    },
+    {
+      "login": "smith",
+      "name": "Mike",
+      "email": "mike@example.com",
+      "scmAccounts": []
+    },
+    {
+      "login": "ryan",
+      "name": "John",
+      "email": "john@example.com",
+      "scmAccounts": []
+    }
+  ]
+}
diff --git a/server/sonar-web/src/test/json/users-spec/search.json b/server/sonar-web/src/test/json/users-spec/search.json
new file mode 100644 (file)
index 0000000..abe8f1d
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "total": 3,
+  "p": 1,
+  "ps": 50,
+  "users": [
+    {
+      "login": "admin",
+      "name": "Administrator",
+      "email": "admin@example.com",
+      "scmAccounts": []
+    },
+    {
+      "login": "smith",
+      "name": "Bob",
+      "email": "bob@example.com",
+      "scmAccounts": ["smith@example.com"]
+    },
+    {
+      "login": "ryan",
+      "name": "John",
+      "email": "john@example.com",
+      "scmAccounts": ["ryan@example.com", "ryan", "john", "another@example.com"]
+    }
+  ]
+}
index a6a46527f57fcf458f33d6938f95dd543d6190a6..3fd4400cea369ec00c8fe11da4b1bbfb76d8f2e8 100644 (file)
@@ -24,7 +24,7 @@ var express = require('express'),
     url = require('url'),
     JS_RE = /\.js$/,
     THIRD_PARTY_RE = /\/third-party\//,
-    TEMPLATES_RE = /\/templates\//;
+    TEMPLATES_RE = /\/templates.js/;
 
 var app = express();
 
diff --git a/server/sonar-web/src/test/views/users.jade b/server/sonar-web/src/test/views/users.jade
new file mode 100644 (file)
index 0000000..d469a62
--- /dev/null
@@ -0,0 +1,5 @@
+extends layouts/main
+
+block body
+  #content
+    #users