Project

General

Profile

Patch #43032 » v2_01_bookmark_filters.patch

Leonid Murin, 2025-12-19 15:18

View differences:

app/helpers/queries_helper.rb
26 26
    ungrouped = []
27 27
    grouped = {label_string: [], label_date: [], label_time_tracking: [], label_attachment: []}
28 28
    query.available_filters.map do |field, field_options|
29
      if /^cf_\d+\./.match?(field)
30
        group = (field_options[:through] || field_options[:field]).try(:name)
31
      elsif field =~ /^(.+)\./
32
        # association filters
33
        group = :"field_#{$1}"
34
      elsif field_options[:type] == :relation
35
        group = :label_relations
36
      elsif field_options[:type] == :tree
37
        group = query.is_a?(IssueQuery) ? :label_relations : nil
38
      elsif %w(member_of_group assigned_to_role).include?(field)
39
        group = :field_assigned_to
40
      elsif field_options[:type] == :date_past || field_options[:type] == :date
41
        group = :label_date
42
      elsif %w(estimated_hours spent_time).include?(field)
43
        group = :label_time_tracking
44
      elsif %w(attachment attachment_description).include?(field)
45
        group = :label_attachment
46
      elsif [:string, :text, :search].include?(field_options[:type])
47
        group = :label_string
48
      end
49
      if group
29
      if (group = group_for_filter(field, field_options, query))
50 30
        (grouped[group] ||= []) << [field_options[:name], field]
51 31
      else
52 32
        ungrouped << [field_options[:name], field]
......
66 46
    s
67 47
  end
68 48

  
49
  def group_for_filter(field, field_options, query)
50
    if /^cf_\d+\./.match?(field)
51
      (field_options[:through] || field_options[:field]).try(:name)
52
    elsif field =~ /^(.+)\./
53
      # association filters
54
      :"field_#{$1}"
55
    elsif field_options[:type] == :relation
56
      :label_relations
57
    elsif field_options[:type] == :tree
58
      query.is_a?(IssueQuery) ? :label_relations : nil
59
    elsif %w(member_of_group assigned_to_role).include?(field)
60
      :field_assigned_to
61
    elsif field_options[:type] == :date_past || field_options[:type] == :date
62
      :label_date
63
    elsif %w(estimated_hours spent_time).include?(field)
64
      :label_time_tracking
65
    elsif %w(attachment attachment_description).include?(field)
66
      :label_attachment
67
    elsif [:string, :text, :search].include?(field_options[:type])
68
      :label_string
69
    end
70
  end
71

  
69 72
  def query_filters_hidden_tags(query)
70 73
    tags = ''.html_safe
71 74
    query.filters.each do |field, options|
72
- 
app/assets/stylesheets/application.css
780 780

  
781 781
.add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
782 782

  
783
select#bookmarked_filters {
784
  max-width: 100%;
785
}
786

  
783 787
#issue_is_private_wrap {float:right; margin-right:1em;}
784 788
.toggle-multiselect { margin-right:5px; cursor:pointer;}
785 789
.buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
app/controllers/queries_controller.rb
22 22
  layout :query_layout
23 23

  
24 24
  before_action :find_query, :only => [:edit, :update, :destroy]
25
  before_action :find_optional_project, :only => [:new, :create]
25
  before_action :find_optional_project, :only => [:new, :create, :save_bookmarked_filters]
26 26

  
27 27
  accept_api_auth :index
28 28

  
......
119 119
    super unless query_layout == 'admin'
120 120
  end
121 121

  
122
  def save_bookmarked_filters
123
    respond_to do |format|
124
      format.js do
125
        @query = query_class.new
126
        @query.user = ::User.current
127
        @query.project = @project
128
        ::User.current.pref.save_bookmarked_query_filters(@query, params[:bookmarked_filters] || [])
129
      end
130
      format.any { render_404 }
131
    end
132
  end
133

  
122 134
  private
123 135

  
124 136
  def find_query
app/helpers/queries_helper.rb
25 25
  def filters_options_for_select(query)
26 26
    ungrouped = []
27 27
    grouped = {label_string: [], label_date: [], label_time_tracking: [], label_attachment: []}
28
    query.available_filters.map do |field, field_options|
29
      if (group = group_for_filter(field, field_options, query))
30
        (grouped[group] ||= []) << [field_options[:name], field]
31
      else
32
        ungrouped << [field_options[:name], field]
28
    bookmarks = bookmarked_filters(query)
29
    query.available_filters.
30
      sort_by { |field, filter| sort_bookmarks(field, filter, bookmarks) }.
31
      map do |field, field_options|
32
        if bookmarks.include?(field)
33
          ungrouped << ["#{field_options[:name]}\u2b50", field]
34
        elsif (group = group_for_filter(field, field_options, query))
35
          (grouped[group] ||= []) << [field_options[:name], field]
36
        else
37
          ungrouped << [field_options[:name], field]
38
        end
33 39
      end
34
    end
35 40
    # Remove empty groups
