Commit f745c6e5b5aa84a882a73ed00e8c3c6d28c70657

Authored by Dmitriy Zaporozhets
2 parents 361a8781 fd12e5c6

Merge branch 'improve_large_groups' into 'master'

Improve for large groupsImprove for large groups

This merge request is a set of patched to improve application for large groups with more then 100 people.

Pages to be improved:
:white_check_mark: Group#members page
:white_check_mark: Project#issues page
:white_check_mark: Project#merge_requests page
:white_check_mark: Project#new_issue page
:white_check_mark: Project#new_mr page
:white_check_mark: Project#members page
app/assets/javascripts/api.js.coffee
... ... @@ -3,6 +3,7 @@
3 3 user_path: "/api/:version/users/:id.json"
4 4 notes_path: "/api/:version/projects/:id/notes.json"
5 5 namespaces_path: "/api/:version/namespaces.json"
  6 + project_users_path: "/api/:version/projects/:id/users.json"
6 7  
7 8 # Get 20 (depends on api) recent notes
8 9 # and sort the ascending from oldest to newest
... ... @@ -50,6 +51,23 @@
50 51 ).done (users) ->
51 52 callback(users)
52 53  
  54 + # Return project users list. Filtered by query
  55 + # Only active users retrieved
  56 + projectUsers: (project_id, query, callback) ->
  57 + url = Api.buildUrl(Api.project_users_path)
  58 + url = url.replace(':id', project_id)
  59 +
  60 + $.ajax(
  61 + url: url
  62 + data:
  63 + private_token: gon.api_token
  64 + search: query
  65 + per_page: 20
  66 + active: true
  67 + dataType: "json"
  68 + ).done (users) ->
  69 + callback(users)
  70 +
53 71 # Return namespaces list. Filtered by query
54 72 namespaces: (query, callback) ->
55 73 url = Api.buildUrl(Api.namespaces_path)
... ...
app/assets/javascripts/project_users_select.js.coffee 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +$ ->
  2 + projectUserFormatResult = (user) ->
  3 + if user.avatar_url
  4 + avatar = user.avatar_url
  5 + else if gon.gravatar_enabled
  6 + avatar = gon.gravatar_url
  7 + avatar = avatar.replace('%{hash}', md5(user.email))
  8 + avatar = avatar.replace('%{size}', '24')
  9 + else
  10 + avatar = gon.relative_url_root + "/assets/no_avatar.png"
  11 +
  12 + "<div class='user-result'>
  13 + <div class='user-image'><img class='avatar s24' src='#{avatar}'></div>
  14 + <div class='user-name'>#{user.name}</div>
  15 + <div class='user-username'>#{user.username}</div>
  16 + </div>"
  17 +
  18 + projectUserFormatSelection = (user) ->
  19 + user.name
  20 +
  21 + $('.ajax-project-users-select').each (i, select) ->
  22 + project_id = $('body').data('project-id')
  23 +
  24 + $(select).select2
  25 + placeholder: $(select).data('placeholder') || "Search for a user"
  26 + multiple: $(select).hasClass('multiselect')
  27 + minimumInputLength: 0
  28 + query: (query) ->
  29 + Api.projectUsers project_id, query.term, (users) ->
  30 + data = { results: users }
  31 + query.callback(data)
  32 +
  33 + initSelection: (element, callback) ->
  34 + id = $(element).val()
  35 + if id isnt ""
  36 + Api.user(id, callback)
  37 +
  38 +
  39 + formatResult: projectUserFormatResult
  40 + formatSelection: projectUserFormatSelection
  41 + dropdownCssClass: "ajax-project-users-dropdown"
  42 + dropdownAutoWidth: true
  43 + escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
  44 + m
... ...
app/assets/javascripts/users_select.js.coffee
1 1 $ ->
2 2 userFormatResult = (user) ->
3   - if user.avatar
4   - avatar = user.avatar.url
  3 + if user.avatar_url
  4 + avatar = user.avatar_url
