From 04b0c735331011bf514ae183b46d123e381dbe1b Mon Sep 17 00:00:00 2001 From: murin Date: Thu, 17 Jul 2025 09:54:10 +0300 Subject: Refactor the selection of the group for a filter from the QueriesHelper#filters_options_for_select method into a separate method. This reduces the complexity of QueriesHelper#filters_options_for_select and makes it easier for plugins to add new groups to query filters. --- app/helpers/queries_helper.rb | 45 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 3aef7083a..66b90b5d4 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -26,27 +26,7 @@ module QueriesHelper ungrouped = [] grouped = {label_string: [], label_date: [], label_time_tracking: [], label_attachment: []} query.available_filters.map do |field, field_options| - if /^cf_\d+\./.match?(field) - group = (field_options[:through] || field_options[:field]).try(:name) - elsif field =~ /^(.+)\./ - # association filters - group = :"field_#{$1}" - elsif field_options[:type] == :relation - group = :label_relations - elsif field_options[:type] == :tree - group = query.is_a?(IssueQuery) ? :label_relations : nil - elsif %w(member_of_group assigned_to_role).include?(field) - group = :field_assigned_to - elsif field_options[:type] == :date_past || field_options[:type] == :date - group = :label_date - elsif %w(estimated_hours spent_time).include?(field) - group = :label_time_tracking - elsif %w(attachment attachment_description).include?(field) - group = :label_attachment - elsif [:string, :text, :search].include?(field_options[:type]) - group = :label_string - end - if group + if (group = group_for_filter(field, field_options, query)) (grouped[group] ||= []) << [field_options[:name], field] else ungrouped << [field_options[:name], field] @@ -66,6 +46,29 @@ module QueriesHelper s end + def group_for_filter(field, field_options, query) + if /^cf_\d+\./.match?(field) + (field_options[:through] || field_options[:field]).try(:name) + elsif field =~ /^(.+)\./ + # association filters + :"field_#{$1}" + elsif field_options[:type] == :relation + :label_relations + elsif field_options[:type] == :tree + query.is_a?(IssueQuery) ? :label_relations : nil + elsif %w(member_of_group assigned_to_role).include?(field) + :field_assigned_to + elsif field_options[:type] == :date_past || field_options[:type] == :date + :label_date + elsif %w(estimated_hours spent_time).include?(field) + :label_time_tracking + elsif %w(attachment attachment_description).include?(field) + :label_attachment + elsif [:string, :text, :search].include?(field_options[:type]) + :label_string + end + end + def query_filters_hidden_tags(query) tags = ''.html_safe query.filters.each do |field, options| -- 2.25.1 From f865678d68c67e1b7ab0121ef6cb1fd40ff0ffcc Mon Sep 17 00:00:00 2001 From: murin Date: Thu, 17 Jul 2025 17:16:34 +0300 Subject: Add bookmarks for filters in queries Bookmarked filters always appear at the top of the "Add filter" dropdown, marked with a star emoji in their names. --- app/assets/stylesheets/application.css | 4 ++ app/controllers/queries_controller.rb | 14 +++++- app/helpers/queries_helper.rb | 47 ++++++++++++++++--- .../bookmarked_filters_controller.js | 24 ++++++++++ app/models/user_preference.rb | 25 ++++++++++ app/views/calendars/show.html.erb | 2 + app/views/gantts/show.html.erb | 2 + .../queries/_bookmarked_filters.html.erb | 18 +++++++ app/views/queries/_filter_select.html.erb | 4 ++ app/views/queries/_filters.html.erb | 5 +- app/views/queries/_query_form.html.erb | 2 + .../queries/save_bookmarked_filters.js.erb | 19 ++++++++ config/locales/en.yml | 1 + config/locales/ru.yml | 1 + config/routes.rb | 12 ++++- lib/redmine/preparation.rb | 20 ++++++-- 16 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 app/javascript/controllers/bookmarked_filters_controller.js create mode 100644 app/views/queries/_bookmarked_filters.html.erb create mode 100644 app/views/queries/_filter_select.html.erb create mode 100644 app/views/queries/save_bookmarked_filters.js.erb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index e994c12af..3834845d8 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -781,6 +781,10 @@ fieldset#date-range p { margin: 2px 0 2px 0; } .add-filter {width:35%; float:right; text-align: right; vertical-align: top;} +select#bookmarked_filters { + max-width: 100%; +} + #issue_is_private_wrap {float:right; margin-right:1em;} .toggle-multiselect { margin-right:5px; cursor:pointer;} .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb index 24f37eda2..949eae767 100644 --- a/app/controllers/queries_controller.rb +++ b/app/controllers/queries_controller.rb @@ -22,7 +22,7 @@ class QueriesController < ApplicationController layout :query_layout before_action :find_query, :only => [:edit, :update, :destroy] - before_action :find_optional_project, :only => [:new, :create] + before_action :find_optional_project, :only => [:new, :create, :save_bookmarked_filters] accept_api_auth :index @@ -119,6 +119,18 @@ class QueriesController < ApplicationController super unless query_layout == 'admin' end + def save_bookmarked_filters + respond_to do |format| + format.js do + @query = query_class.new + @query.user = ::User.current + @query.project = @project + ::User.current.pref.save_bookmarked_query_filters(@query, params[:bookmarked_filters] || []) + end + format.any { render_404 } + end + end + private def find_query diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 66b90b5d4..0ead31deb 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -25,13 +25,18 @@ module QueriesHelper def filters_options_for_select(query) ungrouped = [] grouped = {label_string: [], label_date: [], label_time_tracking: [], label_attachment: []} - query.available_filters.map do |field, field_options| - if (group = group_for_filter(field, field_options, query)) - (grouped[group] ||= []) << [field_options[:name], field] - else - ungrouped << [field_options[:name], field] + bookmarks = bookmarked_filters(query) + query.available_filters. + sort_by { |field, filter| sort_bookmarks(field, filter, bookmarks) }. + map do |field, field_options| + if bookmarks.include?(field) + ungrouped << ["#{field_options[:name]}\u2b50", field] + elsif (group = group_for_filter(field, field_options, query)) + (grouped[group] ||= []) << [field_options[:name], field] + else + ungrouped << [field_options[:name], field] + end end - end # Remove empty groups grouped.delete_if {|k, v| v.empty?} # Don't group dates if there's only one (eg. time entries filters) @@ -46,6 +51,15 @@ module QueriesHelper s end + def bookmarked_filters(query) + ::User.current.pref.bookmarked_query_filters(query.class) + end + + ONE_ARRAY = [1].freeze + def sort_bookmarks(field, filter, bookmarks) + bookmarks.include?(field) ? [0, filter[:name].downcase] : ONE_ARRAY + end + def group_for_filter(field, field_options, query) if /^cf_\d+\./.match?(field) (field_options[:through] || field_options[:field]).try(:name) @@ -81,6 +95,27 @@ module QueriesHelper tags end + def bookmarked_filters_options(query) + options_for_select( + query.available_filters.map { |id, filter| [filter[:name], id] }.sort_by(&:first), + selected_bookmarked_filters(query) + ) + end + + def selected_bookmarked_filters(query) + query.available_filters.keys & + User.current.pref.bookmarked_query_filters(query.class) + end + private :selected_bookmarked_filters + + def path_to_save_bookmarked_filters + if @project + save_bookmarked_filters_project_queries_path(@project) + else + save_bookmarked_filters_queries_path + end + end + def query_columns_hidden_tags(query) tags = ''.html_safe query.columns.each do |column| diff --git a/app/javascript/controllers/bookmarked_filters_controller.js b/app/javascript/controllers/bookmarked_filters_controller.js new file mode 100644 index 000000000..29958007d --- /dev/null +++ b/app/javascript/controllers/bookmarked_filters_controller.js @@ -0,0 +1,24 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static values = { + url: String + } + + connect() { + // Don't send select value on form submission + this.element.removeAttribute('name') + } + + saveBookmarkedFilters(event) { + const values = Array.from(event.target.selectedOptions).map(({ value }) => value); + $.ajax({ + url: this.urlValue, + type: 'post', + data: { + bookmarked_filters: values, + ...event.params + } + }); + } +} diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index e1842b131..55650bb41 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -210,4 +210,29 @@ class UserPreference < ApplicationRecord my_page_settings.keep_if {|block, settings| blocks.include?(block)} end private :clear_unused_block_settings + + def bookmarked_query_filters(query_class) + bookmarked_filters[query_class.to_s.underscore] || [] + end + + def save_bookmarked_query_filters(query, filter_names) + query_filters = bookmarked_query_filters(query.class) + old_selected_filters = query.available_filters.keys & query_filters + + deleted = old_selected_filters - filter_names + added = filter_names - old_selected_filters + + self.bookmarked_filters = {query.class.to_s.underscore => query_filters - deleted + added} + save! + end + + def bookmarked_filters + self[:bookmarked_filters] || {} + end + private :bookmarked_filters + + def bookmarked_filters=(value) + self[:bookmarked_filters] = bookmarked_filters.merge!(value) + end + private :bookmarked_filters= end diff --git a/app/views/calendars/show.html.erb b/app/views/calendars/show.html.erb index d5cb6a6a1..4a5b6a1b6 100644 --- a/app/views/calendars/show.html.erb +++ b/app/views/calendars/show.html.erb @@ -17,6 +17,8 @@ <%= render :partial => 'queries/filters', :locals => {:query => @query} %> + + <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %> diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb index 45428b03d..09ffc777f 100644 --- a/app/views/gantts/show.html.erb +++ b/app/views/gantts/show.html.erb @@ -70,6 +70,8 @@ + + <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %>

