Skip to content

Instantly share code, notes, and snippets.

@tinynumbers
Last active April 21, 2024 08:57
Show Gist options
  • Save tinynumbers/5896537 to your computer and use it in GitHub Desktop.
Save tinynumbers/5896537 to your computer and use it in GitHub Desktop.
ActiveAdmin controller extension to persist resource index filters between requests.
# this stuff goes in config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
# ...
# put these lines in the "Controller Filters" section of the ActiveAdmin.setup block
# These two are defined in ActiveAdmin::FilterSaver::Controller, which is loaded below.
config.before_filter :restore_search_filters
config.after_filter :save_search_filters
# ...
end
# put the following lines below the main ActiveAdmin.setup block (also in config/initializers/active_admin.rb)
require 'active_admin/filter_saver/controller'
ActiveAdmin.before_load do |app|
# Add our Extensions
ActiveAdmin::BaseController.send :include, ActiveAdmin::FilterSaver::Controller
end
# -*- encoding : utf-8 -*-
# put this in lib/active_admin/filter_saver/controller.rb
module ActiveAdmin
module FilterSaver
# Extends the ActiveAdmin controller to persist resource index filters between requests.
#
# @author David Daniell / тιηуηυмвєяѕ <info@tinynumbers.com>
module Controller
private
SAVED_FILTER_KEY = :last_search_filter
def restore_search_filters
filter_storage = session[SAVED_FILTER_KEY]
if params[:clear_filters].present?
params.delete :clear_filters
if filter_storage
logger.info "clearing filter storage for #{controller_key}"
filter_storage.delete controller_key
end
if request.post?
# we were requested via an ajax post from our custom JS
# this render will abort the request, which is ok, since a GET request will immediately follow
render json: { filters_cleared: true }
end
elsif filter_storage && params[:action].to_sym == :index && params[:q].blank?
saved_filters = filter_storage[controller_key]
unless saved_filters.blank?
params[:q] = saved_filters
end
end
end
def save_search_filters
if params[:action].to_sym == :index
session[SAVED_FILTER_KEY] ||= Hash.new
session[SAVED_FILTER_KEY][controller_key] = params[:q]
end
end
# Get a symbol for keying the current controller in the saved-filter session storage.
def controller_key
#params[:controller].gsub(/\//, '_').to_sym
current_path = request.env['PATH_INFO']
current_route = Rails.application.routes.recognize_path(current_path)
current_route.sort.flatten.join('-').gsub(/\//, '_').to_sym
end
end
end
end
# put this in e.g. app/assets/javascripts/admin/index-filters.coffee
# and include this in app/assets/javascripts/active_admin.js
$ ->
# Extend the clear-filters button to clear saved filters
$('.clear_filters_btn').click (evt) ->
# This will send a synchronous post with clear_filters set to true -
# our AA FilterSaver controller extension looks for this parameter to
# know when to clear session-stored filters for a resource - and then
# the default AA clear-filters button behavior will issue a get request
# to actually re-render the page.
$.ajax this.href, {
async: false,
data: { clear_filters: true },
type: 'POST'
}
@tinynumbers
Copy link
Author

This works by calling a before_filter to restore any saved search filters, and an after_filter to save any filters from the current request.

Filters are only restored if the current index page request does not contain a :q param (i.e., new filter values).

There's a smelly hack in here to ajax-post a "clear filters" request just before the GET request attached to the filter form "clear filters" link. This is because the "clear filters" button just sends a request for the index page, without any :q parameter. So the ajax request clears the saved filters before this :q-less request comes through (otherwise the :q-less request would trigger restoring of the saved filters).

In a real implementation of this, the clear-filters button would send a special value for :q which would cause the filters to be cleared. If I make a pull request for this feature, that's how it will be implemented.

@astamm78
Copy link

astamm78 commented Sep 5, 2013

This is a great help. Thanks

@elsurudo
Copy link

This is great, thanks!

The only problem is I was getting a "method not found" error on my ActiveAdmin::Devise::SessionsController when logging in/out.

I added ActiveAdmin::Devise::SessionsController.send :include, ActiveAdmin::FilterSaver::Controller to my ActiveAdmin.before_load block to fix that up. Are you aware of any pitfalls in my band-aid solution?

@mugar
Copy link

mugar commented Sep 12, 2014

Hey,
I tried your solution in my app and I'm getting this error when I launch the rails built in server /home/mugabo/.rvm/gems/ruby-2.1.1@railstutorial_rails_4_0/gems/activesupport-4.0.8/lib/active_support/dependencies.rb:461:in `load_missing_constant': Circular dependency detected while autoloading constant Base (RuntimeError)

I have no idea what's wrong with my config. Thx for help.

@gnapse
Copy link

gnapse commented Oct 7, 2014

Nice extension @tinynumbers.

Just one note. When I applied this to my app, I had to make a small adjustment so that the fix would not get loaded in my devise-powered ActiveAdmin's login page.

# config/initializers/active_admin.rb

ActiveAdmin.setup do |config|

  # ...

  config.before_filter :restore_search_filters, unless: :devise_controller?
  config.after_filter :save_search_filters, unless: :devise_controller?

  # ...

end

Notice the unless clause on the filters. Without this, the devise controller was trying to invoke those filters when visiting the login page, but since this controller does not derive from ActiveAdmin::BaseController, it raised a 'method not found' error.

I hope this is useful for anyone else out there in a similar situation ;)