5 5 else if gon.gravatar_enabled
6 6 avatar = gon.gravatar_url
7 7 avatar = avatar.replace('%{hash}', md5(user.email))
... ...
app/assets/stylesheets/application.scss
... ... @@ -65,6 +65,7 @@
65 65 @import "sections/wall.scss";
66 66 @import "sections/dashboard.scss";
67 67 @import "sections/stat_graph.scss";
  68 +@import "sections/groups.scss";
68 69  
69 70 /**
70 71 * Code ighlight
... ...
app/assets/stylesheets/generic/common.scss
... ... @@ -112,6 +112,7 @@ pre.well-pre {
112 112 .dropdown-menu > li > a:hover,
113 113 .dropdown-menu > li > a:focus {
114 114 background: #29b;
  115 + color: #FFF
115 116 }
116 117  
117 118 .breadcrumb > li + li:before {
... ...
app/assets/stylesheets/generic/forms.scss
... ... @@ -51,3 +51,27 @@ label {
51 51 .input-mn-300 {
52 52 min-width: 300px;
53 53 }
  54 +
  55 +.custom-form-control {
  56 + width: 150px;
  57 +}
  58 +
  59 +@media (min-width: $screen-sm-min) {
  60 + .custom-form-control {
  61 + width: 150px;
  62 + }
  63 +}
  64 +
  65 +/* Medium devices (desktops, 992px and up) */
  66 +@media (min-width: $screen-md-min) {
  67 + .custom-form-control {
  68 + width: 170px;
  69 + }
  70 +}
  71 +
  72 +/* Large devices (large desktops, 1200px and up) */
  73 +@media (min-width: $screen-lg-min) {
  74 + .custom-form-control {
  75 + width: 200px;
  76 + }
  77 +}
