Skip to content

Configure filters in the LiveResource like fields - #2431

Draft
Flo0807 wants to merge 1 commit into
feature/adapter-neutral-filters-and-metricsfrom
feature/generic-filters
Draft

Flo0807 wants to merge 1 commit into
feature/adapter-neutral-filters-and-metricsfrom
feature/generic-filters

Conversation

@Flo0807

@Flo0807 Flo0807 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #357.

Stack (merge in this order)

  1. Build queries only in the Ecto adapter #2338 Build queries only in the Ecto adapter, based on develop
  2. Let the adapter turn filter conditions and metric aggregates into queries #2340 Let the adapter turn filter conditions and metric aggregates into queries
  3. Configure filters in the LiveResource like fields #2431 Configure filters in the LiveResource like fields ← this PR
  4. Describe the resource in the adapter and cast expressions with it #2448 Describe the resource in the adapter and cast expressions with it
  5. Read through a Backpex.Query #2449 Read through a Backpex.Query
  6. Apply the parts of a query an adapter doesn't run in memory #2450 Apply the parts of a query an adapter doesn't run in memory
  7. Write through neutral changes and let adapters declare writes #2451 Write through neutral changes and let adapters declare writes
  8. Replace item_query, :select and :custom_alias with scope, load and computed values #2454 Replace item_query, :select and :custom_alias with scope, load and computed values
  9. Compute aggregates of related items #2455 Compute aggregates of related items

Note

Stacked on #2340 (which is stacked on #2338). It builds on the expression-based filter contract of #2340, so the base branch is feature/adapter-neutral-filters-and-metrics. Both PRs target v0.25, so users migrate query/4 once, straight to the callbacks of this PR.

What changes

Filters now work like fields: a LiveResource configures the built-in filters with options instead of defining a module per filter.

def filters do
  [
    category_id: %{module: Backpex.Filters.Select, label: "Category", options: &category_options/1},
    user_id: %{module: Backpex.Filters.MultiSelect, label: "Users", prompt: "Select users ...", options: &user_options/1},
    likes: %{module: Backpex.Filters.Range, label: "Likes", type: :number},
    published: %{
      module: Backpex.Filters.Boolean,
      label: "Published?",
      options: [%{label: "Published", key: "published", predicate: expr(published == true)}]
    },
    age: %{
      module: Backpex.Filters.Select,
      label: "Age",
      options: [{"Under 18", "minor"}, {"18 and older", "adult"}],
      condition: fn
        {field, _config}, "minor", _assigns -> expr(ref(^field) < 18)
        {field, _config}, "adult", _assigns -> expr(ref(^field) >= 18)
      end
    }
  ]
end
  • Config schema per filter, like fields (towards Uniform Options #336): use Backpex.Filter, config_schema: [...]. Backpex.Filter.validate_config!/2 validates every filter with NimbleOptions against the general options (module, label, default, presets, can?, condition) and the module's schema. It raises on unknown or invalid options, or when a filter has neither a :label nor label/0. Backpex.LiveResource.active_filters/1 returns validated filters.
  • Generic built-in filters: Select and MultiSelect take :options (a list or a function of the assigns) and :prompt. Range takes :type (:date, :datetime or :number) and Boolean takes :options.
  • Reusable modules still work: use Backpex.Filters.Select and the other built-in filters keep their callbacks (options/1, prompt/0, type/0). The callbacks become the defaults of the options, and options in the LiveResource take precedence. Modules that only set label, prompt, options and can? keep working unchanged.
  • New general options:
    • :can? is ANDed with the module's can?/1, so per-resource visibility no longer needs a module.
    • :condition replaces the module's condition/3 for a single filter. It covers filters that only exist to change the condition, e.g. soft-delete selects, overlap ranges or timezone-shifted date ranges.
  • Callbacks receive the filter {name, config}, consistent with fields:
    • type/1 is now type/2 and validate/2 is now validate/3. A filter that still defines either doesn't compile.
    • changeset/3 and condition/3 receive the tuple instead of the field. Old changeset/3 code raises a FunctionClauseError in Ecto instead of passing silently (verified).
    • render/1 and render_form/1 get @filter.
  • Adapters apply filters via Backpex.Filter.to_condition/3, which honours the :condition option.
  • New translation: "Select an option..." (default prompt of the select filter).

Why the tuple instead of passing options through assigns

Fields already pass {name, field_options} to their callbacks, and the issue asks for filters to work the same way. The breaking surface stays small:

Demo

  • Replaced six demo filter modules with inline configs (Posts, Products).
  • Kept PostCategorySelect as an example of a reusable filter module.
  • Added role and age filters to Users; age uses :condition.

Docs

  • Rewrote How to add a Filter, and updated the custom filter, validation, presets and visibility guides.
  • Added a "Filters are configured like fields" section with before/after examples to the v0.25 upgrade guide.

Merged with #2340 (and develop through it)

develop's filter fixes are ported to the generic filters:

  • Backpex.Filters.Boolean ignores selected keys without an option.
  • Backpex.Filters.Range refuses nested boundaries as a cast error and parses the boundaries of a :number range on an integer field as integers, with the field type from Backpex.Resource.field_type/2.

develop's new filter tests use the {name, config} callbacks and validated filter configs.

Checks

  • Merged with Let the adapter turn filter conditions and metric aggregates into queries #2340's expressions (2026-10-28): the filter modules, guides and tests of this PR return Backpex.Expr expressions; the :condition option and the demo's age filter use expr/1. mix test incl. integration_test/ 1442 passed (182 doctests), mix test --only e2e 60 passed, mix format --check-formatted, mix credo, mix deps.unlock --check-unused and mix gettext.extract --check-up-to-date pass. Demo: 51 tests, the 38 Playwright tests and bun run lint pass. The create-filter skill now shows the {field, _config} callbacks of this PR.
  • Merged with the updated Let the adapter turn filter conditions and metric aggregates into queries #2340 (2026-10-28): mix test incl. integration_test/ 1406 passed (173 doctests), mix test --only e2e 60 passed, mix format --check-formatted, mix credo and mix deps.unlock --check-unused pass. Demo: 51 tests, the 38 Playwright tests and bun run lint pass.
  • Backpex, merged with the current Let the adapter turn filter conditions and metric aggregates into queries #2340: 842 tests and 173 doctests, mix credo (no issues), mix compile --warnings-as-errors --force, mix format --check-formatted, mix docs (no warnings), Sobelow, mix deps.unlock --check-unused and mix gettext.extract --check-up-to-date pass.
  • Demo, on the merged head: 396 tests and 3 doctests, the 52 Playwright tests and bun run lint pass.
  • Manually in the demo (before the merge):
    • Users: the role and age selects render the default prompt, and selecting Admin applies the filter and shows the "Role · Admin" badge.
    • Posts: all five filters render from their config, the default redirect works, and the "Last 7 Days" preset applies.

@codecov

codecov Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

- Configure the built-in filters in the LiveResource like fields
- Document configuring filters in the LiveResource
- Configure the demo filters in the LiveResources
- Cover the options of the generic filters in tests
@Flo0807
Flo0807 force-pushed the feature/generic-filters branch from 26ae38d to 7ae5489 Compare November 13, 2026 09:46

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change A breaking change feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generic Filters

1 participant