36 41
    grouped.delete_if {|k, v| v.empty?}
37 42
    # Don't group dates if there's only one (eg. time entries filters)
......
46 51
    s
47 52
  end
48 53

  
54
  def bookmarked_filters(query)
55
    ::User.current.pref.bookmarked_query_filters(query.class)
56
  end
57

  
58
  ONE_ARRAY = [1].freeze
59
  def sort_bookmarks(field, filter, bookmarks)
60
    bookmarks.include?(field) ? [0, filter[:name].downcase] : ONE_ARRAY
61
  end
62

  
49 63
  def group_for_filter(field, field_options, query)
50 64
    if /^cf_\d+\./.match?(field)
51 65
      (field_options[:through] || field_options[:field]).try(:name)
......
81 95
    tags
82 96
  end
83 97

  
98
  def bookmarked_filters_options(query)
99
    options_for_select(
100
      query.available_filters.map { |id, filter| [filter[:name], id] }.sort_by(&:first),
101
      selected_bookmarked_filters(query)
102
    )
103
  end
104

  
105
  def selected_bookmarked_filters(query)
106
    query.available_filters.keys &
107
      User.current.pref.bookmarked_query_filters(query.class)
108
  end
109
  private :selected_bookmarked_filters
110

  
111
  def path_to_save_bookmarked_filters
112
    if @project
113
      save_bookmarked_filters_project_queries_path(@project)
114
    else
115
      save_bookmarked_filters_queries_path
116
    end
117
  end
118

  
84 119
  def query_columns_hidden_tags(query)
85 120
    tags = ''.html_safe
86 121
    query.columns.each do |column|
/dev/null → app/javascript/controllers/bookmarked_filters_controller.js
1
import { Controller } from "@hotwired/stimulus";
2

  
3
export default class extends Controller {
4
  static values = {
5
    url: String
6
  }
7

  
8
  connect() {
9
    // Don't send select value on form submission
10
    this.element.removeAttribute('name')
11
  }
12

  
13
  saveBookmarkedFilters(event) {
14
    const values = Array.from(event.target.selectedOptions).map(({ value }) => value);
15
    $.ajax({
16
      url: this.urlValue,
17
      type: 'post',
18
      data: {
19
        bookmarked_filters: values,
20
        ...event.params
21
      }
22
    });
23
  }
24
}
app/models/user_preference.rb
211 211
    my_page_settings.keep_if {|block, settings| blocks.include?(block)}
212 212
  end
213 213
  private :clear_unused_block_settings
214

  
215
  def bookmarked_query_filters(query_class)
216
    bookmarked_filters[query_class.to_s.underscore] || []
217
  end
218

  
219
  def save_bookmarked_query_filters(query, filter_names)
220
    query_filters = bookmarked_query_filters(query.class)
221
    old_selected_filters = query.available_filters.keys & query_filters
222

  
223
    deleted = old_selected_filters - filter_names
224
    added = filter_names - old_selected_filters
225

  
226
    self.bookmarked_filters = {query.class.to_s.underscore => query_filters - deleted + added}
227
    save!
228
  end
229

  
230
  def bookmarked_filters
231
    self[:bookmarked_filters] || {}
232
  end
233
  private :bookmarked_filters
234

  
235
  def bookmarked_filters=(value)
236
    self[:bookmarked_filters] = bookmarked_filters.merge!(value)
237
  end
238
  private :bookmarked_filters=
214 239
end
app/views/calendars/show.html.erb
17 17
      <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
18 18
    </div>
19 19
  </fieldset>
20

  
21
  <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %>
20 22
</div>
21 23

  
22 24
<span class="contextual pagination">
app/views/gantts/_query_form.html.erb
86 86
          </div>
87 87
        </div>
88 88
      </fieldset>
89

  
90
      <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %>
89 91
    </div>
90 92

  
91 93
    <p class="contextual">
/dev/null → app/views/queries/_bookmarked_filters.html.erb
1
<fieldset id="bookmarked-filters" class="collapsible collapsed">
2
  <legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
3
    <%= sprite_icon("angle-right", rtl: true) %>
4
    <%= t(:label_bookmarked_filters) %>
5
  </legend>
6
  <div class="hidden">
7
    <%= select_tag 'bookmarked_filters',
8
                   bookmarked_filters_options(query),
9
                   multiple: true,
10
                   data: {
11
                     controller: 'bookmarked-filters',
12
                     action: 'bookmarked-filters#saveBookmarkedFilters',
13
                     'bookmarked-filters-url-value': path_to_save_bookmarked_filters,
14
                     'bookmarked-filters-type-param': query.class.to_s
15
                   }
16
    %>
17
  </div>
18
</fieldset>
/dev/null → app/views/queries/_filter_select.html.erb
1
<div class="add-filter">
2
  <%= label_tag('add_filter_select', l(:label_filter_add)) %>
3
  <%= select_tag 'add_filter_select', filters_options_for_select(query), name: nil %>
