docs/developer/tutorial/admin.mdx
Now that we've created the Brand model, let's create an Admin Dashboard interface so admins can manage brands — including editing the rich text description and uploading the logo.
The Admin Scaffold generator creates a complete admin section for a resource:
<CodeGroup>spree generate spree:admin:scaffold Spree::Brand
bin/rails g spree:admin:scaffold Spree::Brand
This will create the following files:
| File Type | Path | Description |
|---|---|---|
| Controller | app/controllers/spree/admin/brands_controller.rb | Handles the logic for the brands resource. |
| View | app/views/spree/admin/brands/index.html.erb | Displays the list of brands using the tables system. |
| View | app/views/spree/admin/brands/new.html.erb | Displays the new brand form. |
| View | app/views/spree/admin/brands/edit.html.erb | Displays the edit brand form. |
| Partial | app/views/spree/admin/brands/_form.html.erb | The form partial used in new and edit views. |
| Initializer | config/initializers/spree_admin_brands_table.rb | Registers the table with columns for the index view. |
| Initializer | config/initializers/spree_admin_brands_navigation.rb | Adds navigation to the admin sidebar. |
It will also automatically add the following routes to your config/routes.rb file:
namespace :admin do
resources :brands
end
You will now be able to access the brands resource at http://localhost:3000/admin/brands in your browser. To reference this route in your code, you can use the spree.admin_brands_path helper.
The generated form only knows about the name column. Let's add the rich text editor for description and the upload field for logo using Spree's admin form builder:
<div class="card mb-6">
<div class="card-body">
<%= f.spree_text_field :name, required: true, autofocus: true %>
<%= f.spree_rich_text_area :description %>
</div>
</div>
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title"><%= Spree.t(:logo) %></h5>
</div>
<div class="card-body">
<%= f.spree_file_field :logo, width: 300, height: 300 %>
</div>
</div>
spree_rich_text_area renders a WYSIWYG editor (Trix) for the Action Text description.spree_file_field handles drag-and-drop upload, image preview, and direct upload to storage. Add crop: true to enable image cropping with a recommended-size indicator.Then permit the two new attributes in the controller:
module Spree
module Admin
class BrandsController < ResourceController
private
def permitted_resource_params
params.require(:brand).permit(
:name,
:description,
:logo
)
end
end
end
end
Open http://localhost:3000/admin/brands/new, create a brand with a formatted description and a logo — everything saves through the standard form.
The scaffold generator creates a table initializer at config/initializers/spree_admin_brands_table.rb with default columns:
Rails.application.config.after_initialize do
Spree.admin.tables.register(:brands, model_class: Spree::Brand, search_param: :name_cont)
Spree.admin.tables.brands.add :name,
label: :name,
type: :link,
sortable: true,
filterable: true,
default: true,
position: 10
Spree.admin.tables.brands.add :created_at,
label: :created_at,
type: :datetime,
sortable: true,
filterable: true,
default: true,
position: 20
Spree.admin.tables.brands.add :updated_at,
label: :updated_at,
type: :datetime,
sortable: true,
filterable: true,
default: false,
position: 30
end
To render the logo thumbnail in the name column — the same pattern the Products table uses — switch the column to a custom partial:
Spree.admin.tables.brands.add :name,
label: :name,
type: :custom,
partial: 'spree/admin/tables/columns/brand_name',
sortable: true,
filterable: true,
default: true,
position: 10
And create the partial:
<%# locals: (record:, column:, value:) %>
<%= link_to spree.edit_admin_brand_path(record), class: 'flex items-center gap-3 no-underline', data: { turbo_frame: '_top' } do %>
<% if record.logo.attached? %>
<%= spree_image_tag record.logo, width: 48, height: 48, class: 'rounded' %>
<% end %>
<span class="text-gray-900 font-medium"><%= record.name %></span>
<% end %>
The scaffold generator also creates a navigation initializer at config/initializers/spree_admin_brands_navigation.rb:
Rails.application.config.after_initialize do
Spree.admin.navigation.sidebar.add :brands,
label: :brands,
url: :admin_brands_path,
icon: 'list',
position: 55,
active: -> { controller_name == 'brands' },
if: -> { can?(:manage, Spree::Brand) }
end
You can customize the icon and position to fit your needs. For example, to use the "award" icon and place it between Products (30) and Customers (40):
Spree.admin.navigation.sidebar.add :brands,
label: :brands,
url: :admin_brands_path,
icon: 'award',
position: 35,
active: -> { controller_name == 'brands' },
if: -> { can?(:manage, Spree::Brand) }
To add "Brands" to the Products submenu instead, use the parent option:
Rails.application.config.after_initialize do
Spree.admin.navigation.sidebar.add :brands,
label: :brands,
url: :admin_brands_path,
position: 50,
parent: :products,
active: -> { controller_name == 'brands' },
if: -> { can?(:manage, Spree::Brand) }
end
After restarting your server, you'll see the new "Brands" navigation link in the admin sidebar!
<Info> For complete navigation API documentation including all available options, submenu creation, badges, and more, see the [Admin Navigation](/developer/admin/navigation) guide. The form helpers used in Step 2 are documented in [Form Builder](/developer/admin/form-builder) and [Components](/developer/admin/components). </Info>Brands are manageable in the admin. Now let's connect them to Products: Extending Core Models.