Skip to content

refactoring and improvements - #1

Merged
Fivell merged 8 commits into
mainfrom
derive-search-keys
Sep 22, 2026
Merged

Fivell merged 8 commits into
mainfrom
derive-search-keys

Conversation

@Fivell

@Fivell Fivell commented Sep 22, 2026

Copy link
Copy Markdown
Member

No description provided.

A default used to be written as a Ransack key: `default: { gteq_datetime: ... }`,
`default: { id_in: [...] }`, `default: { cont: "acme" }`. That is the query
language leaking into the filter declaration - the author wanted to say "show
the active ones by default" and had to say how Ransack spells it.

The input already knows. It is built against an empty search and asked, so a
value is enough: `:select` answers `_eq`, `:check_boxes` answers `_in` under the
association's primary key, `:date_range` answers both bounds, and `:string`
answers whichever predicate heads its dropdown - which `filters:` on the filter,
or `string_input_filters` on the resource or namespace, may have reordered. A
hardcoded table would have been wrong in all three of those cases.

Asking also carries a host app's own patches for free. One of ours rewrites
DateRangeInput#gt_input_name to append `_datetime` for datetime columns, and the
derived key follows it without the gem knowing anything about it.

Two details the inputs do not volunteer. `current_filter` raises on a filter
whose name already carries its predicate, since it goes looking for
`title_eq_cont`, so `seems_searchable?` is checked first. And it prefers a
predicate the current request already carries, which is the wrong answer for a
default, so the input is built against an empty search rather than the live one.

A Hash still names predicates outright, unchanged, for when the input's own is
not the one you want. A value that cannot be placed - a single value for a
two-ended input - raises rather than picking an end.
#view_context instantiates a fresh view class on every call, and deriving a
search key was calling it once per defaulted filter. The builder depends only on
the resource class and the view, which are the same for every filter in a
request, so there is nothing to keep apart.

The input now also takes its template and object from the builder rather than
being handed separately built ones, which could have disagreed with it.

Per-boot caching would go further - the derived keys do not depend on the
request at all - but it needs a holder that resets when Active Admin reloads,
and derivation only runs on requests that carry no filters, for filters that
declare a default. That is one or two per page here, so it is an optimisation to
make on a measurement rather than on a hunch.
The examples handed out a literal Range for a moving window, which is wrong in
the way that does not show up in a test: `filter` runs when the resource file is
loaded, so `default: 1.week.ago..` pins the window to boot and lets it drift for
the life of the process. Short-lived processes hide it; long-running workers do
not.

Caught while trying the branch on a real app, where the author had written the
Proc form without being told to.
DataAccess had grown into three jobs under one name: replacing Active Admin's
#apply_filtering, working out what the declared defaults come to, and building
Formtastic inputs to ask them which Ransack key they submit under. Only the
first is data access; the last is view introspection that never touches the
collection.

DataAccess is now the one method this gem replaces, eighteen lines of it, and it
reads `filtering_params`. FilterDefaults is everything that method means.

It is included rather than prepended. Nothing in it replaces anything Active
Admin defines, and every seam in it - #filtering_params,
#filter_defaults_apply?, #visible_filters - is one a resource is meant to
override in its own `controller do` block. Including puts the class ahead of the
module, which is the order that wants.

The unit harness got simpler in the process, which is the useful signal: it had
to prepend its stand-in for the key derivation, because the real one sat in a
prepended module and a method on the class could never be reached. Now it is a
plain method on the class.
Two things found by trying the branch on real apps.

A `:string` filter submits the head of its predicate dropdown, and an app that
re-registers Ransack's `contains` / `equals` / `starts_with` aliases reorders
that list - so a bare string default searches for equality there, not a
substring. Correct, since it is what the form submits, and surprising to anyone
reading the example here.

A resource Ransack cannot search has no input to ask. Five pages in one app are
ActiveResource models fronting an HTTP API; the derivation raises with the
reason and the remedy rather than guessing a key.
The suite proved the collection came back filtered, and proved the value reached
the form for exactly one input type. That is the wrong half to leave untested: a
default that filters invisibly is the failure worth catching, and it is also
what Clear Filters returning to the defaults rests on.

Every type now asserts the rendered value - the selected option of a select and
of a boolean, the checked box of a check boxes filter, both ends of a date
range, the numeric field under the predicate its Hash named, the string field,
and the one read off the signed-in admin.

They discriminate: on a resource that declares no default the same field renders
with no value attribute at all, so the assertions fail there.
The filters form resolves `:input_html` against the view before it builds an
input; the derivation was handing the Proc straight over. That is not merely
incomplete, it is wrong in the way nothing reports: `Proc#[]` is `call`, so
Formtastic asking `input_html[:multiple]` invoked the Proc, got a truthy Hash
back, and a :select derived `_in` instead of `_eq` - a key that filters nothing,
with no error.

`input_html: proc { ... }` is ordinary in Active Admin and appears several times
in one of the apps this was tried on, so this would have shipped and then failed
quietly on whichever filter happened to combine the two.
The set was written out twice, in different orders - once where the filters form
strips them, once where the derivation builds its input. They have to agree, and
nothing made them: a fourth option added to the gem would have been remembered
in one place and forgotten in the other, reaching Formtastic silently. Which is
how the Proc in :input_html got through.

FilterDefaults owns what `:default` means, so it owns the list.
@Fivell
Fivell merged commit b64b0c7 into main Sep 22, 2026
4 checks passed
@Fivell
Fivell deleted the derive-search-keys branch September 22, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant