Project

General

Profile

Patch #43032 » 01_bookmark_filters.patch

Leonid Murin, 2025-07-21 15:09

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
781 781

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

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

  
784 788
#issue_is_private_wrap {float:right; margin-right:1em;}
785 789
.toggle-multiselect { margin-right:5px; cursor:pointer;}
786 790
.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
210 210
    my_page_settings.keep_if {|block, settings| blocks.include?(block)}
211 211
  end
212 212
  private :clear_unused_block_settings
213

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

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

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

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

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

  
234
  def bookmarked_filters=(value)
235
    self[:bookmarked_filters] = bookmarked_filters.merge!(value)
236
  end
237
  private :bookmarked_filters=
213 238
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/show.html.erb
70 70
      </div>
71 71
    </div>
72 72
  </fieldset>
73

  
74
  <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %>
73 75
</div>
74 76

  
75 77
<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
807 807
  label_my_queries: My custom queries
808 808
  label_filter_add: Add filter
809 809
  label_filter_plural: Filters
810
  label_bookmarked_filters: Bookmarked filters
810 811
  label_equals: is
811 812
  label_not_equals: is not
812 813
  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
184 184
    resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
185 185
      get 'report', :on => :collection
186 186
    end
187
    resources :queries, :only => [:new, :create]
187
    resources :queries, :only => [:new, :create] do
188
      collection do
189
        post :save_bookmarked_filters
190
      end
191
    end
188 192
    shallow do
189 193
      resources :issue_categories
190 194
    end
......
241 245
  post '/issues/new', :to => 'issues#new'
242 246
  match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
243 247

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

  
247 255
  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
......
56 60
                                        :context_menus => [:issues],
57 61
                                        :versions => [:index, :show, :status_by],
58 62
                                        :journals => [:index, :diff],
59
                                        :queries => :index,
63
                                        :queries => [:index, :save_bookmarked_filters],
60 64
                                        :reports => [:issue_report, :issue_report_details]},
61 65
                         :read => true
62 66
          map.permission :add_issues, {:issues => [:new, :create], :attachments => :upload}
......
83 87
        end
84 88

  
85 89
        map.project_module :time_tracking do |map|
86
          map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
90
          map.permission :view_time_entries, {:timelog => [:index, :report, :show],
91
                                              :queries => [:save_bookmarked_filters]},
92
                         :read => true
87 93
          map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
88 94
          map.permission :edit_time_entries,
89 95
                         {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]},
......
153 159
        end
154 160

  
155 161
        map.project_module :calendar do |map|
156
          map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
162
          map.permission :view_calendar, {:calendars => [:show, :update],
163
                                          :queries => [:save_bookmarked_filters]},
164
                         :read => true
157 165
        end
158 166

  
159 167
        map.project_module :gantt do |map|
160
          map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
168
          map.permission :view_gantt, {:gantts => [:show, :update],
169
                                       :queries => [:save_bookmarked_filters]},
170
                         :read => true
161 171
        end
162 172
      end
163 173

  
164
- 
(4-4/4)