diff --git a/app/views/queries/_bookmarked_filters.html.erb b/app/views/queries/_bookmarked_filters.html.erb new file mode 100644 index 000000000..aea9f613d --- /dev/null +++ b/app/views/queries/_bookmarked_filters.html.erb @@ -0,0 +1,18 @@ +

diff --git a/app/views/queries/_filter_select.html.erb b/app/views/queries/_filter_select.html.erb new file mode 100644 index 000000000..9b99afc7a --- /dev/null +++ b/app/views/queries/_filter_select.html.erb @@ -0,0 +1,4 @@ +
+ <%= label_tag('add_filter_select', l(:label_filter_add)) %> + <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %> +
diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb index 42756775a..ae7f863ed 100644 --- a/app/views/queries/_filters.html.erb +++ b/app/views/queries/_filters.html.erb @@ -17,10 +17,7 @@ $(document).ready(function(){
-
-<%= label_tag('add_filter_select', l(:label_filter_add)) %> -<%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %> -
+<%= render partial: 'queries/filter_select', locals: {query: @query} %> <%= hidden_field_tag 'f[]', '' %> <% include_calendar_headers_tags %> diff --git a/app/views/queries/_query_form.html.erb b/app/views/queries/_query_form.html.erb index 77094e16e..336077c54 100644 --- a/app/views/queries/_query_form.html.erb +++ b/app/views/queries/_query_form.html.erb @@ -56,6 +56,8 @@ <% end %> + + <%= render partial: 'queries/bookmarked_filters', locals: {query: @query} %>

diff --git a/app/views/queries/save_bookmarked_filters.js.erb b/app/views/queries/save_bookmarked_filters.js.erb new file mode 100644 index 000000000..8740fead9 --- /dev/null +++ b/app/views/queries/save_bookmarked_filters.js.erb @@ -0,0 +1,19 @@ +$('.add-filter').replaceWith('<%= + escape_javascript(render(partial: 'queries/filter_select', locals: {query: @query})) +%>'); + +$('#add_filter_select').change(function() { + addFilter($(this).val(), '', []); +}); + +(function() { + // Block already added filters in select + const presentFilters = $('#filters-table .field input').map((_, filter) => filter.value); + $('#add_filter_select').find('option').each(function () { + if ($.inArray($(this).attr('value'), presentFilters) !== -1) { + $(this).attr('disabled', true); + } + }); +})(); + +$('#ajax-indicator').hide(); diff --git a/config/locales/en.yml b/config/locales/en.yml index 947a8642f..13c602042 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -807,6 +807,7 @@ en: label_my_queries: My custom queries label_filter_add: Add filter label_filter_plural: Filters + label_bookmarked_filters: Bookmarked filters label_equals: is label_not_equals: is not label_in_less_than: in less than diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 1f02f2979..350c93aa5 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -440,6 +440,7 @@ ru: label_board: Форум label_board_new: Новый форум label_board_plural: Форумы + label_bookmarked_filters: Избранные фильтры label_boolean: Логический label_bulk_edit_selected_issues: Редактировать все выбранные задачи label_calendar: Календарь diff --git a/config/routes.rb b/config/routes.rb index 52c95c6a4..219932020 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -184,7 +184,11 @@ Rails.application.routes.draw do resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do get 'report', :on => :collection end - resources :queries, :only => [:new, :create] + resources :queries, :only => [:new, :create] do + collection do + post :save_bookmarked_filters + end + end shallow do resources :issue_categories end @@ -241,7 +245,11 @@ Rails.application.routes.draw do post '/issues/new', :to => 'issues#new' match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete - resources :queries, :except => [:show] + resources :queries, :except => [:show] do + collection do + post :save_bookmarked_filters + end + end get '/queries/filter', :to => 'queries#filter', :as => 'queries_filter' resources :news, :only => [:index, :show, :edit, :update, :destroy, :create, :new] diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb index a7387f5dc..4953913e7 100644 --- a/lib/redmine/preparation.rb +++ b/lib/redmine/preparation.rb @@ -33,7 +33,11 @@ module Redmine # Permissions AccessControl.map do |map| - map.permission :view_project, {:projects => [:show, :bookmark], :activities => [:index]}, :public => true, :read => true + map.permission :view_project, {:projects => [:show, :bookmark], + :activities => [:index], + :queries => [:save_bookmarked_filters]}, + :public => true, + :read => true map.permission :search_project, {:search => :index}, :public => true, :read => true map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member @@ -56,7 +60,7 @@ module Redmine :context_menus => [:issues], :versions => [:index, :show, :status_by], :journals => [:index, :diff], - :queries => :index, + :queries => [:index, :save_bookmarked_filters], :reports => [:issue_report, :issue_report_details]}, :read => true map.permission :add_issues, {:issues => [:new, :create], :attachments => :upload} @@ -83,7 +87,9 @@ module Redmine end map.project_module :time_tracking do |map| - map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true + map.permission :view_time_entries, {:timelog => [:index, :report, :show], + :queries => [:save_bookmarked_filters]}, + :read => true map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, @@ -153,11 +159,15 @@ module Redmine end map.project_module :calendar do |map| - map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true + map.permission :view_calendar, {:calendars => [:show, :update], + :queries => [:save_bookmarked_filters]}, + :read => true end map.project_module :gantt do |map| - map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true + map.permission :view_gantt, {:gantts => [:show, :update], + :queries => [:save_bookmarked_filters]}, + :read => true end end -- 2.25.1