From 8ee51788fd57c70ed9b2366fee63c9a5c7ee8841 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 10:24:39 +0200 Subject: [PATCH 1/9] Support Active Admin 4 Active Admin 4 removed Filters::ViewHelper and Filters::ActiveSidebar, so those prepends now run only on v3. On v4 the gem hooks FormHelper and the active_search section's condition, and sends warnings to :alert, the only warning-like key the v4 layout renders. Co-Authored-By: Clanker --- active_admin_filter_extensions.gemspec | 2 +- lib/active_admin_filter_extensions.rb | 21 +++++++++++-- .../filters_form.rb | 31 ++++++++++++++++++- .../policies.rb | 6 ++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/active_admin_filter_extensions.gemspec b/active_admin_filter_extensions.gemspec index 5793b91..2e9843e 100644 --- a/active_admin_filter_extensions.gemspec +++ b/active_admin_filter_extensions.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| "the URL." s.required_ruby_version = ">= 3.3" - s.add_dependency "activeadmin", ">= 3.5", "< 4" + s.add_dependency "activeadmin", ">= 3.5", "< 5" s.files = `git ls-files`.split("\n") s.require_paths = ["lib"] diff --git a/lib/active_admin_filter_extensions.rb b/lib/active_admin_filter_extensions.rb index cfc4871..bb6638b 100644 --- a/lib/active_admin_filter_extensions.rb +++ b/lib/active_admin_filter_extensions.rb @@ -32,6 +32,16 @@ class << self def notice_flash_key @notice_flash_key ||= :notice end + + # Active Admin 4 moved the filters form helper and the Current Filters section. + def aa_v4? + Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new("4.0.0.beta1") + end + + # Active Admin 4's layout renders only the :error, :alert and :notice flash keys. + def warning_flash_key + aa_v4? ? :alert : :warning + end end end @@ -49,7 +59,6 @@ def notice_flash_key ActiveAdmin::ResourceController.helper_method :filtering_params, :applied_filter_params, :filter_default_values, :visible_filters - ActiveAdmin::Resource.prepend ActiveAdminFilterExtensions::ResourceExtension ActiveAdmin::ResourceDSL.include ActiveAdminFilterExtensions::Policies::ResourcePolicyDSL ActiveAdmin::Resource.include ActiveAdminFilterExtensions::Policies::ResourcePolicySettings ActiveAdmin::ResourceDSL.include ActiveAdminFilterExtensions::Notice @@ -58,6 +67,12 @@ def notice_flash_key # decision - and therefore the URL - is computed from. ActiveAdmin::ResourceController.include ActiveAdminFilterExtensions::Policies::Controller ActiveAdmin::ResourceController.include ActiveAdminFilterExtensions::Redirect - ActiveAdmin::Filters::ViewHelper.prepend ActiveAdminFilterExtensions::ViewHelper - ActiveAdmin::Filters::ActiveSidebar.prepend ActiveAdminFilterExtensions::ActiveSidebar + if ActiveAdminFilterExtensions.aa_v4? + ActiveAdmin::Resource.prepend ActiveAdminFilterExtensions::ResourceExtensionV4 + ActiveAdmin::FormHelper.prepend ActiveAdminFilterExtensions::ViewHelper + else + ActiveAdmin::Resource.prepend ActiveAdminFilterExtensions::ResourceExtension + ActiveAdmin::Filters::ViewHelper.prepend ActiveAdminFilterExtensions::ViewHelper + ActiveAdmin::Filters::ActiveSidebar.prepend ActiveAdminFilterExtensions::ActiveSidebar + end end diff --git a/lib/active_admin_filter_extensions/filters_form.rb b/lib/active_admin_filter_extensions/filters_form.rb index a061750..231240c 100644 --- a/lib/active_admin_filter_extensions/filters_form.rb +++ b/lib/active_admin_filter_extensions/filters_form.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true module ActiveAdminFilterExtensions + # Stable hooks for page objects, so specs need not select on Active Admin's CSS classes. + FORM_OPTIONS = { html: { "data-filter-extensions-form" => "" } }.freeze + # The filters sidebar hands the form the filters that `:if` and `:unless` allow, so the same # set is rendered and consulted for defaults, and the conditions are resolved once. module ResourceExtension @@ -11,11 +14,37 @@ def filters_sidebar_section # Passed positionally rather than splatted: the section is shown when the resource has # any filter at all, and `**{}` would drop the argument entirely once `:if` and # `:unless` have denied every one of them. - active_admin_filters_form_for assigns[:search], visible_filters + active_admin_filters_form_for assigns[:search], visible_filters, FORM_OPTIONS end end end + # Active Admin 4's sidebar: the section title is rendered inside the block, as + # `h3.filters-form-title`, and the Current Filters panel is a section of its own. + module ResourceExtensionV4 + private + + def filters_sidebar_section + name = :filters + ActiveAdmin::SidebarSection.new name, only: :index, if: -> { active_admin_config.filters.any? } do + h3 I18n.t("active_admin.sidebars.#{name}", default: name.to_s.titlecase), + class: "filters-form-title", "data-filter-extensions-title" => "" + active_admin_filters_form_for assigns[:search], visible_filters, FORM_OPTIONS + end + end + + # Gated on `params[:q]` like ActiveSidebar below; only its condition is widened. + def active_search_sidebar_section + section = super + shown = section.options[:if] + section.options[:if] = lambda { + instance_exec(&shown) || + (active_admin_config.current_filters_enabled? && filter_default_values.present?) + } + section + end + end + # The form renders what it is handed: `:if` and `:unless` were already resolved by # #visible_filters, and the policy keys - merged flat into these options by # #merge_filter_extensions - are not Formtastic's business. required: in particular is also a diff --git a/lib/active_admin_filter_extensions/policies.rb b/lib/active_admin_filter_extensions/policies.rb index 1164b15..8bb8678 100644 --- a/lib/active_admin_filter_extensions/policies.rb +++ b/lib/active_admin_filter_extensions/policies.rb @@ -140,7 +140,7 @@ def enforce_require_any_filter # Active Admin reads a q-less index as "this table has no rows" and renders the blank # slate, Create link and all; a present-but-empty q renders an empty result instead. params[:q] ||= {} - flash.now[:warning] = + flash.now[ActiveAdminFilterExtensions.warning_flash_key] = notice.is_a?(String) ? notice : I18n.t(notice, scope: :active_admin_filter_extensions, default: :require_any_filter) end @@ -181,8 +181,8 @@ def enforce_with_groups(q) filter_policy_errors << keys filter_policy_reset_keys.concat(keys) names = keys.map { |k| strip_ransack_predicate(k).humanize }.uniq.join(" and ") - flash.now[:warning] = I18n.t(:given_together, scope: :active_admin_filter_extensions, - filters: names) + flash.now[ActiveAdminFilterExtensions.warning_flash_key] = + I18n.t(:given_together, scope: :active_admin_filter_extensions, filters: names) present.flatten.each { |key| q.delete(key) } end end From e8349a6cb9ed09dd8f437a7ab24ca5c7522d17a4 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 10:24:50 +0200 Subject: [PATCH 2/9] Run the suite on Active Admin 4 too Co-Authored-By: Clanker --- .github/workflows/ci.yml | 2 +- Gemfile | 14 ++++++++-- spec/integration/sidebar_spec.rb | 47 ++++++++++++++++++++++++++++++++ spec/integration/with_spec.rb | 2 +- spec/rails_helper.rb | 3 +- spec/support/rails_template.rb | 10 +++++++ tasks/test.rake | 8 ++++-- 7 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 spec/integration/sidebar_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0410c38..868676a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: # on 2026-03-31, so the floor there is 3.3 (security-only until 2027-03-31). ruby: ['3.3', '3.4', '4.0'] rails: ['8.0.0', '8.1.0'] - activeadmin: ['3.5.0'] + activeadmin: ['3.5.0', '4.0.0.beta23'] env: RAILS: ${{ matrix.rails }} AA: ${{ matrix.activeadmin }} diff --git a/Gemfile b/Gemfile index 4d98681..30f5a39 100644 --- a/Gemfile +++ b/Gemfile @@ -8,9 +8,17 @@ default_rails_version = "8.0.0" default_activeadmin_version = "3.5.0" gem "rails", "~> #{ENV['RAILS'] || default_rails_version}" -gem "activeadmin", "~> #{ENV['AA'] || default_activeadmin_version}" -gem "sprockets-rails" -gem "sass-rails" +activeadmin_version = ENV["AA"] || default_activeadmin_version +gem "activeadmin", "~> #{activeadmin_version}" +# Active Admin 4 serves its assets through propshaft and an import map, Active Admin 3 through +# Sprockets and Sass. +if Gem::Version.new(activeadmin_version) >= Gem::Version.new("4.0.0.beta1") + gem "importmap-rails" + gem "propshaft" +else + gem "sprockets-rails" + gem "sass-rails" +end group :test do gem "capybara" diff --git a/spec/integration/sidebar_spec.rb b/spec/integration/sidebar_spec.rb new file mode 100644 index 0000000..ada6a6d --- /dev/null +++ b/spec/integration/sidebar_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails_helper" + +# The sidebar markup moved in Active Admin 4: the Current Filters section has its own id and the +# filters section renders its title inside the block. +RSpec.describe "the filters sidebar", type: :feature do + let(:aa_v4) { ActiveAdminFilterExtensions.aa_v4? } + let(:current_filters) { aa_v4 ? "#active_search_sidebar_section" : "#search_status_sidebar_section" } + + it "marks the filters form with a data hook" do + visit "/admin/string_posts" + + expect(page).to have_css("#filters_sidebar_section form[data-filter-extensions-form]") + end + + it "shows the Current Filters panel when only defaults apply" do + visit "/admin/string_posts" + + expect(page).to have_css(current_filters, text: "keep") + end + + it "hides the Current Filters panel when nothing is filtered" do + visit "/admin/no_defaults_posts" + + expect(page).to have_css("#filters_sidebar_section") + expect(page).to have_no_css(current_filters) + end + + it "keeps the Current Filters panel for a filter from the request" do + visit "/admin/no_defaults_posts?q%5Btitle_cont%5D=keep" + + expect(page).to have_css(current_filters, text: "keep") + end + + it "renders the filters title once" do + visit "/admin/string_posts" + + if aa_v4 + expect(page).to have_css("#filters_sidebar_section h3.filters-form-title[data-filter-extensions-title]", + count: 1, text: "Filters") + else + expect(page).to have_no_css("#filters_sidebar_section .filters-form-title") + expect(page).to have_css("#filters_sidebar_section h3", count: 1, text: "Filters") + end + end +end diff --git a/spec/integration/with_spec.rb b/spec/integration/with_spec.rb index 1c05618..92d7249 100644 --- a/spec/integration/with_spec.rb +++ b/spec/integration/with_spec.rb @@ -48,7 +48,7 @@ def query_of(url) # PairedPost declares with: on both members - the two declarations are one group. visit "/admin/paired_posts?q%5Bstatus_eq%5D=draft" - expect(page).to have_css(".flash_warning", count: 1) + expect(page).to have_css(".flash_#{ActiveAdminFilterExtensions.warning_flash_key}", count: 1) end it "does not count a blank-array partner as answered" do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 8195e04..9d6e3bf 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -12,7 +12,8 @@ ENV["RAILS_ENV"] = "test" require "rails" ENV["RAILS"] = Rails.version -ENV["RAILS_ROOT"] = File.expand_path("rails/rails-#{ENV['RAILS']}", __dir__) +aa_major = Gem.loaded_specs["activeadmin"].version.segments.first +ENV["RAILS_ROOT"] = File.expand_path("rails/rails-#{ENV['RAILS']}-aa#{aa_major}", __dir__) unless File.exist?(ENV["RAILS_ROOT"]) # A generation that dies midway leaves a partial app the File.exist? guard would then skip diff --git a/spec/support/rails_template.rb b/spec/support/rails_template.rb index 01ffe01..3cba5d5 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -629,6 +629,16 @@ def filter_defaults_apply? end RUBY +# Active Admin 4's flash partial renders only :error, :alert and :notice, with no class per key. +# An app that moves the notice to a key of its own renders that key itself, as this one does. +if Gem.loaded_specs["activeadmin"].version >= Gem::Version.new("4.0.0.beta1") + file "app/views/active_admin/_flash_messages.html.erb", <<~ERB + <% flash_messages.each do |type, message| %> +
<%= message %>
+ <% end %> + ERB +end + run "rm -rf test" route "root to: 'admin/dashboard#index'" rake "db:migrate" diff --git a/tasks/test.rake b/tasks/test.rake index 76bd3de..b5ed0b0 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -4,8 +4,8 @@ desc "Creates a test rails app for the specs to run against" task :setup do require "rails/version" - # The template throws the generated Gemfile away, so the app runs under this gem's sprockets - # bundle with no importmap-rails. Without --skip-javascript, Rails 8.1 writes + # The template throws the generated Gemfile away, so the app runs under this gem's bundle, + # which has no importmap-rails on Active Admin 3. Without --skip-javascript, Rails 8.1 writes # `stale_when_importmap_changes` into ApplicationController and booting it raises NameError. args = %w[ --skip-spring @@ -16,7 +16,9 @@ task :setup do -m spec/support/rails_template.rb ].join(" ") - app_dir = "spec/rails/rails-#{Rails::VERSION::STRING}" + # One app per Active Admin major: its installer writes different assets and initializers. + aa_major = Gem.loaded_specs["activeadmin"].version.segments.first + app_dir = "spec/rails/rails-#{Rails::VERSION::STRING}-aa#{aa_major}" # From scratch, always: `rails new` git-inits the app, so a nested repo survives # `git clean -fdx` and the generator would then write into the surviving tree - leaving a # stale resource behind a freshly stamped digest, which is a green the stamp exists to deny. From 89ba83af1208c88dc300cb3aa36e48cfcd743168 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 10:24:50 +0200 Subject: [PATCH 3/9] Document Active Admin 4 support Co-Authored-By: Clanker --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 828c455..3cc7e10 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,11 @@ writes action results to `:notice`, and on a shared key the arriving message win action's result would silence the notice. The key is read when the notice is written and `flash_key:` on the resource still beats it. +On Active Admin 4 the stock layout renders only the `:error`, `:alert` and `:notice` flash +keys, so a key of your own needs an override of `app/views/active_admin/_flash_messages.html.erb` +that renders it. The `with:` and `require_any_filter` warnings go to `:alert` there, and to +`:warning` on Active Admin 3. + ## Required filters: the mandatory window Some tables must never be queried without a bound - a log partitioned by date, a CDR table @@ -363,7 +368,16 @@ Four prints in small type: another predicate - wants `applied_filter_params`: the same values as an indifferent copy, safe to read by symbol and to mutate. Both are helpers, so a view can call them too. -Tested against Active Admin 3.5 on Rails 8.0 and 8.1, under Ruby 3.3, 3.4 and 4.0. +## Active Admin 4 + +The gem works on Active Admin 3.5 and 4 from the same release; it ships no JavaScript or CSS. +On Active Admin 4 the filters section keeps Active Admin's own `h3.filters-form-title`, and +the Current Filters section (`#active_search_sidebar_section`) also shows when only defaults +apply. Page objects can select the filters form by `[data-filter-extensions-form]` and, on +Active Admin 4, its title by `[data-filter-extensions-title]`. + +Tested against Active Admin 3.5 and 4.0.0.beta23 on Rails 8.0 and 8.1, under Ruby 3.3, 3.4 +and 4.0. ## License From 5d3b8c56d2764b4b5f7c4bff7cd1823fa80973ac Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 13:59:42 +0200 Subject: [PATCH 4/9] Pass Active Admin 4's auth object to Ransack Active Admin 4 hands Ransack its authorization adapter, so a model's ransackable_attributes can depend on the admin. Both of this gem's Ransack calls dropped it, which could leave a permitted filter unapplied or fail key derivation for a defaulted filter. Co-Authored-By: Clanker --- .../data_access.rb | 5 +---- .../search_keys.rb | 9 +++++++- spec/integration/ransack_auth_object_spec.rb | 21 +++++++++++++++++++ spec/spec_helper.rb | 3 +++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 spec/integration/ransack_auth_object_spec.rb diff --git a/lib/active_admin_filter_extensions/data_access.rb b/lib/active_admin_filter_extensions/data_access.rb index 6d42a7c..feb3839 100644 --- a/lib/active_admin_filter_extensions/data_access.rb +++ b/lib/active_admin_filter_extensions/data_access.rb @@ -4,14 +4,11 @@ module ActiveAdminFilterExtensions # The single method this gem replaces: Active Admin searches the collection on `params[:q]` # read directly, and it searches #filtering_params instead. Everything that method means is # in FilterDefaults. - # - # The Ransack call is the Active Admin 3 one, which this gem targets: Active Admin 4 passes - # `auth_object: active_admin_authorization` there as well. module DataAccess protected def apply_filtering(chain) - @search = chain.ransack(filtering_params) + @search = chain.ransack(filtering_params, **filter_ransack_options) # An :empty policy keeps the request intact - the form still shows what the admin # typed - and blanks the collection instead. return @search.result.none if filter_policy_render_empty? diff --git a/lib/active_admin_filter_extensions/search_keys.rb b/lib/active_admin_filter_extensions/search_keys.rb index 1ee1210..0659992 100644 --- a/lib/active_admin_filter_extensions/search_keys.rb +++ b/lib/active_admin_filter_extensions/search_keys.rb @@ -60,10 +60,17 @@ def filter_input_options(options, builder) # time it is called, and every defaulted filter would pay for another one. def filter_input_builder @filter_input_builder ||= ::ActiveAdmin::Filters::FormBuilder.new( - :q, active_admin_config.resource_class.ransack({}), view_context, {} + :q, active_admin_config.resource_class.ransack({}, **filter_ransack_options), view_context, {} ) end + # What Active Admin passes to Ransack next to the search: version 4 hands over its + # authorization adapter, so a model's `ransackable_attributes(auth_object)` can depend on + # the admin; version 3 passes nothing. + def filter_ransack_options + ActiveAdminFilterExtensions.aa_v4? ? { auth_object: active_admin_authorization } : {} + end + def raise_filter_default_error(attribute, message) raise ArgumentError, "filter :#{attribute} declares a `default:` but #{message}" end diff --git a/spec/integration/ransack_auth_object_spec.rb b/spec/integration/ransack_auth_object_spec.rb new file mode 100644 index 0000000..38afc64 --- /dev/null +++ b/spec/integration/ransack_auth_object_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "rails_helper" + +# Active Admin 4 passes its authorization adapter to Ransack, so a model's allowlist can depend +# on the admin. The gem's own search and its search-key derivation must pass it on as well. +RSpec.describe "the Ransack auth object", type: :feature do + it "reaches the model's allowlist from every search the index builds" do + seen = [] + allow(Post).to receive(:ransackable_attributes).and_wrap_original do |original, auth_object = nil| + seen << auth_object + original.call(auth_object) + end + + visit "/admin/string_posts" + + expected = ActiveAdminFilterExtensions.aa_v4? ? ActiveAdmin::AuthorizationAdapter : NilClass + expect(seen).not_to be_empty + expect(seen).to all(be_a(expected)) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fdea2db..b5452ad 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -55,6 +55,9 @@ def filter_search_keys(attribute, _options) @search_keys.fetch(attribute) { [attribute.to_s] } end + # The real one asks Active Admin's version, which this suite does not load. + def filter_ransack_options = {} + prepend ActiveAdminFilterExtensions::DataAccess include ActiveAdminFilterExtensions::Policies::Controller include ActiveAdminFilterExtensions::FilterOptions From 16a092e13c010090a6670d3ba7543adb4c4f4703 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 13:59:42 +0200 Subject: [PATCH 5/9] Document running the suite on Active Admin 4 Co-Authored-By: Clanker --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3cc7e10..06c3068 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,14 @@ Active Admin 4, its title by `[data-filter-extensions-title]`. Tested against Active Admin 3.5 and 4.0.0.beta23 on Rails 8.0 and 8.1, under Ruby 3.3, 3.4 and 4.0. +The suite runs on Active Admin 3.5 by default. The `AA` and `RAILS` variables pick other +versions; `Gemfile.lock` is not committed, so re-resolve the bundle when switching: + +```sh +AA=4.0.0.beta23 bundle update activeadmin +AA=4.0.0.beta23 bundle exec rake spec +``` + ## License MIT From ce78c837ec8c86d001c1ae918584a08b83fe6bd5 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 14:06:30 +0200 Subject: [PATCH 6/9] Exclude ActiveAdmin 5 prereleases RubyGems treats 5.0.0.beta1 as satisfying "< 5". Co-Authored-By: Clanker --- active_admin_filter_extensions.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_admin_filter_extensions.gemspec b/active_admin_filter_extensions.gemspec index 2e9843e..bbe749e 100644 --- a/active_admin_filter_extensions.gemspec +++ b/active_admin_filter_extensions.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| "the URL." s.required_ruby_version = ">= 3.3" - s.add_dependency "activeadmin", ">= 3.5", "< 5" + s.add_dependency "activeadmin", ">= 3.5", "< 5.0.0.a" s.files = `git ls-files`.split("\n") s.require_paths = ["lib"] From 99e85bb9454d6e30481531ff3d414796eb94023e Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 14:16:45 +0200 Subject: [PATCH 7/9] Let an arriving alert win over the filter warnings On Active Admin 4 the warnings share :alert with the app's own messages, so an action's alert on the redirect was overwritten. Co-Authored-By: Clanker --- README.md | 4 +++- lib/active_admin_filter_extensions/policies.rb | 17 ++++++++++++----- spec/integration/require_any_filter_spec.rb | 8 ++++++++ spec/support/rails_template.rb | 6 ++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 06c3068..744264c 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,9 @@ action's result would silence the notice. The key is read when the notice is wri On Active Admin 4 the stock layout renders only the `:error`, `:alert` and `:notice` flash keys, so a key of your own needs an override of `app/views/active_admin/_flash_messages.html.erb` that renders it. The `with:` and `require_any_filter` warnings go to `:alert` there, and to -`:warning` on Active Admin 3. +`:warning` on Active Admin 3. An `:alert` that arrived with the request wins over the warning. +[activeadmin/activeadmin#9161](https://github.com/activeadmin/activeadmin/pull/9161) makes the +layout render every key; until a release ships it, the override is still needed. ## Required filters: the mandatory window diff --git a/lib/active_admin_filter_extensions/policies.rb b/lib/active_admin_filter_extensions/policies.rb index 8bb8678..7afc577 100644 --- a/lib/active_admin_filter_extensions/policies.rb +++ b/lib/active_admin_filter_extensions/policies.rb @@ -140,9 +140,10 @@ def enforce_require_any_filter # Active Admin reads a q-less index as "this table has no rows" and renders the blank # slate, Create link and all; a present-but-empty q renders an empty result instead. params[:q] ||= {} - flash.now[ActiveAdminFilterExtensions.warning_flash_key] = - notice.is_a?(String) ? notice : I18n.t(notice, scope: :active_admin_filter_extensions, - default: :require_any_filter) + unless notice.is_a?(String) + notice = I18n.t(notice, scope: :active_admin_filter_extensions, default: :require_any_filter) + end + flash_filter_warning(notice) end # Ransack silently ignores a condition nobody declared - junk, a stale bookmark after @@ -169,6 +170,13 @@ def any_declared_filter_answered? end end + # On Active Admin 4 the warning shares :alert with the app's own messages. A message that + # arrived with the request wins, as it does for the notice. + def flash_filter_warning(message) + key = ActiveAdminFilterExtensions.warning_flash_key + flash.now[key] = message if flash[key].blank? + end + def enforce_with_groups(q) # Membership is judged on the normalized request: a blank-array partner would pass a # raw present? check while Ransack drops it - half the group executing silently. @@ -181,8 +189,7 @@ def enforce_with_groups(q) filter_policy_errors << keys filter_policy_reset_keys.concat(keys) names = keys.map { |k| strip_ransack_predicate(k).humanize }.uniq.join(" and ") - flash.now[ActiveAdminFilterExtensions.warning_flash_key] = - I18n.t(:given_together, scope: :active_admin_filter_extensions, filters: names) + flash_filter_warning(I18n.t(:given_together, scope: :active_admin_filter_extensions, filters: names)) present.flatten.each { |key| q.delete(key) } end end diff --git a/spec/integration/require_any_filter_spec.rb b/spec/integration/require_any_filter_spec.rb index 1b194ac..3e32d8c 100644 --- a/spec/integration/require_any_filter_spec.rb +++ b/spec/integration/require_any_filter_spec.rb @@ -31,6 +31,14 @@ expect(page).to have_no_link("Create one") end + it "leaves an alert that arrived with the request in place" do + # On Active Admin 4 the warning and an action's alert share the :alert key. + visit "/admin/picky_posts/poke" + + expect(page).to have_content("poked") + expect(page).to have_no_content("keep me") + end + it "answers once asked" do visit "/admin/picky_posts?q%5Btitle_cont%5D=keep" diff --git a/spec/support/rails_template.rb b/spec/support/rails_template.rb index 3cba5d5..8ec2f4f 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -508,6 +508,12 @@ def filter_defaults_apply? filter :title_cont, as: :string, extensions: { validate: ->(value) { value.match?(/\\d/) && "Title must not contain digits." } } filter :status, as: :select, collection: %w[draft published] + + # Lands on the bare index, where the guard warns on the key Active Admin 4 also uses + # for :alert. + collection_action :poke do + redirect_to collection_path, alert: "poked" + end end ActiveAdmin.register Post, as: "EmptyDigitsPost" do From 63e1d771a22b560119b971f6b98472c614079121 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 14:16:45 +0200 Subject: [PATCH 8/9] Tidy the Active Admin 4 support after review Co-Authored-By: Clanker --- lib/active_admin_filter_extensions.rb | 4 +++- lib/active_admin_filter_extensions/filters_form.rb | 4 ++-- spec/integration/sidebar_spec.rb | 7 +++++++ spec/support/rails_template.rb | 5 +++++ tasks/test.rake | 2 ++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/active_admin_filter_extensions.rb b/lib/active_admin_filter_extensions.rb index bb6638b..5fd4522 100644 --- a/lib/active_admin_filter_extensions.rb +++ b/lib/active_admin_filter_extensions.rb @@ -35,7 +35,9 @@ def notice_flash_key # Active Admin 4 moved the filters form helper and the Current Filters section. def aa_v4? - Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new("4.0.0.beta1") + return @aa_v4 if defined?(@aa_v4) + + @aa_v4 = Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new("4.0.0.beta1") end # Active Admin 4's layout renders only the :error, :alert and :notice flash keys. diff --git a/lib/active_admin_filter_extensions/filters_form.rb b/lib/active_admin_filter_extensions/filters_form.rb index 231240c..656e9f3 100644 --- a/lib/active_admin_filter_extensions/filters_form.rb +++ b/lib/active_admin_filter_extensions/filters_form.rb @@ -27,8 +27,8 @@ module ResourceExtensionV4 def filters_sidebar_section name = :filters ActiveAdmin::SidebarSection.new name, only: :index, if: -> { active_admin_config.filters.any? } do - h3 I18n.t("active_admin.sidebars.#{name}", default: name.to_s.titlecase), - class: "filters-form-title", "data-filter-extensions-title" => "" + title = I18n.t("active_admin.sidebars.#{name}", default: name.to_s.titlecase) + h3 title, class: "filters-form-title", "data-filter-extensions-title" => "" active_admin_filters_form_for assigns[:search], visible_filters, FORM_OPTIONS end end diff --git a/spec/integration/sidebar_spec.rb b/spec/integration/sidebar_spec.rb index ada6a6d..3b5aff0 100644 --- a/spec/integration/sidebar_spec.rb +++ b/spec/integration/sidebar_spec.rb @@ -20,6 +20,13 @@ expect(page).to have_css(current_filters, text: "keep") end + it "keeps the Current Filters panel off where the resource turned it off" do + visit "/admin/quiet_default_posts" + + expect(page).to have_css("#filters_sidebar_section") + expect(page).to have_no_css(current_filters) + end + it "hides the Current Filters panel when nothing is filtered" do visit "/admin/no_defaults_posts" diff --git a/spec/support/rails_template.rb b/spec/support/rails_template.rb index 8ec2f4f..9278037 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -75,6 +75,11 @@ def current_admin_user filter :title, extensions: { default: "keep" } end + ActiveAdmin.register Post, as: "QuietDefaultPost" do + config.current_filters = false + filter :title, extensions: { default: "keep" } + end + ActiveAdmin.register Post, as: "ScalarPost" do filter :title_cont, as: :string, extensions: { default: "keep" } end diff --git a/tasks/test.rake b/tasks/test.rake index b5ed0b0..9fb8b69 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -16,6 +16,8 @@ task :setup do -m spec/support/rails_template.rb ].join(" ") + # Plain `rake setup` has not activated the bundle, and Gem.loaded_specs needs it. + require "bundler/setup" # One app per Active Admin major: its installer writes different assets and initializers. aa_major = Gem.loaded_specs["activeadmin"].version.segments.first app_dir = "spec/rails/rails-#{Rails::VERSION::STRING}-aa#{aa_major}" From e4db531c7ae6ca8bbd91af55e9c4f306e8910f68 Mon Sep 17 00:00:00 2001 From: Denis Talakevich Date: Thu, 12 Nov 2026 16:34:02 +0200 Subject: [PATCH 9/9] Remove app names from the spec template comments Co-Authored-By: Clanker --- spec/support/rails_template.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/rails_template.rb b/spec/support/rails_template.rb index 9278037..a5f191b 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -294,7 +294,7 @@ def filter_defaults_apply? default_filters_notice "Showing the default window" # The capped window: never unbounded (required:), never wider than the cap, never before - # the retention floor - the didww-rs/pbx/nessie range guards, as options. + # the retention floor - the range guards apps write by hand, as options. filter :published_date, as: :date_range, extensions: { required: true, max_range: 90.days, @@ -412,7 +412,7 @@ def filter_defaults_apply? end ActiveAdmin.register Post, as: "RejectPost" do - # :reject - the didww-api-3 add_integer_filter_check shape: flash and bounce back. + # :reject - the shape of a hand-written filter check: flash and bounce back. filter :title_cont, as: :string, extensions: { on_invalid: :reject, validate: ->(value) { value.match?(/\\d/) && "Title must not contain digits." } }