Commit f745c6e5b5aa84a882a73ed00e8c3c6d28c70657
Exists in
spb-stable
and in
3 other branches
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:Group#members page
Project#issues page
Project#merge_requests page
Project#new_issue page
Project#new_mr page
Project#members page
Showing
27 changed files
with
345 additions
and
172 deletions
Show diff stats
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) |
@@ -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/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 < ApplicationController | @@ -63,7 +63,14 @@ class GroupsController < 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 < Projects::ApplicationController | @@ -28,7 +28,7 @@ class Projects::IssuesController < 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 < Projects::ApplicationController | @@ -28,6 +28,7 @@ class Projects::MergeRequestsController < 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 |
@@ -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 | | 28 | |
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 | ||
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 | | 51 | |
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 < Spinach::FeatureSteps | @@ -29,6 +29,7 @@ class Groups < 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 |