... ...
app/assets/stylesheets/generic/selects.scss
1 1 /** Select2 selectbox style override **/
2   -
3 2 .select2-container, .select2-container.select2-drop-above {
4 3 .select2-choice {
5 4 background: #FFF;
... ... @@ -12,9 +11,13 @@
12 11 }
13 12  
14 13 .select2-drop-active {
15   - border: 1px solid #BBB;
  14 + border: 1px solid #BBB !important;
16 15 margin-top: 4px;
17 16  
  17 + &.select2-drop-above {
  18 + margin-bottom: 8px;
  19 + }
  20 +
18 21 .select2-search input {
19 22 background: #fafafa;
20 23 border-color: #DDD;
... ... @@ -78,3 +81,9 @@ select {
78 81 .project-refs-form .select2-container {
79 82 margin-right: 10px;
80 83 }
  84 +
  85 +.ajax-users-dropdown, .ajax-project-users-dropdown {
  86 + .select2-search {
  87 + padding-top: 4px;
  88 + }
  89 +}
... ...
app/assets/stylesheets/sections/groups.scss 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +.new-group-member-holder {
  2 + margin-top: 50px;
  3 + background: #f9f9f9;
  4 + padding-top: 20px;
  5 +}
  6 +
  7 +.member-search-form {
  8 + float: left;
  9 +}
... ...
app/assets/stylesheets/sections/issues.scss
... ... @@ -14,8 +14,8 @@
14 14  
15 15 .issue-check {
16 16 float: left;
17   - padding: 8px 0;
18 17 padding-right: 8px;
  18 + margin-bottom: 10px;
19 19 min-width: 15px;
20 20 }
21 21  
... ... @@ -38,13 +38,21 @@
38 38 }
39 39 }
40 40  
41   -input.check_all_issues {
  41 +.check-all-holder {
  42 + height: 32px;
42 43 float: left;
43   - padding: 0;
44   - margin: 0;
45   - margin-right: 10px;
46   - position: relative;
47   - top: 13px;
  44 + margin-right: 12px;
  45 + padding: 6px 10px;
  46 + border: 1px solid #ccc;
  47 + @include border-radius(4px);
  48 +
  49 +
  50 + input.check_all_issues {
  51 + padding: 0;
  52 + margin: 0;
  53 + position: relative;
  54 + top: 3px;
  55 + }
48 56 }
49 57  
50 58 .issues_content {
... ... @@ -91,6 +99,13 @@ input.check_all_issues {
91 99 .update_selected_issues {
92 100 margin-left: 4px;
93 101 }
  102 +
  103 + .select2-container .select2-choice {
  104 + height: 32px;
  105 + line-height: 28px;
  106 + color: #444 !important;
  107 + font-weight: 500;
  108 + }
94 109 }
95 110 }
96 111  
... ...
app/controllers/groups_controller.rb
... ... @@ -63,7 +63,14 @@ class GroupsController &lt; ApplicationController
63 63  
64 64 def members
65 65 @project = group.projects.find(params[:project_id]) if params[:project_id]
66   - @members = group.users_groups.order('group_access DESC')
  66 + @members = group.users_groups
  67 +
  68 + if params[:search].present?
  69 + users = group.users.search(params[:search])
  70 + @members = @members.where(user_id: users)
  71 + end
  72 +
  73 + @members = @members.order('group_access DESC').page(params[:page]).per(50)
67 74 @users_group = UsersGroup.new
68 75 end
69 76  
... ...
app/controllers/projects/issues_controller.rb
... ... @@ -28,7 +28,7 @@ class Projects::IssuesController &lt; Projects::ApplicationController
28 28 @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
29 29 sort_param = params[:sort] || 'newest'
30 30 @sort = sort_param.humanize unless sort_param.empty?
31   -
  31 + @assignees = User.where(id: @project.issues.pluck(:assignee_id))
32 32  
33 33 respond_to do |format|
34 34 format.html
... ...
app/controllers/projects/merge_requests_controller.rb
... ... @@ -28,6 +28,7 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController
28 28 assignee_id, milestone_id = params[:assignee_id], params[:milestone_id]
29 29 @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
30 30 @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
  31 + @assignees = User.where(id: @project.merge_requests.pluck(:assignee_id))
31 32 end
32 33  
33 34 def show
... ...
app/helpers/application_helper.rb
... ... @@ -162,15 +162,6 @@ module ApplicationHelper
162 162  
163 163 alias_method :url_to_image, :image_url
164 164  
165   - def users_select_tag(id, opts = {})
166   - css_class = "ajax-users-select "
167   - css_class << "multiselect " if opts[:multiple]
168   - css_class << (opts[:class] || '')
169   - value = opts[:selected] || ''
170   -
171   - hidden_field_tag(id, value, class: css_class)
172   - end
173   -
174 165 def body_data_page
175 166 path = controller.controller_path.split('/')
176 167 namespace = path.first if path.second
... ...
app/helpers/issues_helper.rb
... ... @@ -70,11 +70,11 @@ module IssuesHelper
70 70 end
71 71  
72 72 def bulk_update_milestone_options
73   - options_for_select(["None (backlog)", nil]) + options_from_collection_for_select(project_active_milestones, "id", "title", params[:milestone_id])
  73 + options_for_select(["None (backlog)"]) + options_from_collection_for_select(project_active_milestones, "id", "title", params[:milestone_id])
74 74 end
75 75  
76 76 def bulk_update_assignee_options
77   - options_for_select(["None (unassigned)", nil]) + options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id])
  77 + options_for_select(["None (unassigned)"]) + options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id])
78 78 end
79 79  
80 80 def assignee_options object
... ...
app/helpers/selects_helper.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +module SelectsHelper
  2 + def users_select_tag(id, opts = {})
  3 + css_class = "ajax-users-select "
  4 + css_class << "multiselect " if opts[:multiple]
  5 + css_class << (opts[:class] || '')
  6 + value = opts[:selected] || ''
  7 +
  8 + hidden_field_tag(id, value, class: css_class)
  9 + end
  10 +
  11 + def project_users_select_tag(id, opts = {})
  12 + css_class = "ajax-project-users-select "
  13 + css_class << "multiselect " if opts[:multiple]
  14 + css_class << (opts[:class] || '')
  15 + value = opts[:selected] || ''
  16 + placeholder = opts[:placeholder] || 'Select user'
  17 +
  18 + hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder)
  19 + end
  20 +end
