Solidus
Search…
Customizing the backend

Designing your feature

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 rejection system that allows you to mark certain email addresses as rejected and require an admin to manually review and approve any orders placed with that email address.
To simplify the implementation, we'll assume the rejected 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:
    rejected orders are flagged automatically;
    admins can manually approve rejected orders.
Without further ado, let's start writing some code!

Adding new columns

The first step is to add the rejected column to the spree_orders table, which we'll use to determine whether an order has been rejected. This is quite simple to do with a migration:
1
$ rails g migration AddRejectedToSpreeOrders rejected:boolean
2
$ rails db:migrate
Copied!
This will add the rejected boolean column to spree_orders.

Hooking into order events

The first step is to flag an order as rejected when the email address on the order has been rejected. You can do this by creating a class whose job is to analyze an order and determine whether it should be flagged as rejected:
app/models/amazing_store/order_analyzer.rb
1
module AmazingStore
2
class OrderAnalyzer
3
def analyze(order)
4
order.update!(rejected: order_rejected?(order))
5
end
6
7
private
8
9
def rejected_emails
10
ENV.fetch('REJECTED_EMAILS', '').split(',')
11
end
12
13
def order_rejected?(order)
14
order.email.in?(rejected_emails)
15
end
16
end
17
end
Copied!
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
1
# ...
2
3
Spree::Event.subscribe 'order_finalized' do |event|
4
AmazingStore::OrderAnalyzer.new.analyze(event.payload[:order])
5
end
Copied!
At this point, we have a dead-simple order analyzer that determines whether each new order should be rejected or not. Now, we need to allow admins to manually review rejected orders and decide whether to reject them or remove them from the rejected.

Implementing new actions

In order to allow admins to remove an order from the rejected, 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 an override to accomplish that:
app/overrides/amazing_store/spree/admin/orders_controller/add_remove_from_rejected_action.rb
1
module AmazingStore
2
module Spree
3
module Admin
4
module OrdersController
5
module AddRemoveFromRejectedAction
6
def remove_from_rejected
7
load_order
8
9
@order.update!(rejected: false)
10
11
redirect_to edit_admin_order_path(@order)
12
end
13
14
::Spree::Admin::OrdersController.prepend self
15
end
16
end
17
end
18
end
19
end
Copied!
Now that the controller action has been implemented, we can define a route for it:
config/routes.rb
1
# ...
2
3
Spree::Core::Engine.routes.draw do
4
namespace :admin do
5
resources :orders, only: [] do
6
member do
7
put :remove_from_rejected
8
end
9
end
10
end
11
end
Copied!
In the next section, we'll see how to hook our custom controller action to a new button in the backend.

Defacing admin views

We are going to add a "Remove from rejected" 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.
Just like for the storefront, you can still copy-paste the backend views into your application if you want to override them. However, this approach is quite hard to maintain, since it would prevent you to get any Solidus upgrades to the backend for views that have been overridden. It becomes even more complex when you consider all the different overrides applied to the backend by Solidus extensions. Deface is a declarative, maintainable way of patching backend views while still benefitting from Solidus upgrades.
First of all, we need to install Deface by adding it to our Gemfile:
Gemfile
1
# ...
2
gem 'deface'
Copied!
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_rejected.html.erb.deface
1
<!-- insert_before "erb[silent]:contains('if can?(:fire, @order)')" -->
2
<li>
3
<% if @order.rejected? %>
4
<%= button_to(
5
t('spree.remove_from_rejected'),
6
remove_from_rejected_admin_order_url(@order),
7
method: :put,
8
class: 'btn btn-primary',
9
) %>
10
<% end %>
11
</li>
Copied!
The path of overrides is extremely important: the directory where you put the override must match the path of the view you want to customize (minus the extension), and the override must have the .html.erb.deface extension for Deface to apply it correctly. If an override is not getting applied, the first thing you look at should be the path.
Deface provides a lot of selectors, actions and tools for debugging your overrides — taking the time to understand how to use them correctly will help you a lot when overriding different parts of the backend. You can look at the Deface documentation and even at Solidus extensions if you need some inspiration with a tricky override.
The last thing we need to do for our button to appear properly is add the spree.remove_from_rejected 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.yml
1
en:
2
spree:
3
remove_from_rejected: Remove from rejected
Copied!
While it's also possible to hardcode the string in your views/controllers, using Rails' native internationalization features will allow you to write code that is easier to maintain and will make it easier to go global, should you ever need it.
You can override default Spree translations in the exact same way, if you want to change the default labels or messages in the backend.

Taking it from here

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.
Last modified 3mo ago