« HE:labs
HE:labs

Quick add and quick edit on Active Admin

Postado por Tomás Augusto Müller em 28/05/2014

In this post we'll learn how to implement a "quick add" and "quick edit" feature on active admin that you can use to give users more agility on administrative operations.

Bad UI

Not quite a web interface, but you get the point.

Application Setup

Let's skip some steps here by using the Pah gem to setup and configure a fresh Rails app for us. Pah is the base Rails application used here at HE:labs. With a configured and running Rails app the next step is install active admin, and set up a simple user authentication system, so users can log in into our administrative area. If you are starting from point zero, you can check the user authentication mechanism using Facebook Login implemented on Strawberrycake project, and later on add Facebook authentication on active admin.

Or, if you already have all of the above, just skip these steps and lets get our hands dirty!

Our sample model

Imagine that you have to build a administrative panel to manage the projects of your company. You have a Project model that have a name, description, client, start date, end date, participants, tags, budget, documents, images, links, etc. The list of attributes and associations can easily continue.

Considering the objectives of this post, let's suppose that inserting a new project and updating the budget are the most common operations. It would be very handy if we could quick insert new projects only by supplying the project name and client, without leaving the list of projects and loosing any filter previously applied to the projects index. Also, having link to update the project budget, with the same behavior, could let our users even happier.

Preparing active admin

To accomplish both of our objectives we are going to use modal dialogs. Here specifically, we will use Fancybox. After installing Fancybox into your application (you can use this gem), change the active admin assets to look like this:

app/assets/javascripts/active_admin.js.coffee:

1 #= require active_admin/base
2 #= require fancybox
3 jQuery ->
4   $('a.fancybox').fancybox()

app/assets/stylesheets/active_admin.css.scss:

1 @import "active_admin/mixins";
2 @import "active_admin/base";
3 @import "fancybox";

Finally, considering our sample model Project, create the projects admin panel:

1 rails generate active_admin:resource Project

The quick add

Add the following routes to your application:

1 get '/admin/projects/new/quick_add' => 'admin/projects#quick_add', as: :admin_project_quick_add
2 post '/admin/projects/quick_create' => 'admin/projects#quick_create', as: :admin_project_quick_create

Edit the app/admin/project.rb and add the following action item to let users open the quick add modal only from project index:

1 action_item only: :index do
2   link_to 'Quick add', admin_project_quick_add_path, class: 'fancybox', data: { 'fancybox-type' => 'ajax' }
3 end

Still on app/admin/project.rb, implement the quick_add and quick_create controller actions, and don't forget to setup the permitted params.

 1 controller do
 2   def quick_add
 3     @project = Project.new
 4     render layout: false
 5   end
 6 
 7   def quick_create
 8     @project = Project.new(permitted_params[:project])
 9     @project.save
10     render 'quick_response', layout: false
11   end
12 end

Finally, the views:

app/views/admin/projects/quick_add.html.slim:

 1 #modal
 2   h2 New Project
 3   p.modal-description Project Quick Add.
 4 
 5   section#quick-errors
 6     = render 'error_messages', object: @project
 7 
 8   section.form-fluid
 9     = simple_form_for(@project, url: admin_project_quick_create_path, remote: true) do |f|
10       .form-inputs
11         = f.input :name
12         = f.input :client
13       ul.form-actions
14         li
15           = f.submit 'Save'

app/views/admin/projects/quick_response.js.erb:

1 <% if @project.errors.any? %>
2   var html = "<%= escape_javascript(render 'error_messages', object: @project) %>";
3   $('#quick-add-errors').html(html);
4 <% else %>
5   window.location.reload();
6 <% end %>

This same file will be used as response for the quick edit feature that follows. Also, note the usage of an error_messages partial. Here I'm using the one included by Pah.

The quick edit

Add the following routes to your application:

1 get '/admin/projects/:id/edit/quick_edit' => 'admin/projects#quick_edit', as: :admin_project_quick_edit
2 patch '/admin/projects/:id/quick_update' => 'admin/projects#quick_update', as: :admin_project_quick_update

