Defect #35192 » watcher-dialog-load-more.patch
| app/assets/javascripts/application-legacy.js | ||
|---|---|---|
| 859 | 859 |
$form.on('ajax:complete', 'a[data-remote]', function() {
|
| 860 | 860 |
restoreChecked(); |
| 861 | 861 |
}); |
| 862 |
// Restore selection after "load more" appends users to the list |
|
| 863 |
$form.on('redmine:watcherUsersUpdated', function() {
|
|
| 864 |
restoreChecked(); |
|
| 865 |
}); |
|
| 862 | 866 |
} |
| 863 | 867 | |
| 864 | 868 |
var check = function() {
|
| app/controllers/watchers_controller.rb | ||
|---|---|---|
| 18 | 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 19 | 19 | |
| 20 | 20 |
class WatchersController < ApplicationController |
| 21 |
# Number of users listed per page in the "add watcher" dialog |
|
| 22 |
WATCHER_USERS_PER_PAGE = 100 unless const_defined?(:WATCHER_USERS_PER_PAGE) |
|
| 23 | ||
| 21 | 24 |
before_action :require_login, :find_watchables, :only => [:watch, :unwatch] |
| 22 | 25 | |
| 23 | 26 |
def watch |
| ... | ... | |
| 150 | 153 | |
| 151 | 154 |
def users_for_new_watcher |
| 152 | 155 |
scope = nil |
| 156 |
limit = WATCHER_USERS_PER_PAGE |
|
| 157 |
offset = params[:offset].to_i |
|
| 158 |
offset = 0 if offset < 0 |
|
| 153 | 159 |
if params[:q].blank? |
| 154 | 160 |
if @project.present? |
| 155 | 161 |
scope = @project.principals.assignable_watchers |
| ... | ... | |
| 157 | 163 |
scope = Principal.joins(:members).where(:members => { :project_id => @projects }).assignable_watchers.distinct
|
| 158 | 164 |
end |
| 159 | 165 |
else |
| 160 |
scope = Principal.assignable_watchers.limit(100) |
|
| 166 |
# A search query already caps the result, so no "load more" is needed |
|
| 167 |
scope = Principal.assignable_watchers.limit(limit) |
|
| 168 |
end |
|
| 169 |
scope = scope.sorted.like(params[:q]) |
|
| 170 |
if params[:q].blank? |
|
| 171 |
# Fetch one extra record to know whether a further page exists |
|
| 172 |
users = scope.offset(offset).limit(limit + 1).to_a |
|
| 173 |
@more_users_for_watcher = users.size > limit |
|
| 174 |
@watcher_users_next_offset = offset + limit |
|
| 175 |
users = users.first(limit) |
|
| 176 |
else |
|
| 177 |
@more_users_for_watcher = false |
|
| 178 |
users = scope.to_a |
|
| 161 | 179 |
end |
| 162 |
users = scope.sorted.like(params[:q]).to_a |
|
| 163 | 180 |
if @watchables && @watchables.size == 1 |
| 164 | 181 |
watchable_object = @watchables.first |
| 165 | 182 |
users -= watchable_object.visible_watcher_users |
| app/javascript/controllers/load_more_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 |
import { get } from "@rails/request.js"
|
|
| 3 | ||
| 4 |
// Loads the next page of a list and appends it in place of the "load more" |
|
| 5 |
// trigger. The server is expected to return an HTML fragment (the next page of |
|
| 6 |
// items, optionally followed by a new trigger). |
|
| 7 |
export default class extends Controller {
|
|
| 8 |
static targets = ["trigger"] |
|
| 9 |
static values = { afterLoadEvent: String }
|
|
| 10 | ||
| 11 |
async load(event) {
|
|
| 12 |
event.preventDefault() |
|
| 13 | ||
| 14 |
// Keep a direct reference to the clicked trigger: the fragment we insert may |
|
| 15 |
// contain a new trigger, so resolving triggerTarget again would be ambiguous. |
|
| 16 |
const trigger = this.triggerTarget |
|
| 17 | ||
| 18 |
const response = await get(event.currentTarget.href, { responseKind: "html" })
|
|
| 19 |
if (!response.ok) {
|
|
| 20 |
return |
|
| 21 |
} |
|
| 22 | ||
| 23 |
trigger.insertAdjacentHTML("beforebegin", await response.html)
|
|
| 24 |
trigger.remove() |
|
| 25 |
this.dispatchAfterLoadEvent() |
|
| 26 |
} |
|
| 27 | ||
| 28 |
dispatchAfterLoadEvent() {
|
|
| 29 |
if (this.hasAfterLoadEventValue) {
|
|
| 30 |
this.element.dispatchEvent(new CustomEvent(this.afterLoadEventValue, { bubbles: true }))
|
|
| 31 |
} |
|
| 32 |
} |
|
| 33 |
} |
|
| app/views/watchers/_new.html.erb | ||
|---|---|---|
| 38 | 38 |
{checkboxSelector: 'input[name=\"watcher[user_ids][]\"]'}
|
| 39 | 39 |
)" |
| 40 | 40 |
) %> |
| 41 |
<div id="users_for_watcher"> |
|
| 42 |
<%= principals_check_box_tags('watcher[user_ids][]', users) %>
|
|
| 41 |
<div id="users_for_watcher" |
|
| 42 |
data-controller="load-more" |
|
| 43 |
data-load-more-after-load-event-value="redmine:watcherUsersUpdated"> |
|
| 44 |
<%= render :partial => 'watchers/users_for_watcher', :locals => {:users => users} %>
|
|
| 43 | 45 |
</div> |
| 44 | 46 | |
| 45 | 47 |
<p class="buttons"> |
| app/views/watchers/_users_for_watcher.html.erb | ||
|---|---|---|
| 1 |
<%= principals_check_box_tags 'watcher[user_ids][]', users %> |
|
| 2 | ||
| 3 |
<% if @more_users_for_watcher %> |
|
| 4 |
<p class="watcher-more-users" data-load-more-target="trigger"> |
|
| 5 |
<%= link_to( |
|
| 6 |
l(:label_more), |
|
| 7 |
url_for( |
|
| 8 |
:controller => 'watchers', |
|
| 9 |
:action => 'autocomplete_for_user', |
|
| 10 |
:object_type => params[:object_type], |
|
| 11 |
:object_id => params[:object_id], |
|
| 12 |
:project_id => params[:project_id], |
|
| 13 |
:q => params[:q], |
|
| 14 |
:offset => @watcher_users_next_offset |
|
| 15 |
), |
|
| 16 |
:data => {:action => 'load-more#load'}
|
|
| 17 |
) %> |
|
| 18 |
</p> |
|
| 19 |
<% end %> |
|
| app/views/watchers/autocomplete_for_user.html.erb | ||
|---|---|---|
| 1 |
<%= principals_check_box_tags 'watcher[user_ids][]', @users %> |
|
| 1 |
<%= render :partial => 'watchers/users_for_watcher', :locals => {:users => @users} %>
|
|
| config/locales/en.yml | ||
|---|---|---|
| 792 | 792 |
label_nobody: nobody |
| 793 | 793 |
label_next: Next |
| 794 | 794 |
label_previous: Previous |
| 795 |
label_more: More |
|
| 795 | 796 |
label_used_by: Used by |
| 796 | 797 |
label_details: Details |
| 797 | 798 |
label_add_note: Add a note |
| config/locales/ja.yml | ||
|---|---|---|
| 602 | 602 |
label_nobody: 無記名 |
| 603 | 603 |
label_next: 次 |
| 604 | 604 |
label_previous: 前 |
| 605 |
label_more: さらに表示 |
|
| 605 | 606 |
label_used_by: 使用中 |
| 606 | 607 |
label_details: 詳細 |
| 607 | 608 |
label_add_note: コメントを追加 |
| test/functional/watchers_controller_test.rb | ||
|---|---|---|
| 516 | 516 |
assert_select 'input[name=?][value="10"]', 'watcher[user_ids][]' |
| 517 | 517 |
end |
| 518 | 518 | |
| 519 |
def test_autocomplete_without_query_should_not_paginate_within_a_page |
|
| 520 |
@request.session[:user_id] = 2 |
|
| 521 |
get :autocomplete_for_user, :params => {:project_id => 'ecookbook'}, :xhr => true
|
|
| 522 |
assert_response :success |
|
| 523 |
assert_select 'input[name=?]', 'watcher[user_ids][]', :count => 2 |
|
| 524 |
assert_select 'p.watcher-more-users', :count => 0 |
|
| 525 |
end |
|
| 526 | ||
| 527 |
def test_autocomplete_without_query_should_load_more_users |
|
| 528 |
project = Project.find(1) |
|
| 529 |
WatchersController::WATCHER_USERS_PER_PAGE.times do |
|
| 530 |
User.add_to_project(User.generate!, project) |
|
| 531 |
end |
|
| 532 | ||
| 533 |
@request.session[:user_id] = 2 |
|
| 534 |
get :autocomplete_for_user, :params => {:project_id => 'ecookbook'}, :xhr => true
|
|
| 535 |
assert_response :success |
|
| 536 |
assert_select 'input[name=?]', 'watcher[user_ids][]', |
|
| 537 |
:count => WatchersController::WATCHER_USERS_PER_PAGE |
|
| 538 |
assert_select 'p.watcher-more-users a[data-action=?][href*=?]', |
|
| 539 |
'load-more#load', "offset=#{WatchersController::WATCHER_USERS_PER_PAGE}"
|
|
| 540 |
# The old submit-style onclick handler must not be used |
|
| 541 |
assert_select 'a[onclick]', false |
|
| 542 | ||
| 543 |
get :autocomplete_for_user, |
|
| 544 |
:params => {:project_id => 'ecookbook',
|
|
| 545 |
:offset => WatchersController::WATCHER_USERS_PER_PAGE}, |
|
| 546 |
:xhr => true |
|
| 547 |
assert_response :success |
|
| 548 |
# eCookbook has 2 fixture watchers, so the second page holds the remaining 2 |
|
| 549 |
assert_select 'input[name=?]', 'watcher[user_ids][]', :count => 2 |
|
| 550 |
assert_select 'p.watcher-more-users', :count => 0 |
|
| 551 |
end |
|
| 552 | ||
| 553 |
def test_autocomplete_with_query_should_not_paginate |
|
| 554 |
(WatchersController::WATCHER_USERS_PER_PAGE + 1).times do |i| |
|
| 555 |
User.generate!(:firstname => 'Loadmore', :lastname => "User#{i}")
|
|
| 556 |
end |
|
| 557 | ||
| 558 |
@request.session[:user_id] = 2 |
|
| 559 |
get :autocomplete_for_user, |
|
| 560 |
:params => {:project_id => 'ecookbook', :q => 'loadmore'}, :xhr => true
|
|
| 561 |
assert_response :success |
|
| 562 |
# A search query is capped by limit and never shows a "load more" trigger |
|
| 563 |
assert_select 'input[name=?]', 'watcher[user_ids][]', |
|
| 564 |
:count => WatchersController::WATCHER_USERS_PER_PAGE |
|
| 565 |
assert_select 'p.watcher-more-users', :count => 0 |
|
| 566 |
end |
|
| 567 | ||
| 568 |
def test_new_should_wire_load_more_controller |
|
| 569 |
@request.session[:user_id] = 2 |
|
| 570 |
get :new, :params => {:object_type => 'issue', :object_id => '2'}, :xhr => true
|
|
| 571 |
assert_response :success |
|
| 572 |
assert_match /data-controller=\\"load-more\\"/, response.body |
|
| 573 |
end |
|
| 574 | ||
| 519 | 575 |
def test_append |
| 520 | 576 |
@request.session[:user_id] = 2 |
| 521 | 577 |
assert_no_difference 'Watcher.count' do |