From 9e8a6613478993f3e6eac81eebe8d9c992085ed2 Mon Sep 17 00:00:00 2001 From: ishikawa999 <14245262+ishikawa999@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:54:51 +0900 Subject: [PATCH] Add default any_searchable filter --- app/models/issue_query.rb | 5 ++- app/models/query.rb | 15 ++++++- test/functional/issues_controller_test.rb | 10 ++++- test/unit/query_test.rb | 54 +++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index c59e8d35c..d8286c994 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -103,7 +103,10 @@ class IssueQuery < Query def initialize(attributes=nil, *args) super(attributes) - self.filters ||= {'status_id' => {:operator => "o", :values => [""]}} + self.filters ||= { + 'status_id' => {:operator => "o", :values => [""]}, + 'any_searchable' => {:operator => "~", :values => [""]} + } end def draw_relations diff --git a/app/models/query.rb b/app/models/query.rb index e95933b08..74c04a2b9 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -347,6 +347,8 @@ class Query < ApplicationRecord :tree => ["=", "~", "!*", "*"] } + NO_VALUE_REQUIRED_OPERATORS = %w[o c !* * nd t ld nw w lw l2w nm m lm y *o !o].freeze + class_attribute :available_columns self.available_columns = [] @@ -526,7 +528,9 @@ class Query < ApplicationRecord # filter requires one or more values (values_for(field) and values_for(field).first.present?) or # filter doesn't require any value - ["o", "c", "!*", "*", "nd", "t", "ld", "nw", "w", "lw", "l2w", "nm", "m", "lm", "y", "*o", "!o"].include? operator_for(field) + NO_VALUE_REQUIRED_OPERATORS.include?(operator_for(field)) or + # filter with blank value is skipped only for :search filters + skip_filter_for_blank_value?(field, operator_for(field), values_for(field)) end if filters end @@ -993,6 +997,7 @@ class Query < ApplicationRecord next unless v and !v.empty? operator = operator_for(field) + next if skip_filter_for_blank_value?(field, operator, v) # "me" value substitution if %w(assigned_to_id author_id user_id watcher_id updated_by last_updated_by).include?(field) @@ -1099,6 +1104,14 @@ class Query < ApplicationRecord private + def skip_filter_for_blank_value?(field, operator, values) + return false unless type_for(field) == :search + return false if NO_VALUE_REQUIRED_OPERATORS.include?(operator) + + values = Array(values) + values.none?(&:present?) + end + def grouped_query(&) r = nil if grouped? diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 3ea54226d..339be908a 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -157,7 +157,10 @@ class IssuesControllerTest < Redmine::ControllerTest assert_response :success # default filter - assert_query_filters [['status_id', 'o', '']] + assert_query_filters [ + ['status_id', 'o', ''], + ['any_searchable', '~', ''] + ] end def test_index_with_project_and_filter @@ -237,7 +240,10 @@ class IssuesControllerTest < Redmine::ControllerTest '*' => {:op => '*', :values => ['']} } } - default_filter = {'status_id' => {:operator => 'o', :values => ['']}} + default_filter = { + 'status_id' => {:operator => 'o', :values => ['']}, + 'any_searchable' => {:operator => '~', :values => ['']} + } to_test.each do |field, expression_and_expected| expression_and_expected.each do |filter_expression, expected| get(:index, :params => {:set_filter => 1, field => filter_expression}) diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 6318596f1..69a7c3524 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -1010,6 +1010,22 @@ class QueryTest < ActiveSupport::TestCase assert_equal [1, 3], find_issues_with_query(query).map(&:id).sort end + def test_filter_that_does_not_require_value_should_not_be_skipped + open_issue = Issue.generate!(:status => IssueStatus.where(:is_closed => false).first) + closed_issue = Issue.generate!(:status => IssueStatus.where(:is_closed => true).first) + + query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => {:operator => 'o', :values => ['']} + } + ) + + assert query.valid? + assert_includes query.issues, open_issue + assert_not_includes query.issues, closed_issue + end + def test_filter_any_searchable User.current = User.find(1) query = IssueQuery.new( @@ -1025,6 +1041,44 @@ class QueryTest < ActiveSupport::TestCase assert_equal [1, 2, 3], result.map(&:id).sort end + def test_filter_any_searchable_with_blank_value_should_be_noop + User.current = User.find(1) + + base_query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => {:operator => 'o', :values => ['']} + } + ) + query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => {:operator => 'o', :values => ['']}, + 'any_searchable' => {:operator => '~', :values => ['']} + } + ) + + assert query.valid? + assert_equal base_query.statement, query.statement + assert_equal base_query.issues.pluck(:id).sort, query.issues.pluck(:id).sort + end + + def test_filter_any_searchable_with_partially_blank_value_should_be_invalid + set_language_if_valid 'en' + User.current = User.find(1) + + query = IssueQuery.new( + :name => '_', + :filters => { + 'status_id' => {:operator => 'o', :values => ['']}, + 'any_searchable' => {:operator => '><', :values => ['', 'recipe']} + } + ) + + assert_not query.valid? + assert_include 'Any searchable text cannot be blank', query.errors.full_messages + end + def test_filter_any_searchable_with_multiple_words User.current = User.find(1) query = IssueQuery.new( -- 2.50.1 (Apple Git-155)