Plugin for ActiveAdmin. Provides batch Update and Delete for scoped_collection (Filters + Scope) across all pages.
- Description
- Installation
- Usage
- Custom actions
- Details and settings
- Why don't I see the sidebar with collection actions?
- Can I use my own handler for the update and delete actions?
- How can I rename a button?
- How can I change the modal dialog title?
- How can I tell the user how many records are affected?
- How can I ask for an extra confirmation before the form is submitted?
- Can I replace the pop-up form with my own?
- How do I notify the user about success and errors?
- Can I perform an action only on selected items?
- License
This gem gives you the ability to perform various batch actions on any filtered (or scoped) resource. An action applies to all records across all pages. It is similar to an ActiveAdmin batch action, but affects all filtered records. This is useful when you want to delete or update a lot of records in one click.
Add this line to your application's Gemfile:
# last version
gem 'active_admin_scoped_collection_actions'
# master branch
gem 'active_admin_scoped_collection_actions', github: 'activeadmin-plugins/active_admin_scoped_collection_actions'And then execute:
bundleAdd the following line at the end of app/assets/javascript/active_admin.js:
//= require active_admin_scoped_collection_actionsAlso include the CSS in app/assets/stylesheets/active_admin.css.scss:
@import "active_admin_scoped_collection_actions";Usually you need two standard actions: Delete and Update.
For example, if you have resource Posts and you want to have a delete action, add:
scoped_collection_action :scoped_collection_destroyExample:
ActiveAdmin.register Post do
config.batch_actions = true
scoped_collection_action :scoped_collection_destroy
index do
# ...
end
endImportant Visit the Posts page with your browser and you will see no changes. Now, perform any filter with the Filters sidebar. Only after you filter will you see a delete button. It will be in the sidebar under Filters.
Update is the second standard action and is more complex. It has a form hash wrapped in a Proc:
scoped_collection_action :scoped_collection_update, form: -> do
{ name: 'text',
diagonal: 'text',
manufactured_at: 'datepicker',
vendor_id: Vendor.all.map { |region| [region.name, region.id] },
has_3g: [['Yes', 't'], ['No', 'f']] }
endIn this example the Phone model has fields:
name— varchar stringdiagonal— integer (or float)manufactured_at— datetimevendor_id— associationbelongs_to :vendor, class_name: 'Vendor', foreign_key: :vendor_idhas_3g— boolean
The form parameter is a Proc which returns a Hash. It defines which fields you want to be able to update. Hash
keys are column names in the database, hash values are types of HTML inputs. Supported values are text,
checkbox, datepicker, and an Array of options for a selectbox. If you want something more complex, you can
build your own forms.
A field value can also be a Hash, {type: 'text', class: 'my-widget'}. Such a field is rendered as a plain input
with your own class on it, so you can turn it into any widget (datetime picker, autocomplete, etc.) with your own
JavaScript. The dialog triggers the mass_update_modal_dialog:after_open event on body with the form as an
argument — this is the place to initialize your widgets:
$(document).on('mass_update_modal_dialog:after_open', function (event, form) {
$(form).find('input.my-widget').myWidget();
});Example: we have a Phone resource with a manufactured_at column. We need an action which will erase this date.
In the ActiveAdmin resource:
ActiveAdmin.register Phone do
config.batch_actions = true
scoped_collection_action :erase_date do
scoped_collection_records.update_all(manufactured_at: nil)
end
index do
# ...
end
endThis simple code will create a new "Erase date" button in the sidebar. After clicking it, the user will see the confirmation message "Are you sure?". After confirming, all filtered records will be updated.
Sidebar visibility by default depends on several things.
First you must set:
config.batch_actions = trueInternally this gem uses batch_actions, so without them collection actions will not work.
scoped_collection_action :something_hereYour resource should have some collection actions. If it does not have any, the sidebar will not appear.
By default we do not allow performing actions on all the records — this protects you from accidental deletion. The sidebar with buttons appears only after you filter or scope the resource records.
And lastly, you can manage sidebar visibility through the resource config:
# Always
config.scoped_collection_actions_if = -> { true }
# Only for scopes
config.scoped_collection_actions_if = -> { params[:scope] }
# etc.You can also manage the visibility of each action individually:
scoped_collection_action :scoped_collection_destroy, if: proc { can? :destroy, Blog }You can pass a block to the default update and delete actions, and do a custom redirect after it. Use head
with a location: instead of redirect_to.
This example renders a form which allows changing the name field, and after that redirects to the dashboard
page:
scoped_collection_action :scoped_collection_update,
form: -> {
{ name: 'text' }
} do
scoped_collection_records.update_all(name: params[:changes][:name])
flash[:notice] = 'Name successfully changed.'
head :no_content, location: admin_dashboard_path
endEvery scoped_collection_action has the :title option.
Example:
scoped_collection_action :erase_date, title: 'Nullify' do
scoped_collection_records.update_all(manufactured_at: nil)
endSimilar to the button title — use the :confirm option:
scoped_collection_action :scoped_collection_destroy, confirm: 'Delete all phones?'The :confirm_summary option adds a sentence with the number of affected records to the dialog:
scoped_collection_action :scoped_collection_destroy, confirm_summary: trueDelete all?
You are going to delete 100500 record(s).
[ OK ] [ Cancel ]
true uses the default text — the confirm_destroy_summary locale key for :scoped_collection_destroy and
confirm_action_summary for any other action. Pass a String or a Proc to use your own text. {count} in the
text is replaced with the number.
The amount is requested from the server with the same filters, scope and checked records the action itself will use, so it costs one extra COUNT query. Until the answer arrives the message is not displayed at all — there is a spinner in its place and OK is disabled, so nothing can be confirmed before the user sees what they confirm.
When that request happens depends on which option is set:
- neither
:confirm_summarynor:confirm_submit— nothing is counted and no request is made; :confirm_summary— once, when the dialog is opened;:confirm_submit— once, when OK is pressed and the second step appears, and not at all when the user checked no fields, because then there is nothing to confirm and the action runs straight away.
Setting both does not make two requests. :confirm_submit takes over, and :confirm_summary only supplies
the wording for the sentence on its second step.
Both options in one flow — an update action with confirm_submit: and the summary it shows, from the sidebar
button to the result:
An action without a form is confirmed by the modal dialog itself — it has no fields and the user just presses OK.
An action with a form applies changes immediately after OK is pressed. Use the :confirm_submit option to turn
such an action into two steps, where the second one replaces the fields with a summary of what is going to
happen:
scoped_collection_action :scoped_collection_update,
confirm_submit: true,
form: -> { { body: 'text' } }Are you sure?
You are going to update 100500 record(s) with:
Body: Text here...
Author_id: Jane
[ OK ] [ Back ]
"Back" returns to the form with everything the user filled in still there. Only the fields the user checked are
listed, and when nothing is checked there is nothing to confirm — the action is performed right away. The text of
the sentence is the confirm_submit_summary locale key, or the :confirm_summary option when you set it.
true shows the default message ("Are you sure?"). Pass a String or a Proc to use your own:
scoped_collection_action :scoped_collection_update,
confirm_submit: 'Update all filtered phones?',
form: -> { { name: 'text' } }Yes, but you must also take care of the mandatory parameters passed to the server.
scoped_collection_action :my_pop_action, class: 'my_popup'Now in the HTML page you have a button:
<button class="my_popup" data="{"auth_token":"2a+KLu5u9McQENspCiep0DGZI6D09fCVXAN9inrwRG0=","batch_action":"my_pop_action","confirm":"Are you sure?"}">My pop action</button>Without a handler, clicking the button does nothing.
You can render the form in any way you want:
- it can be a popup (Fancybox, Simplemodal, etc.), or an inline collapsible form;
- it can even be a separate full page.
One thing is important — how you send the data to the server. It should be:
| Method | POST |
| URL | /admin/collection_path/batch_action |
| Query string | identical to the current page |
The easiest way to build it is:
url = window.location.pathname + '/batch_action' + window.location.searchAnd the request body params should look like:
changes[manufactured_at] = "2015-07-21 18:11"
changes[diagonal] = "7"
changes[some_filed_name]='new value'
authenticity_token = "2a+KLu5u9McQENspCiep0DGZI6D09fCVXAN9inrwRG0="
batch_action = "my_pop_action"
authenticity_token and batch_action can be taken from the data attribute of the button.
Example in JavaScript:
url = window.location.pathname + '/batch_action' + window.location.search
form_data = {
changes: { "manufactured_at": "2015-07-21 18:11", "diagonal": "7" },
collection_selection: [],
authenticity_token: "2a+KLu5u9McQENspCiep0DGZI6D09fCVXAN9inrwRG0=",
batch_action: "my_pop_action"
}
$.post(url, form_data).always(function () {
window.location.reload();
});We recommend using Rails flash messages.
Example with updating the phone diagonal attribute. In this case the Phone model has a validation:
class Phone < ActiveRecord::Base
validates :diagonal, numericality: { only_integer: true }
endscoped_collection_action :change_diagonal, form: { diagonal: 'text' } do
errors = []
scoped_collection_records.find_each do |record|
errors << record.errors.full_messages.join('. ') unless record.update(diagonal: params[:changes][:diagonal])
end
if errors.empty?
flash[:notice] = 'Diagonal changed successfully'
else
flash[:error] = errors.join('. ')
end
head :no_content
endWhen you try to update the diagonal with "5.6" you will see a flash error:
Diagonal must be an integer.
But if you use your own custom popup, you can show messages with JS.
The standard index page of a resource with batch_action enabled has a selectable column.
If you have checked some items and perform any collection action, the handler will take care of it. If you write custom actions, you should do it like this:
scoped_collection_action :do_something do
scoped_collection_records.find_each do |record|
record.update(name: 'x')
end
endReleased under the MIT License.


