When adding a feature to the backend UI, it's important that you spend some time designing the ideal UX for your store administrators. There are usually different ways to implement the same feature, and the best approach depends on how store admins use the backend.
In this guide, we'll implement a very simple blacklisting system that allows you to mark certain email addresses as blacklisted and require an admin to manually review and approve any orders placed with that email address.
To simplify the implementation, we'll assume the blacklisted email addresses are stored in an environment variable as a comma-separated string. Here are a couple of user stories we'll use as reference for the feature's requirement:
Blacklisted orders are flagged automatically.
Admins can manually approve blacklisted orders.
Without further ado, let's start writing some code!
The first step is to add the
blacklisted column to the
spree_orders table, which we'll use to determine whether an order has been blacklisted. This is quite simple to do with a migration:
$ rails g migration AddBlacklistedToSpreeOrders blacklisted:boolean$ rails db:migrate
This will add the
blacklisted boolean column to
The first step is to flag an order as blacklisted when the email address on the order has been blacklisted. You can do this by creating a class whose job is to analyze an order and determine whether it should be flagged as blacklisted:
lib/awesome_store/order_analyzer.rbmodule AwesomeStoreclass OrderAnalyzerdef analyze(order)order.update!(blacklisted: )endprivatedef blacklisted_emailsENV.fetch('BLACKLISTED_EMAILS', '').split(',')enddef order_blacklisted?(order)order.email.in?(blacklisted_emails)endendend
You will then need to subscribe to the
order_finalized event, which is fired when an order is placed successfully, and call the analyzer:
config/initializers/spree.rb# ...Spree::Event.subscribe 'order_finalized' do |payload|AwesomeStore::OrderAnalyzer.new.analyze(payload[:order])end
Our new business logic is pretty easy to test in integration:
spec/models/spree/order_spec.rbRSpec.describe Spree::Order dodescribe '#finalize!' dobefore doallow(ENV).to receive(:fetch).with('BLACKLISTED_EMAILS', any_args).and_return('firstname.lastname@example.org')endendcontext 'when the email has been blacklisted' doit 'marks the order as blacklisted' doorder = create(:order_ready_to_complete, email: 'email@example.com')order.finalize!expect(order).to be_blacklistedendendcontext 'when the email has not been blacklisted' doit 'does not mark the order as blacklisted' doorder = create(:order_ready_to_complete, email: 'firstname.lastname@example.org')order.finalize!expect(order).not_to be_blacklistedendendendend
At this point, we have a dead-simple order analyzer that determines whether each new order should be blacklisted or not. Now, we need to allow admins to manually review blacklisted orders and decide whether to reject them or remove them from the blacklist.
In order to allow admins to remove an order from the blacklist, we'll add a button to the order detail page that will trigger a new controller action.
The first step is to add our custom action to
Spree::Admin::OrdersController. We'll use a decorator to accomplish that:
app/decorators/awesome_store/spree/admin/oders_controller/add_remove_from_blacklist_action.rbmodule AwesomeStoremodule Spreemodule Adminmodule OrdersControllermodule AddRemoveFromBlacklistActiondef email@example.com!(blacklisted: false)redirect_to edit_admin_order_path(@order)end::Spree::Admin::OrdersController.prepend selfendendendendend
Now that the controller action has been implemented, we can define a route for it:
config/routes.rb# ...Spree::Core::Engine.routes.draw donamespace :admin doresources :orders, only:  domember doput :remove_from_blacklistendendendend
In the next section, we'll see how to hook our custom controller action to a new button in the backend.
We are going to add a "Remove from blacklist" to the order toolbar:
We are going to use the popular Deface gem to apply a patch to the default view. In case you're not familiar with it, Deface is a gem that allows you to "virtually" patch third-party views, meaning you can edit them without having to completely replace them in your application.
First of all, we need to install Deface by adding it to our
Gemfile# ...gem 'deface'
Once done, we need to identify which view we want to customize. By browsing through the backend's codebase, we can see the view in question is
spree/backend/orders/edit.html.erb. If we inspect the view's source code, we can also see that we want our button to be included in the content for the
:page_actions element, so that it's added to the toolbar actions when editing an order.
Equipped with this information, we can now write our Deface override:
app/overrides/spree/backend/orders/edit/add_remove_from_blacklist.html.erb.deface<!-- insert_after "erb[silent]:contains('content_for :page_actions')" --><li><% if @order.blacklisted? %><%= button_to(t('spree.remove_from_blacklist'),remove_from_blacklist_admin_order_url(@order),method: :post,class: 'btn btn-primary',) %><% end %></li>
The last thing we need to do for our button to appear properly is add the
spree.remove_from_blacklist translation key to our application. We just need to add the key to the
config/locales/en.yml file in our application, like this:
config/locales/en.ymlen:spree:remove_from_blacklist: Remove from blacklist
It's finally time to write a full-fledged feature test to make sure the new button appears and our new functionality works correctly in integration:
spec/features/admin/orders/blacklisting_spec.rbRSpec.describe 'Order blacklisting', :js dostub_authorization!it 'can be removed from the blacklist' doorder = create(:completed_order, blacklisted: true)visit spree.edit_admin_order_path(order)click_button t('spree.remove_from_blacklist')order.reloadexpect(order).not_to be_blacklistedendend
If we did everything well, our test should pass with flying colors!
Congratulations! You have implemented your first custom feature for the Solidus backend.
Of course, we have just scratched the surface of what's possible: the backend provides a lot of UI components and capabilities you may leverage. We suggest spending some time in the backend's codebase to get accustomed with all the different tools at your disposal, and doing some planning/research before every custom feature.
By using a combination of custom controller actions, view overrides and automated tests, you'll be able to write custom admin features that are fully integrated with the Solidus experience, and yet are a joy to maintain and evolve over time.