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 | 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) | ... | ... |
... | ... | @@ -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
app/assets/stylesheets/application.scss
app/assets/stylesheets/generic/common.scss
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/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 < 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 < 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 < 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 | ... | ... |
... | ... | @@ -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 | |
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 | |
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 | |
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
features/steps/group/group.rb
... | ... | @@ -29,6 +29,7 @@ class Groups < 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 | ... | ... |