... ...
app/views/groups/_new_group_member.html.haml
1 1 = form_for @users_group, url: group_users_groups_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
2   - %h4.append-bottom-20
3   - New member(s) for
4   - %strong #{@group.name}
5   - group
6   -
7   - %p 1. Choose users you want in the group
8 2 .form-group
9 3 = f.label :user_ids, "People", class: 'control-label'
10 4 .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large')
11 5  
12   - %p 2. Set access level for them
13 6 .form-group
14 7 = f.label :group_access, "Group Access", class: 'control-label'
15 8 .col-sm-10= select_tag :group_access, options_for_select(UsersGroup.group_access_roles, @users_group.group_access), class: "project-access-select select2"
... ...
app/views/groups/members.html.haml
... ... @@ -6,14 +6,34 @@
6 6 %strong= link_to "here", help_permissions_path, class: "vlink"
7 7  
8 8 %hr
9   -.ui-box
  9 +
  10 +.clearfix
  11 + = form_tag members_group_path(@group), method: :get, class: 'form-inline member-search-form' do
  12 + .form-group
  13 + = search_field_tag :search, params[:search], { placeholder: 'Find member by name', class: 'form-control search-text-input input-mn-300' }
  14 + = submit_tag 'Search', class: 'btn'
  15 +
  16 + - if current_user.can? :manage_group, @group
  17 + .pull-right
  18 + = link_to '#', class: 'btn btn-new js-toggle-visibility-link' do
  19 + Add members
  20 + %i.icon-chevron-down
  21 +
  22 + .js-toggle-visibility-container.hide.new-group-member-holder
  23 + = render "new_group_member"
  24 +
  25 +.ui-box.prepend-top-20