Edit the app/admin/project.rb and override the index page. The trick here is how we append our new Quick Edit link to the default ones - View, Edit, Delete:

 1 index do
 2   column :id
 3   column :name
 4   column :client
 5   column :budget
 6   # more columns definitions...
 7 
 8   actions defaults: true do |project|
 9     link_to 'Quick Edit', admin_project_quick_edit_path(project), class: 'fancybox', data: { 'fancybox-type' => 'ajax' }
10   end
11 end

Still on app/admin/project.rb, implement the quick_edit and quick_update controller actions.

 1 controller do
 2   # def quick_add...
 3   # def quick_create...
 4 
 5   def quick_edit
 6     @project = Project.find(params[:id])
 7     render layout: false
 8   end
 9 
10   def quick_update
11     @project = Project.find(params[:id])
12     @project.update(permitted_params[:project])
13     render 'quick_response', layout: false
14   end
15 end

Finally, the view:

app/views/admin/projects/quick_edit.html.slim:

 1 #modal
 2   h2 Quick Edit Project
 3   p.modal-description Edit Project Budget.
 4 
 5   section#quick-errors
 6     = render 'error_messages', object: @project
 7 
 8   section.form-fluid
 9     = simple_form_for(@project, url: admin_project_quick_update_path, remote: true) do |f|
10       .form-inputs
11         = f.input :budget
12       ul.form-actions
13         li
14           = f.submit 'Save'

Tests

Now let's write the tests for our custom quick add and quick edit features.

Yes, we test routes. So, here it is:

spec/routing/admin_projects_routing_spec.rb:

 1 require 'spec_helper'
 2 
 3 describe Admin::ProjectsController do
 4 
 5   describe 'quick add' do
 6     describe 'routes' do
 7       it { expect(get('/admin/projects/new/quick_add')).to route_to('admin/projects#quick_add') }
 8       it { expect(post('/admin/projects/quick_create')).to route_to('admin/projects#quick_create') }
 9     end
10 
11     describe 'route helpers' do
12       it { expect(admin_project_quick_add_path).to eq('/admin/projects/new/quick_add') }
13       it { expect(admin_project_quick_create_path).to eq('/admin/projects/quick_create') }
14     end
15   end
16 
17   describe 'quick edit' do
18     describe 'routes' do
19       it { expect(get('/admin/projects/1/edit/quick_edit')).to route_to(controller: 'admin/projects', action: 'quick_edit', id: '1') }
20       it { expect(patch('/admin/projects/1/quick_update')).to route_to(controller: 'admin/projects', action: 'quick_update', id: '1') }
21     end
22 
23     describe 'route helpers' do
24       it { expect(admin_project_quick_edit_path(1)).to eq('/admin/projects/1/edit/quick_edit') }
25       it { expect(admin_project_quick_update_path(1)).to eq('/admin/projects/1/quick_update') }
26     end
27   end
28 
29 end

And now the tests for the custom action methods added to projects (active)admin controller. To implement them, here we are going to use a shared example for authentication required, and a sign_in method defined in one of our spec support files.

