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,6 +3,7 @@
3 user_path: "/api/:version/users/:id.json" 3 user_path: "/api/:version/users/:id.json"
4 notes_path: "/api/:version/projects/:id/notes.json" 4 notes_path: "/api/:version/projects/:id/notes.json"
5 namespaces_path: "/api/:version/namespaces.json" 5 namespaces_path: "/api/:version/namespaces.json"
  6 + project_users_path: "/api/:version/projects/:id/users.json"
6 7
7 # Get 20 (depends on api) recent notes 8 # Get 20 (depends on api) recent notes
8 # and sort the ascending from oldest to newest 9 # and sort the ascending from oldest to newest
@@ -50,6 +51,23 @@ @@ -50,6 +51,23 @@
50 ).done (users) -> 51 ).done (users) ->
51 callback(users) 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 # Return namespaces list. Filtered by query 71 # Return namespaces list. Filtered by query
54 namespaces: (query, callback) -> 72 namespaces: (query, callback) ->
55 url = Api.buildUrl(Api.namespaces_path) 73 url = Api.buildUrl(Api.namespaces_path)
app/assets/javascripts/project_users_select.js.coffee 0 → 100644
@@ -0,0 +1,44 @@ @@ -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 userFormatResult = (user) -> 2 userFormatResult = (user) ->
3 - if user.avatar  
4 - avatar = user.avatar.url 3 + if user.avatar_url
  4 + avatar = user.avatar_url
5 else if gon.gravatar_enabled 5 else if gon.gravatar_enabled
6 avatar = gon.gravatar_url 6 avatar = gon.gravatar_url
7 avatar = avatar.replace('%{hash}', md5(user.email)) 7 avatar = avatar.replace('%{hash}', md5(user.email))
app/assets/stylesheets/application.scss
@@ -65,6 +65,7 @@ @@ -65,6 +65,7 @@
65 @import "sections/wall.scss"; 65 @import "sections/wall.scss";
66 @import "sections/dashboard.scss"; 66 @import "sections/dashboard.scss";
67 @import "sections/stat_graph.scss"; 67 @import "sections/stat_graph.scss";
  68 +@import "sections/groups.scss";
