active_admin_filter_extensions 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51bc6cafe8895502a79f648a96f6c7e4f28fbb9c3a985eac07c43aa7d86e320f
4
- data.tar.gz: 37c3a1beef3da668ac558dc7364b076f8212d980f9efce5da8110213fac3aa22
3
+ metadata.gz: d0c78b8910b00dd6300b7b3eebb5e5538eb534df7bb4ae2f37e206ed7c7e0a9b
4
+ data.tar.gz: 39b6b4f970c13cf26b71837b73463e9b1d13468eb2c6150deb4513759ddad7a6
5
5
  SHA512:
6
- metadata.gz: a4fbb0cfa85464dbee996d124f7fa47120bd618e04a2fe645683b8a32593107921d0eda74ad39cbb8bb3898e23c4816ae9453c13b25e7eb52dfd31740def0535
7
- data.tar.gz: d81c7d2722af7494ab5ff73255ea76818ed826d8196b9f04e9b5c94417ac1d29bbc24696311124b80394ab29826e26f58150a5a49f74235d18edb55b65b5a626
6
+ metadata.gz: a7854f6af602da404309b0734e83ab47b1d60df0a7ed7667abecc4525efb5e0b1d70f9f459bf43a30179e3823bf5621efab8ae969494cec12ffe9830d8214904
7
+ data.tar.gz: a5681dbf451ed707c92c6ce2be45cba12e4f7aaa81854eed31b783fc4335aa65720c26f7e6f83ef67a7d9638a93034bd8a2e20172efb37c9492986bb7464d167
data/README.md CHANGED
@@ -170,7 +170,17 @@ end
170
170
  ```
171
171
 
172
172
  The message is flashed only when the defaults actually took effect. Pass `flash_key:` to use
173
- something other than `:notice`.
173
+ something other than `:notice`, or move the default for the whole application once:
174
+
175
+ ```ruby
176
+ # config/initializers/active_admin_filter_extensions.rb
177
+ ActiveAdminFilterExtensions.notice_flash_key = :filters_notice
178
+ ```
179
+
180
+ Worth doing where the layout renders its own key for "why this page is cut down": Active Admin
181
+ writes action results to `:notice`, and on a shared key the arriving message wins, so a batch
182
+ action's result would silence the notice. The key is read when the notice is written and
183
+ `flash_key:` on the resource still beats it.
174
184
 
175
185
  ## Required filters: the mandatory window
176
186
 
@@ -348,7 +358,10 @@ Four prints in small type:
348
358
  never a condition and is never touched.
349
359
  * **Anything that reads `params[:q]` for the effective filters** (drill-down links, exports)
350
360
  should read `filtering_params` instead - on the rendered page they agree, but only
351
- `filtering_params` is also right on the non-redirected formats.
361
+ `filtering_params` is also right on the non-redirected formats. A page that translates those
362
+ values before passing them on - renaming a key an input suffixed, turning a sentinel into
363
+ another predicate - wants `applied_filter_params`: the same values as an indifferent copy,
364
+ safe to read by symbol and to mutate. Both are helpers, so a view can call them too.
352
365
 
353
366
  Tested against Active Admin 3.5 on Rails 8.0 and 8.1, under Ruby 3.3, 3.4 and 4.0.
354
367
 
@@ -28,7 +28,7 @@ module ActiveAdminFilterExtensions
28
28
  # #filter_defaults_apply? which lets defaults through on a partly filtered request keeps
29
29
  # the values that were actually asked for.
30
30
  #
31
- # @return [Hash, ActionController::Parameters] values passed to Ransack
31
+ # @return [Hash] values passed to Ransack
32
32
  def filtering_params
33
33
  requested = requested_filtering_params
34
34
  # Materialized on every path: computing it is also what validates, loudly, that every
@@ -56,6 +56,20 @@ module ActiveAdminFilterExtensions
56
56
  defaults.merge(requested || {})
57
57
  end
58
58
 
59
+ # The same values, as an indifferent deep copy: readable by symbol and safe to mutate. A page
60
+ # that builds its own collection or its own links usually has to translate those values
61
+ # into whatever it talks to - renaming a key an input suffixed, turning a sentinel into a
62
+ # different predicate - and must not do that to the Hash #filtering_params itself returns,
63
+ # which is also what fills the filters form and decides the canonicalizing redirect.
64
+ #
65
+ # @return [ActiveSupport::HashWithIndifferentAccess]
66
+ def applied_filter_params
67
+ # deep_dup, not just with_indifferent_access: that copies the containers but keeps the
68
+ # value objects, so a `gsub!` or `replace` on a String value would reach back into the
69
+ # request's own and change what the next #filtering_params answers.
70
+ filtering_params.deep_dup.with_indifferent_access
71
+ end
72
+
59
73
  def complete_with_groups(defaults)
60
74
  with_groups.each do |keys|
61
75
  next unless keys.any? { |key| defaults.key?(key) }
@@ -12,7 +12,12 @@ module ActiveAdminFilterExtensions
12
12
  # message is only flashed when the defaults actually took effect, so it stays quiet once the
13
13
  # admin has filtered for themselves.
14
14
  module Notice
15
- def default_filters_notice(message, flash_key: :notice)
15
+ # The key this resource's notice goes under: what it declared, else the app-wide default.
16
+ def self.flash_key_for(config)
17
+ config.default_filters_notice_flash_key || ActiveAdminFilterExtensions.notice_flash_key
18
+ end
19
+
20
+ def default_filters_notice(message, flash_key: nil)
16
21
  # Declared on the resource rather than closed over, so that the redirect - which answers
17
22
  # the request the notice belongs on with a 302 - can find it and carry it across.
18
23
  config.default_filters_notice = message
@@ -27,7 +32,8 @@ module ActiveAdminFilterExtensions
27
32
  # default slice - the notice on top of it would be noise.
28
33
  next if filter_policy_errors.present?
29
34
 
30
- flash.now[flash_key] = ::MethodOrProcHelper.render_in_context(self, message)
35
+ flash.now[Notice.flash_key_for(active_admin_config)] =
36
+ ::MethodOrProcHelper.render_in_context(self, message)
31
37
  end
32
38
  end
33
39
  end
@@ -71,7 +71,7 @@ module ActiveAdminFilterExtensions
71
71
  filter_policy_errors.empty? && filter_defaults_engaged?
72
72
  # On a shared key - the default is :notice, the same key a batch action writes - the
73
73
  # arriving message wins: it names what just happened, the notice can wait.
74
- key = active_admin_config.default_filters_notice_flash_key
74
+ key = Notice.flash_key_for(active_admin_config)
75
75
  flash[key] = ::MethodOrProcHelper.render_in_context(self, message) if flash[key].blank?
76
76
  end
77
77
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveAdminFilterExtensions
4
- VERSION = "1.0.1"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "activeadmin"
4
+ require "active_support/core_ext/object/deep_dup"
4
5
  require "active_admin_filter_extensions/version"
5
6
 
6
7
  # The violation messages live in plain i18n - config/locales/en.yml here, overridable by the
@@ -17,6 +18,21 @@ require "active_admin_filter_extensions/policies"
17
18
  require "active_admin_filter_extensions/notice"
18
19
 
19
20
  module ActiveAdminFilterExtensions
21
+ class << self
22
+ # Where a filters notice goes when the resource does not say. Active Admin writes action
23
+ # results to :notice, and an app whose layout renders its own key for "why this page is
24
+ # cut down" would otherwise repeat `flash_key:` on every index that declares a notice:
25
+ #
26
+ # ActiveAdminFilterExtensions.notice_flash_key = :notice_message
27
+ #
28
+ # Read when the notice is written, not when the resource is registered, so initializer
29
+ # order does not decide it.
30
+ attr_writer :notice_flash_key
31
+
32
+ def notice_flash_key
33
+ @notice_flash_key ||= :notice
34
+ end
35
+ end
20
36
  end
21
37
 
22
38
  # `add_filter` stores whatever options it is given without a whitelist, so `default:` needs no
@@ -30,7 +46,8 @@ ActiveAdmin.before_load do |_app|
30
46
  ActiveAdmin::ResourceController.include ActiveAdminFilterExtensions::SearchKeys
31
47
  ActiveAdmin::ResourceController.include ActiveAdminFilterExtensions::RequiredFilters
32
48
  ActiveAdmin::ResourceController.include ActiveAdminFilterExtensions::FilterDefaults
33
- ActiveAdmin::ResourceController.helper_method :filtering_params, :filter_default_values, :visible_filters
49
+ ActiveAdmin::ResourceController.helper_method :filtering_params, :applied_filter_params,
50
+ :filter_default_values, :visible_filters
34
51
 
35
52
  ActiveAdmin::Resource.prepend ActiveAdminFilterExtensions::ResourceExtension
36
53
  ActiveAdmin::ResourceDSL.include ActiveAdminFilterExtensions::Policies::ResourcePolicyDSL
@@ -53,6 +53,36 @@ RSpec.describe ActiveAdminFilterExtensions::DataAccess do
53
53
  end
54
54
  end
55
55
 
56
+ describe "#applied_filter_params" do
57
+ let(:filters) { { position_eq: { extensions: { default: "20" } } } }
58
+
59
+ it "reads by symbol what the request spelled as a String" do
60
+ expect(controller.send(:applied_filter_params)[:position_eq]).to eq("20")
61
+ end
62
+
63
+ it "hands out a copy, so a caller translating the values cannot rewrite the search" do
64
+ applied = controller.send(:applied_filter_params)
65
+ applied[:position_gt] = applied.delete(:position_eq)
66
+
67
+ expect(controller.send(:filtering_params)).to eq("position_eq" => "20")
68
+ expect(searched_with).to eq("position_eq" => "20")
69
+ end
70
+
71
+ context "when the request carries a value of its own" do
72
+ let(:filters) { { title_cont: {} } }
73
+ # Mutable on purpose: with_indifferent_access alone copies the Hash but hands out this
74
+ # very String, so editing it in place would reach back into the request.
75
+ let(:params) { { q: { "title_cont" => String.new("hello") } } }
76
+
77
+ it "copies the values too, so editing one in place cannot reach the request" do
78
+ controller.send(:applied_filter_params)[:title_cont].replace("bye")
79
+
80
+ expect(controller.send(:filtering_params)).to eq("title_cont" => "hello")
81
+ expect(searched_with).to eq("title_cont" => "hello")
82
+ end
83
+ end
84
+ end
85
+
56
86
  describe "a Hash default" do
57
87
  let(:filters) { { created_at: { as: :date_range, extensions: { default: { gteq: "2020-01-01" } } } } }
58
88
 
@@ -191,6 +191,33 @@ RSpec.describe "Filter default values", type: :feature do
191
191
 
192
192
  expect(page).to have_no_content("Showing published posts by default")
193
193
  end
194
+
195
+ describe "the app-wide key" do
196
+ # Deliberately a key no resource declares, so an example cannot pass by coincidence:
197
+ # NoticePost's own key is :filters_notice.
198
+ around do |example|
199
+ previous = ActiveAdminFilterExtensions.notice_flash_key
200
+ ActiveAdminFilterExtensions.notice_flash_key = :app_wide_notice
201
+ example.run
202
+ ActiveAdminFilterExtensions.notice_flash_key = previous
203
+ end
204
+
205
+ it "takes a notice that named no key of its own" do
206
+ # The resource says only the message; the app said where its notices go. Asserted on
207
+ # the rendered flash class, which is what the layout keys off.
208
+ visit "/admin/colliding_notice_posts"
209
+
210
+ expect(page).to have_css(".flash_app_wide_notice", text: "Showing published posts by default")
211
+ expect(page).to have_no_css(".flash_notice")
212
+ end
213
+
214
+ it "loses to the key the resource declared" do
215
+ visit "/admin/notice_posts"
216
+
217
+ expect(page).to have_css(".flash_filters_notice", text: "Showing published posts by default")
218
+ expect(page).to have_no_css(".flash_app_wide_notice")
219
+ end
220
+ end
194
221
  end
195
222
 
196
223
  describe "requests that carry no q of their own" do
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/object/blank"
4
+ require "active_support/core_ext/object/deep_dup"
4
5
  require "active_support/core_ext/hash/indifferent_access"
5
6
  require "active_support/hash_with_indifferent_access"
6
7
  # Pure Ruby, no Active Admin boot required - `visible_filters` resolves `:if` / `:unless` with it.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_admin_filter_extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Fedoronchuk