spec/controllers/admin/projects_controller_spec.rb:

  1 require 'spec_helper'
  2 
  3 describe Admin::ProjectsController do
  4 
  5   let!(:user) { create(:user, admin: true) }
  6 
  7   describe "GET 'quick_add'" do
  8     include_examples 'authentication required' do
  9       let(:action) { get :quick_add }
 10     end
 11 
 12     context 'logged in' do
 13       before do
 14         sign_in(user)
 15         get :quick_add
 16       end
 17 
 18       it { expect(assigns(:project)).to be_a Project }
 19       it { should respond_with(:success) }
 20       it { should render_template(:quick_add) }
 21       it { should render_template(layout: false) }
 22     end
 23   end
 24 
 25   describe "POST 'quick_create'" do
 26     include_examples 'authentication required' do
 27       let(:action) { xhr :post, :quick_create }
 28     end
 29 
 30     context 'logged in' do
 31       before do
 32         sign_in(user)
 33       end
 34 
 35       context 'with invalid params' do
 36         let(:params) do
 37           { project: { name: nil, client: nil } }
 38         end
 39 
 40         it 'not create a new project' do
 41           expect do
 42             xhr :post, :quick_create, params
 43           end.to_not change(Project, :count)
 44         end
 45 
 46         it 'assign @project' do
 47           xhr :post, :quick_create, params
 48           expect(assigns(:project)).to be_a(Project)
 49         end
 50 
 51         it "render the 'quick_response'" do
 52           xhr :post, :quick_create, params
 53           should render_template(:quick_response)
 54           should render_template(layout: false)
 55         end
 56       end
 57 
 58       context 'with valid params' do
 59         let(:params) do
 60           { project: { name: 'XPTO Project', client: 'Foo Bar Client Name' } }
 61         end
 62 
 63         it 'create a new project' do
 64           expect do
 65             xhr :post, :quick_create, params
 66           end.to change(Project, :count).by(1)
 67         end
 68 
 69         it 'assign the new @project' do
 70           xhr :post, :quick_create, params
 71           expect(assigns(:project)).to be_a(Project)
 72         end
 73 
 74         it '@project is persisted' do
 75           xhr :post, :quick_create, params
 76           expect(assigns(:project)).to be_persisted
 77         end
 78 
 79         it "render the 'quick_response'" do
 80           xhr :post, :quick_create, params
 81           should render_template(:quick_response)
 82           should render_template(layout: false)
 83         end
 84       end
 85     end
 86   end
 87 
 88   describe "GET 'quick_edit'" do
 89     let!(:project) { create(:project) }
 90 
 91     include_examples 'authentication required' do
 92       let(:action) { get :quick_edit, id: project }
 93     end
 94 
 95     context 'logged in' do
 96       before do
 97         sign_in(user)
 98         get :quick_edit, id: project
 99       end
100 
101       it { expect(assigns(:project)).to eq project }
102       it { should respond_with(:success) }
103       it { should render_template(:quick_edit) }
104       it { should render_template(layout: false) }
105     end
106   end
107 
108   describe "POST 'quick_update'" do
109     let!(:project) { create(:project, budget: 77000) }
110 
111     include_examples 'authentication required' do
112       let(:action) { xhr :post, :quick_update, id: project }
113     end
114 
115     context 'logged in' do
116       before do
117         sign_in(user)
118       end
119 
120       context 'with invalid params' do
121         let(:params) do
122           { id: project, project: { budget: 'abc' } }
123         end
124 
125         it 'not create a new project' do
126           expect do
127             xhr :post, :quick_update, params
128           end.to change { project.reload.budget }.to be_zero
129         end
130 
131         it 'assign the edited @project' do
132           xhr :post, :quick_update, params
133           expect(assigns(:project)).to eq project
134         end
135 
136         it "render the 'quick_response'" do
137           xhr :post, :quick_update, params
138           should render_template(:quick_response)
139           should render_template(layout: false)
140         end
141       end
142 
143       context 'with valid params' do
144         let(:params) do
145           { id: project, project: { budget: '88000' } }
146         end
147 
148         it 'change project budget' do
149           expect do
150             xhr :post, :quick_update, params
151           end.to change{ project.reload.budget }.from(77000).to(88000)
152         end
153 
154         it 'assign the updated @project' do
155           xhr :post, :quick_update, params
156           expect(assigns(:project)).to eq project
157         end
158 
159         it "render the 'quick_response'" do
160           xhr :post, :quick_update, params
161           should render_template(:quick_response)
162           should render_template(layout: false)
163         end
164       end
165     end
166   end
167 
168 end

Conclusion

Administrative panels always will be among us. They are very useful for clients to have complete control over a system feature. But sometimes they can turn into something very boring, scary and hard to use, due the clutter of having too much data, controls and information, all into one single interface.

Active admin can help us a lot to easily ship full featured administrative interfaces. Beyond all features that the gem offer out of the box, we can build custom features like the ones shown on this post to facilitate the life of our customers.

Tags: active admin

Compartilhe

Sabia que nosso blog agora está no Medium? Confira Aqui!