10 26 .title
11 27 %strong #{@group.name}
12 28 group members
13 29 %small
14   - (#{@members.count})
  30 + (#{@members.total_count})
15 31 %ul.well-list
16 32 - @members.each do |member|
17 33 = render 'users_groups/users_group', member: member, show_controls: true
18   -- if current_user.can? :manage_group, @group
19   - = render "new_group_member"
  34 += paginate @members, theme: 'gitlab'
  35 +
  36 +:coffeescript
  37 + $('form.member-search-form').on 'submit', (event) ->
  38 + event.preventDefault()
  39 + Turbolinks.visit @.action + '?' + $(@).serialize()
... ...
app/views/projects/issues/_form.html.haml
... ... @@ -24,7 +24,7 @@
24 24 %i.icon-user
25 25 Assign to
26 26 .col-sm-10
27   - = f.select(:assignee_id, assignee_options(@issue), { include_blank: "Select a user" }, {class: 'select2'})
  27 + = project_users_select_tag('issue[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control')
28 28 &nbsp;
29 29 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link'
30 30 .form-group
... ...
app/views/projects/issues/_head.html.haml
... ... @@ -17,10 +17,10 @@
17 17  
18 18 %li.pull-right
19 19 .pull-right
20   - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'inline issue-search-form' do
  20 + = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do
21 21 .append-right-10.hidden-xs.hidden-sm
22 22 = search_field_tag :issue_search, nil, { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' }
23 23 - if can? current_user, :write_issue, @project
24   - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
  24 + = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do
25 25 %i.icon-plus
26 26 New Issue
... ...
app/views/projects/issues/_issues.html.haml
1   -.ui-box
2   - .title
  1 +.append-bottom-10
  2 + .check-all-holder
3 3 = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left"
4   - .clearfix
5   - .issues_bulk_update.hide
6   - = form_tag bulk_update_project_issues_path(@project), method: :post do
7   - %span Update selected issues with &nbsp;
8   - = select_tag('update[status]', options_for_select(['open', 'closed']), prompt: "Status")
9   - = select_tag('update[assignee_id]', bulk_update_assignee_options, prompt: "Assignee")
10   - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone")
11   - = hidden_field_tag 'update[issues_ids]', []
12   - = hidden_field_tag :status, params[:status]
13   - = button_tag "Save", class: "btn update_selected_issues btn-small btn-save"
14   - .issues-filters
15   - %span Filter by
16   - .dropdown.inline.prepend-left-10
17   - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
18   - %i.icon-tags
19   - %span.light labels:
20   - - if params[:label_name].present?
21   - %strong= params[:label_name]
22   - - else
23   - Any
24   - %b.caret
25   - %ul.dropdown-menu
26   - %li
27   - = link_to project_filter_path(label_name: nil) do
28   - Any
29   - - issue_label_names.each do |label_name|
30   - %li
31   - = link_to project_filter_path(label_name: label_name) do
32   - %span{class: "label #{label_css_class(label_name)}"}
33   - %i.icon-tag
34   - = label_name
35   - .dropdown.inline.prepend-left-10
36   - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
37   - %i.icon-user
38   - %span.light assignee:
39   - - if @assignee.present?
40   - %strong= @assignee.name
41   - - elsif params[:assignee_id] == "0"
42   - Unassigned
43   - - else
44   - Any
45   - %b.caret
46   - %ul.dropdown-menu
47   - %li
48   - = link_to project_filter_path(assignee_id: nil) do
49   - Any
50   - = link_to project_filter_path(assignee_id: 0) do
51   - Unassigned
52   - - @project.team.members.sort_by(&:name).each do |user|
53   - %li
54   - = link_to project_filter_path(assignee_id: user.id) do
55   - = image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
56   - = user.name
  4 + .issues-filters
  5 + .dropdown.inline
  6 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
  7 + %i.icon-tags
  8 + %span.light labels:
  9 + - if params[:label_name].present?
  10 + %strong= params[:label_name]
  11 + - else
  12 + Any
  13 + %b.caret
  14 + %ul.dropdown-menu
  15 + %li
  16 + = link_to project_filter_path(label_name: nil) do
  17 + Any
  18 + - issue_label_names.each do |label_name|
  19 + %li
  20 + = link_to project_filter_path(label_name: label_name) do
  21 + %span{class: "label #{label_css_class(label_name)}"}
  22 + %i.icon-tag
  23 + = label_name
  24 + .dropdown.inline.prepend-left-10
  25 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
  26 + %i.icon-user
  27 + %span.light assignee:
  28 + - if @assignee.present?
  29 + %strong= @assignee.name
  30 + - elsif params[:assignee_id] == "0"
  31 + Unassigned
  32 + - else
  33 + Any
  34 + %b.caret
  35 + %ul.dropdown-menu
  36 + %li
  37 + = link_to project_filter_path(assignee_id: nil) do
  38 + Any
  39 + = link_to project_filter_path(assignee_id: 0) do
  40 + Unassigned
  41 + - @assignees.sort_by(&:name).each do |user|
  42 + %li
  43 + = link_to project_filter_path(assignee_id: user.id) do
  44 + = image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
  45 + = user.name
57 46  
58   - .dropdown.inline.prepend-left-10
59   - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
60   - %i.icon-time
61   - %span.light milestone:
62   - - if @milestone.present?
63   - %strong= @milestone.title
64   - - elsif params[:milestone_id] == "0"
65   - None (backlog)
66   - - else
67   - Any
68   - %b.caret
69   - %ul.dropdown-menu
70   - %li
71   - = link_to project_filter_path(milestone_id: nil) do
72   - Any
73   - = link_to project_filter_path(milestone_id: 0) do
74   - None (backlog)
75   - - project_active_milestones.each do |milestone|
76   - %li
77   - = link_to project_filter_path(milestone_id: milestone.id) do
78   - %strong= milestone.title
79   - %small.light= milestone.expires_at
  47 + .dropdown.inline.prepend-left-10
  48 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
  49 + %i.icon-time
  50 + %span.light milestone:
  51 + - if @milestone.present?
  52 + %strong= @milestone.title
  53 + - elsif params[:milestone_id] == "0"
  54 + None (backlog)
  55 + - else
  56 + Any
  57 + %b.caret
  58 + %ul.dropdown-menu
  59 + %li
  60 + = link_to project_filter_path(milestone_id: nil) do
  61 + Any
  62 + = link_to project_filter_path(milestone_id: 0) do
  63 + None (backlog)
  64 + - project_active_milestones.each do |milestone|
  65 + %li
  66 + = link_to project_filter_path(milestone_id: milestone.id) do
  67 + %strong= milestone.title
  68 + %small.light= milestone.expires_at
80 69  
81   - .pull-right
82   - = render 'shared/sort_dropdown'
  70 + .pull-right
  71 + = render 'shared/sort_dropdown'
83 72  
  73 + .clearfix
  74 + .issues_bulk_update.hide
  75 + = form_tag bulk_update_project_issues_path(@project), method: :post do
  76 + = select_tag('update[status]', options_for_select(['Open', 'Closed']), prompt: "Status")
  77 + = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee')
  78 + = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone")
  79 + = hidden_field_tag 'update[issues_ids]', []
  80 + = hidden_field_tag :status, params[:status]
  81 + = button_tag "Update issues", class: "btn update_selected_issues btn-save"
84 82  
  83 +.ui-box
85 84 %ul.well-list.issues-list
86 85 = render @issues
87 86 - if @issues.blank?
... ...
app/views/projects/merge_requests/_form.html.haml
... ... @@ -47,7 +47,7 @@
47 47 %i.icon-user
48 48 Assign to
49 49 .col-sm-10
50   - = f.select(:assignee_id, assignee_options(@merge_request), { include_blank: "Select a user" }, {class: 'select2'})
  50 + = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control')
51 51 &nbsp;
52 52 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link'
53 53 .form-group
... ...
app/views/projects/merge_requests/index.html.haml
... ... @@ -10,59 +10,57 @@
10 10 .col-md-3
11 11 = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project)
12 12 .col-md-9
13   - .ui-box
14   - .title
15   - .mr-filters
16   - %span Filter by
17   - .dropdown.inline.prepend-left-10
18   - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
19   - %i.icon-user
20   - %span.light assignee:
21   - - if @assignee.present?
22   - %strong= @assignee.name
23   - - elsif params[:assignee_id] == "0"
24   - Unassigned
25   - - else
26   - Any
27   - %b.caret
28   - %ul.dropdown-menu
29   - %li
30   - = link_to project_filter_path(assignee_id: nil) do
31   - Any
32   - = link_to project_filter_path(assignee_id: 0) do
33   - Unassigned
34   - - @project.team.members.sort_by(&:name).each do |user|
35   - %li
36   - = link_to project_filter_path(assignee_id: user.id) do
37   - = image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
38   - = user.name
  13 + .mr-filters.append-bottom-10
  14 + .dropdown.inline
  15 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
  16 + %i.icon-user
  17 + %span.light assignee:
  18 + - if @assignee.present?
  19 + %strong= @assignee.name
  20 + - elsif params[:assignee_id] == "0"
  21 + Unassigned
  22 + - else
  23 + Any
  24 + %b.caret
  25 + %ul.dropdown-menu
  26 + %li
  27 + = link_to project_filter_path(assignee_id: nil) do
  28 + Any
  29 + = link_to project_filter_path(assignee_id: 0) do
  30 + Unassigned
  31 + - @assignees.sort_by(&:name).each do |user|
  32 + %li
  33 + = link_to project_filter_path(assignee_id: user.id) do
  34 + = image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
  35 + = user.name
39 36  
40   - .dropdown.inline.prepend-left-10
41   - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
42   - %i.icon-time
43   - %span.light milestone:
44   - - if @milestone.present?
45   - %strong= @milestone.title
46   - - elsif params[:milestone_id] == "0"
47   - None (backlog)
48   - - else
49   - Any
50   - %b.caret
51   - %ul.dropdown-menu
52   - %li
53   - = link_to project_filter_path(milestone_id: nil) do
54   - Any
55   - = link_to project_filter_path(milestone_id: 0) do
56   - None (backlog)
57   - - project_active_milestones.each do |milestone|
58   - %li
59   - = link_to project_filter_path(milestone_id: milestone.id) do
60   - %strong= milestone.title
61   - %small.light= milestone.expires_at
  37 + .dropdown.inline.prepend-left-10
  38 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
  39 + %i.icon-time
  40 + %span.light milestone:
  41 + - if @milestone.present?
  42 + %strong= @milestone.title
  43 + - elsif params[:milestone_id] == "0"
  44 + None (backlog)
  45 + - else
  46 + Any
  47 + %b.caret
  48 + %ul.dropdown-menu
  49 + %li
  50 + = link_to project_filter_path(milestone_id: nil) do
  51 + Any
  52 + = link_to project_filter_path(milestone_id: 0) do
  53 + None (backlog)
  54 + - project_active_milestones.each do |milestone|
  55 + %li
  56 + = link_to project_filter_path(milestone_id: milestone.id) do
  57 + %strong= milestone.title
  58 + %small.light= milestone.expires_at
62 59  
63   - .pull-right
64   - = render 'shared/sort_dropdown'
  60 + .pull-right
  61 + = render 'shared/sort_dropdown'
65 62  
  63 + .ui-box
66 64 %ul.well-list.mr-list
67 65 = render @merge_requests
68 66 - if @merge_requests.blank?
... ...
app/views/projects/team_members/_group_members.html.haml
  1 +- group_users_count = @group.users_groups.count
1 2 .ui-box
2 3 .title
3 4 %strong #{@group.name}
4   - group members (#{@group.users_groups.count})
  5 + group members (#{group_users_count})
5 6 .pull-right
6 7 = link_to members_group_path(@group), class: 'btn btn-small' do
7 8 %i.icon-edit
8 9 %ul.well-list
9   - - @group.users_groups.order('group_access DESC').each do |member|
  10 + - @group.users_groups.order('group_access DESC').limit(20).each do |member|
10 11 = render 'users_groups/users_group', member: member, show_controls: false
  12 + - if group_users_count > 20
  13 + %li
  14 + and #{group_users_count - 20} more. For full list visit #{link_to 'group members page', members_group_path(@group)}
... ...
app/views/shared/_sort_dropdown.html.haml
1 1 .dropdown.inline.prepend-left-10
2   - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"}
  2 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
3 3 %span.light sort:
4 4 - if @sort.present?
5 5 = @sort
... ...
features/steps/group/group.rb
... ... @@ -29,6 +29,7 @@ class Groups &lt; Spinach::FeatureSteps
29 29  
30 30 And 'I select user "Mary Jane" from list with role "Reporter"' do
31 31 user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
  32 + click_link 'Add members'
32 33 within ".users-group-form" do
33 34 select2(user.id, from: "#user_ids", multiple: true)
34 35 select "Reporter", from: "group_access"
... ...
lib/api/entities.rb
... ... @@ -6,6 +6,12 @@ module API
6 6 expose :is_admin?, as: :is_admin
7 7 expose :can_create_group?, as: :can_create_group
8 8 expose :can_create_project?, as: :can_create_project
  9 +
  10 + expose :avatar_url do |user, options|
  11 + if user.avatar.present?
  12 + user.avatar.url
  13 + end
  14 + end
9 15 end
10 16  
11 17 class UserSafe < Grape::Entity
... ...
lib/api/projects.rb
... ... @@ -11,7 +11,7 @@ module API
11 11 end
12 12 not_found!
13 13 end
14   -
  14 +
15 15 def map_public_to_visibility_level(attrs)
16 16 publik = attrs.delete(:public)
17 17 publik = [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(publik)
... ... @@ -308,6 +308,18 @@ module API
308 308 projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%")
309 309 present paginate(projects), with: Entities::Project
310 310 end
  311 +
  312 +
  313 + # Get a users list
  314 + #
  315 + # Example Request:
  316 + # GET /users
  317 + get ':id/users' do
  318 + @users = User.where(id: user_project.team.users.map(&:id))
  319 + @users = @users.search(params[:search]) if params[:search].present?
  320 + @users = paginate @users
  321 + present @users, with: Entities::User
  322 + end
311 323 end
312 324 end
313 325 end
... ...