Project

General

Profile

Defect #35192 » watcher-dialog-load-more.patch

Takenori TAKAKI, 2026-07-30 06:30

View differences:

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
    (1-1/1)