@vincenzodev
Copy link

Rails 4.1.8 not work!

Fix
def controller_key
#params[:controller].gsub(///, '').to_sym
current_path = request.env['PATH_INFO']
current_route = Rails.application.routes.recognize_path(current_path)
current_route.sort.flatten.join('-').gsub(///, '
').to_s
end

controller_key must return a string

@mgraham
Copy link

mgraham commented Dec 22, 2014

On an active_admin resource that does not accept POST requests, the clear_filters button will fail. In my case, I fixed this to add a dummy route and controller action.

Is there a way for the clear_filters enable POST requests if they are not already enabled?

@wpp
Copy link

wpp commented May 6, 2015

Thank you @tinynumbers. After @vincenzodev's suggestion seems to work in 4.2.0 as well.

@mgraham
Copy link

mgraham commented Mar 10, 2016

To make clear filters also work for CSV downloads, I added the following lines:

            filter_storage.delete "#{controller_key}-format-csv".to_sym
            filter_storage.delete "#{controller_key}-format-xml".to_sym
            filter_storage.delete "#{controller_key}-format-json".to_sym

@Hassan9229
Copy link

How can we get session result in resource model?

@JuanLUJoanne
Copy link

Hi,
I follow the above instruction.
I have add the

active_admin.rb, controller.rb, index-filters.coffee

to my code base.
But I found restore_search_filters is not called.
the params[:q] is nil.
who can help to tell me what's missing ?
Thanks.

user.rb

ActiveAdmin.register User do
  menu parent: 'User Management'
  config.filters = false


controller do
    before_filter :filter_params, only: [:index]
    before_filter :restore_search_filters, only: [:batch_update]
    after_filter :save_search_filters, only: [:index]


    def batch_update(ids, state)
      updated_user = []
      users = User.find(ids)
      users.each do |user|
        if user.update state: User::STATES.index(state.to_sym)
          updated_user << "##{user.id} "
        else
          flash[:alert] << "##{user.id} update failed."
        end
      end
      flash[:alert] = "You have successfully changed the state to #{state} for #{updated_user.size} #{'user'.pluralize(updated_user.size)}"
      params[:q] = restore_search_filters
      redirect_to admin_users_path(params[:q])
    end

  def filter_params
      @filter_params ||= params[:admin_users] || {}
    end
end

the filter in the view:

filters.html.erb

<%= semantic_form_for :admin_users, url: admin_users_path, method: :get, html: { class: 'filter_form' } do |f| %>
  <div class="filter_form_field filter_select">
    <%= f.label 'first_name' %>
    <%= f.text_field 'first_name', value: @filter_params[:first_name] %>
  </div>
  <div class="filter_form_field filter_select">
    <%= f.label 'last_name' %>
    <%= f.text_field 'last_name', value: @filter_params[:last_name] %>
  </div>

@fabien-michel
Copy link

Javascript doesn't work for me. (Hacky method not called before original click event handler). I've to change JS code by this :

# Extend the clear-filters button to clear saved filters
$(document).on 'ready page:load', ->
  original_click = $._data($('.clear_filters_btn')[0], "events").click[0].handler
  $('.clear_filters_btn').off('click')
  $('.clear_filters_btn').on 'click', (evt) ->
    # This will send a synchronous post with clear_filters set to true -
    # our AA FilterSaver controller extension looks for this parameter to
    # know when to clear session-stored filters for a resource - and then
    # the default AA clear-filters button behavior will issue a get request
    # to actually re-render the page.
    $.ajax this.href, {
      async: false,
      data: { clear_filters: true },
      type: 'POST'
    }
    original_click.apply(this, [evt])

@tlueker
Copy link

tlueker commented Mar 30, 2017

I had issues where I could not clear the filters once set. The clear filter ajax seemed to work, but when the page reloaded the filters were still there. Clearing them manually and then hitting Filter button also failed, as the result was aparams[:q] = nil.

I ended up modifying restore_search_filters to look at the value of params["commit"] to decided if I wanted to load params[:q].

  def restore_search_filters
    filter_storage = session[SAVED_FILTER_KEY]
    if params[:clear_filters].present? || (params["commit"].present? && params["commit"] == "Filter")
      params.delete :clear_filters
      if filter_storage
        filter_storage.delete controller_key
        params[:q] = nil if params["commit"].present? && params["commit"] != "Filter"
      end
      if request.post?
        # we were requested via an ajax post from our custom JS
        # this render will abort the request, which is ok, since a GET request will immediately follow
        render json: { filters_cleared: true }
      end
    elsif filter_storage && params[:action].to_sym == :index && params[:q].blank? && (params["commit"].blank? || params["commit"] != "Filter")
      saved_filters = filter_storage[controller_key]
      unless saved_filters.blank?
        params[:q] = saved_filters
      end
    end
  end

@sonniimukh
Copy link

I ended up with the original solution and the following JS approach which is more clear and follows the logic of native Clear Filters action:

$ ->
  # Clear Filters button
  $('.clear_filters_btn').off('click')
  $('.clear_filters_btn').click (e) ->
    params = window.location.search.slice(1).split('&')
    regex = /^(q\[|q%5B|q%5b|page|commit)/
    if typeof Turbolinks != 'undefined'
      Turbolinks.visit(window.location.href.split('?')[0] + '?clear_filters=1&' + (param for param in params when not param.match(regex)).join('&'))
      e.preventDefault()
    else
      window.location.search = 'clear_filters=1&' + (param for param in params when not param.match(regex)).join('&')

Had also to apply the Devise workaround proposed by @elsurudo.

Other than that works perfectly fine on top of Rails 5.1.4 and ActiveAdmin 1.1.0.

Thanks all!

@mirelon
Copy link

mirelon commented Dec 1, 2017

Rails 5.0.6:
controller key needs to return string instead of symbol:

      # Get a symbol for keying the current controller in the saved-filter session storage.
      def controller_key
        #params[:controller].gsub(/\//, '_').to_sym
        current_path = request.env['PATH_INFO']
        current_route = Rails.application.routes.recognize_path(current_path)
        current_route.sort.flatten.join('-').gsub(/\//, '_')
      end

@Sprachprofi
Copy link

Thank you all! The following is a complete working implementation for Rails 7 and vanilla JS:

add this to config/initializers/active_admin.rb

ActiveAdmin.setup do |config|
  ...
  config.before_action :restore_search_filters
  config.after_action :save_search_filters
  ...
end

require 'active_admin/filter_saver/controller'

ActiveAdmin.before_load do |app|
  # Add our Extensions
  ActiveAdmin::BaseController.send :include, ActiveAdmin::FilterSaver::Controller
end

new file: /lib/active_admin/filter_saver/controller.rb

# -*- encoding : utf-8 -*-
module ActiveAdmin
  module FilterSaver

    # Extends the ActiveAdmin controller to persist resource index filters between requests.
    #
    # @author David Daniell / тιηуηυмвєяѕ <info@tinynumbers.com>
    # improvements by tlueker and mirelon
    module Controller

      private

      SAVED_FILTER_KEY = :last_search_filter

      def restore_search_filters
        filter_storage = session[SAVED_FILTER_KEY]
        if params[:clear_filters].present? || (params["commit"].present? && params["commit"] == "Filter")
          params.delete :clear_filters
          if filter_storage
            logger.info "clearing filter storage for #{controller_key}"
            filter_storage.delete controller_key
          end
          if request.post?
            # we were requested via an ajax post from our custom JS
            # this render will abort the request, which is ok, since a GET request will immediately follow
            render json: { filters_cleared: true }
          end
        elsif filter_storage && params[:action].to_sym == :index && params[:q].blank? && (params["commit"].blank? || params["commit"] != "Filter")
          saved_filters = filter_storage[controller_key]
          unless saved_filters.blank?
            params[:q] = saved_filters
          end
        end
      end

      def save_search_filters
        if params[:action].to_sym == :index
          session[SAVED_FILTER_KEY] ||= Hash.new
          session[SAVED_FILTER_KEY][controller_key] = params[:q]
        end
      end

      # Get a symbol for keying the current controller in the saved-filter session storage.
      def controller_key
        #params[:controller].gsub(/\//, '_').to_sym
        current_path = request.env['PATH_INFO']
        current_route = Rails.application.routes.recognize_path(current_path)
        current_route.sort.flatten.join('-').gsub(/\//, '_')
      end

    end

  end
end

add this to the bottom of active_admin.js or into an imported javascript file

$(document).ready(function() {
  $('.clear_filters_btn').on('click', function() {
      $.ajax(this.href, {
        async: false,
        data: { clear_filters: true },
        type: 'POST'
      });
    });
});

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