68 69
69 /** 70 /**
70 * Code ighlight 71 * Code ighlight
app/assets/stylesheets/generic/common.scss
@@ -112,6 +112,7 @@ pre.well-pre { @@ -112,6 +112,7 @@ pre.well-pre {
112 .dropdown-menu > li > a:hover, 112 .dropdown-menu > li > a:hover,
113 .dropdown-menu > li > a:focus { 113 .dropdown-menu > li > a:focus {
114 background: #29b; 114 background: #29b;
  115 + color: #FFF
115 } 116 }
116 117
117 .breadcrumb > li + li:before { 118 .breadcrumb > li + li:before {
app/assets/stylesheets/generic/forms.scss
@@ -51,3 +51,27 @@ label { @@ -51,3 +51,27 @@ label {
51 .input-mn-300 { 51 .input-mn-300 {
52 min-width: 300px; 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 /** Select2 selectbox style override **/ 1 /** Select2 selectbox style override **/
2 -  
3 .select2-container, .select2-container.select2-drop-above { 2 .select2-container, .select2-container.select2-drop-above {
4 .select2-choice { 3 .select2-choice {
5 background: #FFF; 4 background: #FFF;
@@ -12,9 +11,13 @@ @@ -12,9 +11,13 @@
12 } 11 }
13 12
14 .select2-drop-active { 13 .select2-drop-active {
15 - border: 1px solid #BBB; 14 + border: 1px solid #BBB !important;
16 margin-top: 4px; 15 margin-top: 4px;
17 16
  17 + &.select2-drop-above {
  18 + margin-bottom: 8px;
  19 + }
  20 +
18 .select2-search input { 21 .select2-search input {
19 background: #fafafa; 22 background: #fafafa;
20 border-color: #DDD; 23 border-color: #DDD;
@@ -78,3 +81,9 @@ select { @@ -78,3 +81,9 @@ select {
78 .project-refs-form .select2-container { 81 .project-refs-form .select2-container {
79 margin-right: 10px; 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 @@ @@ -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,8 +14,8 @@
14 14
15 .issue-check { 15 .issue-check {
16 float: left; 16 float: left;
17 - padding: 8px 0;  
18 padding-right: 8px; 17 padding-right: 8px;
  18 + margin-bottom: 10px;
19 min-width: 15px; 19 min-width: 15px;
20 } 20 }
21 21
@@ -38,13 +38,21 @@ @@ -38,13 +38,21 @@
38 } 38 }
39 } 39 }
40 40
41 -input.check_all_issues { 41 +.check-all-holder {
  42 + height: 32px;
42 float: left; 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 .issues_content { 58 .issues_content {
@@ -91,6 +99,13 @@ input.check_all_issues { @@ -91,6 +99,13 @@ input.check_all_issues {
91 .update_selected_issues { 99 .update_selected_issues {
92 margin-left: 4px; 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,7 +63,14 @@ class GroupsController &lt; ApplicationController
63 63
64 def members 64 def members
65 @project = group.projects.find(params[:project_id]) if params[:project_id] 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 @users_group = UsersGroup.new 74 @users_group = UsersGroup.new
68 end 75 end
69 76
app/controllers/projects/issues_controller.rb
@@ -28,7 +28,7 @@ class Projects::IssuesController &lt; Projects::ApplicationController @@ -28,7 +28,7 @@ class Projects::IssuesController &lt; Projects::ApplicationController
28 @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? 28 @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
29 sort_param = params[:sort] || 'newest' 29 sort_param = params[:sort] || 'newest'
30 @sort = sort_param.humanize unless sort_param.empty? 30 @sort = sort_param.humanize unless sort_param.empty?
31 - 31 + @assignees = User.where(id: @project.issues.pluck(:assignee_id))
32 32
33 respond_to do |format| 33 respond_to do |format|
34 format.html 34 format.html
app/controllers/projects/merge_requests_controller.rb
@@ -28,6 +28,7 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController @@ -28,6 +28,7 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController
28 assignee_id, milestone_id = params[:assignee_id], params[:milestone_id] 28 assignee_id, milestone_id = params[:assignee_id], params[:milestone_id]
29 @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero? 29 @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
30 @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero? 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 end 32 end
32 33
33 def show 34 def show
app/helpers/application_helper.rb
@@ -162,15 +162,6 @@ module ApplicationHelper @@ -162,15 +162,6 @@ module ApplicationHelper
162 162
163 alias_method :url_to_image, :image_url 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 def body_data_page 165 def body_data_page
175 path = controller.controller_path.split('/') 166 path = controller.controller_path.split('/')
176 namespace = path.first if path.second 167 namespace = path.first if path.second
app/helpers/issues_helper.rb
@@ -70,11 +70,11 @@ module IssuesHelper @@ -70,11 +70,11 @@ module IssuesHelper
70 end 70 end
71 71
72 def bulk_update_milestone_options 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 end 74 end
75 75
76 def bulk_update_assignee_options 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 end 78 end
79 79
80 def assignee_options object 80 def assignee_options object
app/helpers/selects_helper.rb 0 → 100644
@@ -0,0 +1,20 @@ @@ -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 = form_for @users_group, url: group_users_groups_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| 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 .form-group 2 .form-group
9 = f.label :user_ids, "People", class: 'control-label' 3 = f.label :user_ids, "People", class: 'control-label'
10 .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') 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 .form-group 6 .form-group
14 = f.label :group_access, "Group Access", class: 'control-label' 7 = f.label :group_access, "Group Access", class: 'control-label'
15 .col-sm-10= select_tag :group_access, options_for_select(UsersGroup.group_access_roles, @users_group.group_access), class: "project-access-select select2" 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,14 +6,34 @@
6 %strong= link_to "here", help_permissions_path, class: "vlink" 6 %strong= link_to "here", help_permissions_path, class: "vlink"
7 7
8 %hr 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 .title 26 .title
11 %strong #{@group.name} 27 %strong #{@group.name}
12 group members 28 group members
13 %small 29 %small
14 - (#{@members.count}) 30 + (#{@members.total_count})
15 %ul.well-list 31 %ul.well-list
16 - @members.each do |member| 32 - @members.each do |member|
17 = render 'users_groups/users_group', member: member, show_controls: true 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,7 +24,7 @@
24 %i.icon-user 24 %i.icon-user
25 Assign to 25 Assign to
26 .col-sm-10 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 &nbsp; 28 &nbsp;
29 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' 29 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link'
30 .form-group 30 .form-group
app/views/projects/issues/_head.html.haml
@@ -17,10 +17,10 @@ @@ -17,10 +17,10 @@
17 17
18 %li.pull-right 18 %li.pull-right
19 .pull-right 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 .append-right-10.hidden-xs.hidden-sm 21 .append-right-10.hidden-xs.hidden-sm
22 = search_field_tag :issue_search, nil, { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } 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 - if can? current_user, :write_issue, @project 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 %i.icon-plus 25 %i.icon-plus
26 New Issue 26 New Issue
app/views/projects/issues/_issues.html.haml
1 -.ui-box  
2 - .title 1 +.append-bottom-10
  2 + .check-all-holder
3 = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" 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 %ul.well-list.issues-list 84 %ul.well-list.issues-list
86 = render @issues 85 = render @issues
87 - if @issues.blank? 86 - if @issues.blank?
app/views/projects/merge_requests/_form.html.haml
@@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
47 %i.icon-user 47 %i.icon-user
48 Assign to 48 Assign to
49 .col-sm-10 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 &nbsp; 51 &nbsp;
52 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link' 52 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link'
53 .form-group 53 .form-group
app/views/projects/merge_requests/index.html.haml
@@ -10,59 +10,57 @@ @@ -10,59 +10,57 @@
10 .col-md-3 10 .col-md-3
11 = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project) 11 = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project)
12 .col-md-9 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 %ul.well-list.mr-list 64 %ul.well-list.mr-list
67 = render @merge_requests 65 = render @merge_requests
68 - if @merge_requests.blank? 66 - if @merge_requests.blank?
app/views/projects/team_members/_group_members.html.haml
  1 +- group_users_count = @group.users_groups.count
1 .ui-box 2 .ui-box
2 .title 3 .title
3 %strong #{@group.name} 4 %strong #{@group.name}
4 - group members (#{@group.users_groups.count}) 5 + group members (#{group_users_count})
5 .pull-right 6 .pull-right
6 = link_to members_group_path(@group), class: 'btn btn-small' do 7 = link_to members_group_path(@group), class: 'btn btn-small' do
7 %i.icon-edit 8 %i.icon-edit
8 %ul.well-list 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 = render 'users_groups/users_group', member: member, show_controls: false 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 .dropdown.inline.prepend-left-10 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 %span.light sort: 3 %span.light sort:
4 - if @sort.present? 4 - if @sort.present?
5 = @sort 5 = @sort
features/steps/group/group.rb
@@ -29,6 +29,7 @@ class Groups &lt; Spinach::FeatureSteps @@ -29,6 +29,7 @@ class Groups &lt; Spinach::FeatureSteps
29 29
30 And 'I select user "Mary Jane" from list with role "Reporter"' do 30 And 'I select user "Mary Jane" from list with role "Reporter"' do
31 user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane") 31 user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
  32 + click_link 'Add members'
32 within ".users-group-form" do 33 within ".users-group-form" do
33 select2(user.id, from: "#user_ids", multiple: true) 34 select2(user.id, from: "#user_ids", multiple: true)
34 select "Reporter", from: "group_access" 35 select "Reporter", from: "group_access"
lib/api/entities.rb
@@ -6,6 +6,12 @@ module API @@ -6,6 +6,12 @@ module API
6 expose :is_admin?, as: :is_admin 6 expose :is_admin?, as: :is_admin
7 expose :can_create_group?, as: :can_create_group 7 expose :can_create_group?, as: :can_create_group
8 expose :can_create_project?, as: :can_create_project 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 end 15 end
10 16
11 class UserSafe < Grape::Entity 17 class UserSafe < Grape::Entity
lib/api/projects.rb
@@ -11,7 +11,7 @@ module API @@ -11,7 +11,7 @@ module API
11 end 11 end
12 not_found! 12 not_found!
13 end 13 end
14 - 14 +
15 def map_public_to_visibility_level(attrs) 15 def map_public_to_visibility_level(attrs)
16 publik = attrs.delete(:public) 16 publik = attrs.delete(:public)
17 publik = [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(publik) 17 publik = [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(publik)
@@ -308,6 +308,18 @@ module API @@ -308,6 +308,18 @@ module API
308 projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%") 308 projects = Project.where("(id in (?) OR visibility_level in (?)) AND (name LIKE (?))", ids, visibility_levels, "%#{params[:query]}%")
309 present paginate(projects), with: Entities::Project 309 present paginate(projects), with: Entities::Project
310 end 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 end 323 end
312 end 324 end
313 end 325 end