Skip to content

Conversation

@goosys
Copy link
Contributor

@goosys goosys commented Oct 27, 2023

I made the following changes to enable the use of Ransack's extensive predicates expressions, in addition to ransackable_scope.
Please review and let me know if any further changes are needed.


Changes

  • Add AdministrateRansack.ransack? helper to check if a field name with predicates is valid.
  • Fix the labels to enable automatic translation of field names with predicates in i18n.

Breaking changes

  • Drop admin_scope option
    • For associations, admin_scope is no longer necessary as updated to use Field.associated_resource to get the collection.
    • If we want to do something similar, we can do it with Field::BelongsTo like this:
      • Field::BelongsTo.with_options(scope: ->{ Post.published })
    • Since Field::HasMany does not have a scope option, it cannot be done in the same way, but it can be achieved using the Field::ScopedHasMany plugin.
  • Drop admin_label option
    • For associations, use the value of display_resource as the label to match the title or heading of each admin pages.

@goosys goosys force-pushed the feature/predicates branch from 6b049a9 to 7d9478a Compare July 26, 2024 08:34
@blocknotes
Copy link
Owner

Hey @goosys
Sorry for the loong delay on this PR. I came back only recently to this project.

Thank you very much for your contribution here.
But I prefer to avoid merging this PR for a couple of reasons:

  • it contains breaking changes (the removal of admin_label) (which can be fine but they will need to wait a major version, plus I prefer to have them isolated in a specific PR);
  • it contains changes of different kind, I usually try to make the scope as small as possible for maintainability and clean history.

Since it passed quite some time, I'll evaluate to create specific PRs based on this one 👍

Copy link
Owner

@blocknotes blocknotes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As anticipated, I prefer to create more scoped PRs, to avoid breaking changes for now (but soon I would like also to consider breaking changes too), to avoid accessing private instance variables.

<% association = model.reflections[field.to_s] %>
<% if association %>
<% field_key = model.ransackable_scopes.include?(field) ? field : "#{field}_id_eq" %>
<% desc = association.klass.method_defined?(:admin_label) ? :admin_label : :to_s %>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, I prefer to have a specific PR for it. And it can be merged only with the next major version.

class << self
def ransack?(model, params = {}, options = {})
ransack = model.ransack(params, **options)
ransack.instance_variable_get(:@scope_args).present? || ransack.base.c.present?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general idea is good but I prefer to avoid accessing internal Ransack details (like @scope_args instance var) because they do not belong to the public API of the gem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a feature currently missing in Ransack, and I'm hoping it will eventually be implemented there — though it hasn't happened yet (there are some related PRs, though).

It might be faster to contribute directly to Ransack, but if we want to avoid that for now, how about something like the following as a workaround?

def ransack?(model, params, **options)
  begin
    model.ransack!(params, **options)
    true
  rescue StandardError
    false
  end
end

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm... perhaps you could try something like:

  def ransack?(model, field)
    ctx = Ransack::Context.for(model)
    ctx.ransackable_attribute?(field, model) ||
      ctx.ransackable_association?(field, model) ||
      ctx.ransackable_scope?(field, model)
  end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that only checks the model definition, but it doesn’t reflect which field names can actually be used in Ransack.

One of the goals of this PR is to support Ransack-style search conditions like name_or_description_or_email_or_articles_title_cont.
To do that, we need to determine whether a given field is usable after it has gone through all of Ransack’s initialization processes, including things like groupings.

@goosys
Copy link
Contributor Author

goosys commented Apr 30, 2025

I also think the general idea is good, though I believe some of the details still need further discussion.
For example, there's quite a bit of logic in the view files right now, and I think we should try to separate that more cleanly.

Administrate still supports Rails 6.0, but since this gem has already dropped it, we could consider using Renderable Object from Rails 6.1 to encapsulate the logic. What do you think?

@blocknotes
Copy link
Owner

I also think the general idea is good, though I believe some of the details still need further discussion. For example, there's quite a bit of logic in the view files right now, and I think we should try to separate that more cleanly.

Administrate still supports Rails 6.0, but since this gem has already dropped it, we could consider using Renderable Object from Rails 6.1 to encapsulate the logic. What do you think?

Those fields' view does their job for me, but any improvement could be nice (if proposed in a specific PR).

Do you mean having something similar to view components?
My fear using that approach is related to the complexity of the views, for example I wonder how a view like app/views/administrate_ransack/components/_field_has_many.html.erb could become.

@goosys
Copy link
Contributor Author

goosys commented May 2, 2025

The ViewComponent library is separate from Rails itself — Rails only provides support for Renderable objects.
Thanks to the render_in method, we can now pass objects that implement it directly to the render helper.
This allows us to encapsulate business logic within a Renderable object instead of putting it in the view.

Here’s a simplified example. A lot is omitted, but this is the general idea:
By moving business logic out of the view files, the views themselves become much cleaner and easier to maintain.

# lib/administrate_ransack/view/field/base.rb
module AdministrateRansack
  module View
    module Field
      class Base
        def initialize(); end
        def render_in(view_context); end
        def render?; end
        private
        def ransack?; end

# lib/administrate_ransack/view/field/has_many.rb
module AdministrateRansack
  module View
    module Field
      class HasMany < Base
        def render_in(view_context)
          @view_context = view_context
          return unless render?
          view_context.render template_name, local_assigns
        end
        def render?
          super && model.reflections[field.to_s]
        end
        private
        def template_name
          "components/field_has_many"
        end
        def local_assigns
          {
            form: @form,
            field_key: field_key,
            resource_field: resource_field,
            collection: collection
          }
        end
        ...
<%# app/views/administrate_ransack/_filters.html.erb %>
...
<% component = AdministrateRansack::FILTERS[input_type] || 'field_other' %>
<% view_component =  <<Some logics to find view class like `AdministrateRansack::View::Field::HasMany`>> %>
<div class="filter filter-...">
  <%= render view_component.new(
        form: f, model: model, field: field, label: label, type: type, options: options[field]
  ) %>
<%# app/views/administrate_ransack/components/_field_has_many.html.erb %>
<%= form.label(label || field_key, class: 'filter-label') %>
<%= form.select(field_key, collection, {}, multiple: true) %>

What do you think? Do you think it's worth discussing further?
If it seems promising, I'd be happy to open a dedicated issue and PR for it — would you be willing to take a look if I do?

@blocknotes
Copy link
Owner

What do you think? Do you think it's worth discussing further? If it seems promising, I'd be happy to open a dedicated issue and PR for it — would you be willing to take a look if I do?

Yep, the approach is similar to view components.

I'll evaluate better your proposal, but to be honest I'm thinking to go in a different direction at the moment.
Recently I started to work to a major refactoring, here is a preview:
https://github.com/blocknotes/administrate_ransack/pull/44/files

In this way I'll be able to have simpler views and more freedom on the filters customization:

  RANSACK_SEARCH = {
    title: {
      type: :string,
      param: :title_cont
    },
    published: :boolean,
    position: :number,
    by_category: {
      type: :string,
      scope: true
    },
    dt: :date,
  }

I do not exclude the possibility to use renderable / view components / Phlex in the future.

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.

2 participants