4
</div>
app/views/queries/_filters.html.erb
17 17
<div id="filters-table">
18 18
</div>
19 19

  
20
<div class="add-filter">
21
<%= label_tag('add_filter_select', l(:label_filter_add)) %>
22
<%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
23
</div>
20
<%= render partial: 'queries/filter_select', locals: {query: @query} %>
24 21

  
25 22
<%= hidden_field_tag 'f[]', '' %>
26 23
<% include_calendar_headers_tags %>
app/views/queries/_query_form.html.erb
56 56
      </div>
57 57
    </fieldset>
58 58
  <% end %>
59

  
60
  <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %>
59 61
</div>
60 62

  
61 63
<p class="buttons">
/dev/null → app/views/queries/save_bookmarked_filters.js.erb
1
$('.add-filter').replaceWith('<%=
2
  escape_javascript(render(partial: 'queries/filter_select', locals: {query: @query}))
3
%>');
4

  
5
$('#add_filter_select').change(function() {
6
  addFilter($(this).val(), '', []);
7
});
8

  
9
(function() {
10
  // Block already added filters in select
11
  const presentFilters = $('#filters-table .field input').map((_, filter) => filter.value);
12
  $('#add_filter_select').find('option').each(function () {
13
    if ($.inArray($(this).attr('value'), presentFilters) !== -1) {
14
      $(this).attr('disabled', true);
15
    }
16
  });
17
})();
18

  
19
$('#ajax-indicator').hide();
config/locales/en.yml
810 810
  label_my_queries: My custom queries
811 811
  label_filter_add: Add filter
812 812
  label_filter_plural: Filters
813
  label_bookmarked_filters: Bookmarked filters
813 814
  label_equals: is
814 815
  label_not_equals: is not
815 816
  label_in_less_than: in less than
config/locales/ru.yml
440 440
  label_board: ?????
441 441
  label_board_new: ????? ?????
442 442
  label_board_plural: ??????
443
  label_bookmarked_filters: ????????? ???????
443 444
  label_boolean: ??????????
444 445
  label_bulk_edit_selected_issues: ????????????? ??? ????????? ??????
445 446
  label_calendar: ?????????
config/routes.rb
186 186
    resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
187 187
      get 'report', :on => :collection
188 188
    end
189
    resources :queries, :only => [:new, :create]
189
    resources :queries, :only => [:new, :create] do
190
      collection do
191
        post :save_bookmarked_filters
192
      end
193
    end
190 194
    shallow do
191 195
      resources :issue_categories
192 196
    end
......
243 247
  post '/issues/new', :to => 'issues#new'
244 248
  match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
245 249

  
246
  resources :queries, :except => [:show]
250
  resources :queries, :except => [:show] do
251
    collection do
252
      post :save_bookmarked_filters
253
    end
254
  end
247 255
  get '/queries/filter', :to => 'queries#filter', :as => 'queries_filter'
248 256

  
249 257
  resources :news, :only => [:index, :show, :edit, :update, :destroy, :create, :new]
lib/redmine/preparation.rb
33 33

  
34 34
      # Permissions
35 35
      AccessControl.map do |map|
36
        map.permission :view_project, {:projects => [:show, :bookmark], :activities => [:index]}, :public => true, :read => true
36
        map.permission :view_project, {:projects => [:show, :bookmark],
37
                                       :activities => [:index],
38
                                       :queries => [:save_bookmarked_filters]},
39
                       :public => true,
40
                       :read => true
37 41
        map.permission :search_project, {:search => :index}, :public => true, :read => true
38 42
        map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
39 43
        map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
......
59 63
                                        :context_menus => [:issues],
60 64
                                        :versions => [:index, :show, :status_by],
61 65
                                        :journals => [:index, :diff],
62
                                        :queries => :index,
66
                                        :queries => [:index, :save_bookmarked_filters],
63 67
                                        :reports => [:issue_report, :issue_report_details]},
64 68
                         :read => true
65 69
          map.permission :add_issues, {:issues => [:new, :create], :attachments => :upload}
......
86 90
        end
87 91

  
88 92
        map.project_module :time_tracking do |map|
89
          map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
93
          map.permission :view_time_entries, {:timelog => [:index, :report, :show],
94
                                              :queries => [:save_bookmarked_filters]},
95
                         :read => true
90 96
          map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
91 97
          map.permission :edit_time_entries,
92 98
                         {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]},
......
156 162
        end
157 163

  
158 164
        map.project_module :calendar do |map|
159
          map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
165
          map.permission :view_calendar, {:calendars => [:show, :update],
166
                                          :queries => [:save_bookmarked_filters]},
167
                         :read => true
160 168
        end
161 169

  
162 170
        map.project_module :gantt do |map|
163
          map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
171
          map.permission :view_gantt, {:gantts => [:show, :update],
172
                                       :queries => [:save_bookmarked_filters]},
173
                         :read => true
164 174
        end
165 175
      end
166 176

  
167
- 
(5-5/5)