solidus Documentation

Solidus, the open-source eCommerce framework for industry trailblazers.

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy solidus on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name solidus --restart always solidus_storefront-0.1.0
Configured Services & Ports
โš™๏ธ mysql
image: mysql:8.0
โš™๏ธ postgres
image: postgres:13.2
โš™๏ธ app
image: solidus_storefront-0.1.0
๐Ÿ”Œ Ports: ${SANDBOX_PORT:-3000}:${SANDBOX_PORT:-3000}
docker-compose.yml
yaml (docker-compose.yml)
version: '3.7'

services:
  mysql:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - mysql:/var/lib/mysql:cached

  postgres:
    image: postgres:13.2
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_DB: solidus_storefront_solidus_test
    volumes:
      - postgres:/var/lib/postgresql/data:cached

  app:
    platform: linux/amd64
    build:
      context: .dockerdev
      dockerfile: Dockerfile
      args:
        RUBY_VERSION: "2.7.2"
        PG_VERSION: 13
        NODE_VERSION: 14
        BUNDLER_VERSION: 1.17.3
    image: solidus_storefront-0.1.0
    command: bash -c "(bundle check || bundle) && tail -f /dev/null"
    environment:
      DB_USERNAME: root
      DB_PASSWORD: password
      DB_ALL: "1"
      DB_MYSQL_HOST: mysql
      DB_POSTGRES_HOST: postgres
      HISTFILE: "/home/solidus_storefront_user/history/bash_history"
      MYSQL_HISTFILE: "/home/solidus_storefront_user/history/mysql_history"
    ports:
      - "${SANDBOX_PORT:-3000}:${SANDBOX_PORT:-3000}"
    volumes:
      - .:/home/solidus_storefront_user/app:delegated
      - bundle:/home/solidus_storefront_user/gems:cached
      - history:/home/solidus_storefront_user/history:cached
      - .dockerdev/.psqlrc:/home/solidus_storefront_user/.psqlrc:cached
    tty: true
    stdin_open: true
    tmpfs:
      - /tmp
    depends_on:
      - mysql
      - postgres

volumes:
  bundle:
  history:
  postgres:
  mysql:

Technical Documentation: solidusio/solidus

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

Solidus









A free, open-source e-commerce platform that gives you complete control over your store.

Table of Contents

Supporting Solidus

As a community-driven project, Solidus relies on funds and time donated by developers and stakeholders who use Solidus for their businesses. If you'd like to help Solidus keep growing, please consider:

Main Contributor & Director

At present, Nebulab is the main code contributor and director of Solidus, providing technical guidance and coordinating community efforts and activities.

Ambassadors

Support this project by becoming a Solidus Ambassador. Your logo will show up here with a link to your website. Become an Ambassador.


Summary

Solidus is a complete open source e-commerce solution built with Ruby on Rails.
It is a fork of Spree.

See the Solidus class documentation and the Solidus
Guides
for information about the functionality that
Solidus provides.

Solidus consists of several gems. When you require the solidus gem in your
Gemfile, Bundler will install all of the following gems:

All of the gems are designed to work together to provide a fully functional
e-commerce platform. However, you may only want to use the
solidus_core gem
combine it with your own custom frontend, admin interface, and API.

Demo

You can try the live Solidus demo here. The admin section can be accessed here.

Getting started

Begin by making sure you have
Imagemagick installed, which is
required for Paperclip. (You can install it using Homebrew if
you're on a Mac.)

To install Solidus with the current storefront, follow the instructions in
storefront/README.md.

Accessing Solidus Store

Start the Rails server with the command:

bash
bin/rails s

The storefront will be accessible at
http://localhost:3000/ and the admin can be found at
http://localhost:3000/admin/.

For information on how to customize your store, check out the customization guides.

Default Username/Password

As part of running the above installation steps, you will be asked to set an admin email/password combination. The default values are [email protected] and test123, respectively.

Questions?

The best way to ask questions is to join the Solidus Slack and join the #support channel.

Installation options

Instead of a stable build, if you want to use the bleeding edge version of
Solidus, use this line:

ruby
gem 'solidus', github: 'solidusio/solidus'

Note: The master branch is not guaranteed to ever be in a fully functioning
state. It is too risky to use this branch in production.

By default, the installation generator (solidus:install) will run
migrations as well as adding seed and sample data. This can be disabled using

bash
bin/rails g solidus:install --migrate=false --sample=false --seed=false

You can always perform any of these steps later by using these commands.

bash
bin/rails railties:install:migrations
bin/rails db:migrate
bin/rails db:seed
bin/rails spree_sample:load

There are also options and rake tasks provided by
solidus_auth_devise.

Performance

You may notice that your Solidus store runs slowly in development mode. This
can be because in development each CSS and JavaScript is loaded as a separate
include. This can be disabled by adding the following to
config/environments/development.rb.

ruby
config.assets.debug = false

To gain some extra speed you may enable Turbolinks inside of Solidus admin.

Add gem 'turbolinks', '~> 5.0.0' into your Gemfile (if not already present)
and change vendor/assets/javascripts/spree/backend/all.js as follows:

js
//= require turbolinks
//
// ... current file content
//
//= require spree/backend/turbolinks-integration.js

CAUTION Please be aware that Turbolinks can break extensions
and/or customizations to the Solidus admin. Use at your own risk.

Developing Solidus

  • Clone the Git repo

    bash
    git clone git://github.com/solidusio/solidus.git
    cd solidus

Without Docker

  • Install the gem dependencies

    bash
    bin/setup

    Note: If you're using PostgreSQL, MySQL, or MariaDB, you'll need to install those gems through the DB environment variable.

    bash
    # PostgreSQL
    export DB=postgresql
    bin/setup
    
    # MySQL or MariaDB (mysql2 adapter)
    export DB=mysql
    bin/setup

With Docker

bash
docker-compose up -d

Wait for all the gems to be installed (progress can be checked through docker-compose logs -f app).

You can provide the ruby version you want your image to use:

bash
docker-compose build --build-arg RUBY_VERSION=3.0 app
docker-compose up -d

The rails version can be customized at runtime through the RAILS_VERSION environment variable:

bash
RAILS_VERSION='~> 5.0' docker-compose up -d

Running tests:

bash
# sqlite
docker-compose exec app bin/rspec
# postgres
docker-compose exec app env DB=postgres bin/rspec
# mysql
docker-compose exec app env DB=mysql bin/rspec

Accessing the databases:

bash
# sqlite
docker-compose exec app sqlite3 /path/to/db
# postgres
docker-compose exec app env PGPASSWORD=password psql -U root -h postgres
# mysql
docker-compose exec app mysql -u root -h mysql -ppassword

In order to be able to access the sandbox application, just make
sure to provide the appropriate --binding option to rails server. By
default, port 3000 is exposed, but you can change it through SANDBOX_PORT
environment variable:

bash
SANDBOX_PORT=4000 docker-compose up -d
docker-compose exec app bin/sandbox
SANDBOX_PORT=4000 docker-compose exec app bin/dev

Sandbox

Solidus is meant to be run within the context of Rails application. You can
easily create a sandbox application inside of your cloned source directory for
testing purposes.

This sandbox includes solidus_auth_devise and generates with seed and sample
data already loaded.

  • Create the sandbox application

    bash
    bin/sandbox

    You can create a sandbox with PostgreSQL, MySQL, or MariaDB by setting the DB environment variable.

    bash
    # PostgreSQL
    export DB=postgresql
    bin/sandbox
    
    # MySQL or MariaDB (mysql2 adapter)
    export DB=mysql
    bin/sandbox

    Depending on your local environment, it may be necessary for you to set environment variables for your RDBMS, namely:

    • DB_HOST
    • DB_USER
    • DB_PASSWORD

    If you need to create a Rails 5.2 application for your sandbox, for example
    if you are still using Ruby 2.4 which is not supported by Rails 6, you can
    use the RAILS_VERSION environment variable.

    bash
    export RAILS_VERSION='~> 5.2.0'
      bin/setup
      bin/sandbox
  • You can start the Rails server and other services from either the Solidus folder or the
    sandbox one by running the command:

    bash
    bin/dev

Please note: if you run bin/rails server or similar commands, only the Rails server will
start. This might cause the error couldn't find file 'solidus_admin/tailwind.css' when you
try to load all pages.

Tests

Solidus uses RSpec for tests. Refer to its documentation for
more information about the testing library.

CI/CD

We use GitHub Actions to run Solidus' test suites against all supported versions
of Ruby and Rails. Before a pull request can be merged, all of the tests must
pass.

You can see the build statuses on the repository's GitHub Actions
page
.

Run all tests

ChromeDriver is required to run
the backend test suites.

To execute all of the test specs, run the bin/build script at the root of the Solidus project:

bash
createuser --superuser --echo postgres # only the first time
bin/build

The bin/build script runs using PostgreSQL by default, but it can be overridden by setting the DB environment variable to DB=sqlite or DB=mysql (MariaDB uses DB=mysql). For example:

bash
env DB=mysql bin/build

If the command fails with MySQL or MariaDB related errors you can try creating a user with this command:

bash
# Creates a user with the same name as the current user and no restrictions.
mysql --user="root" --execute="CREATE USER '$USER'@'localhost'; GRANT ALL PRIVILEGES ON * . * TO '$USER'@'localhost';"

Run an individual test suite

Each gem contains its own series of tests. To run the tests for the core project:

bash
cd core
bundle exec rspec

By default, rspec runs the tests for SQLite 3. If you would like to run specs
against another database you may specify the database in the command:

bash
env DB=postgresql bundle exec rspec

Code coverage reports

If you want to run the SimpleCov code
coverage report:

bash
COVERAGE=true bundle exec rspec

Extensions

In addition to core functionality provided in Solidus, there are a number of
ways to add features to your store that are not (or not yet) part of the core
project.

A list can be found at extensions.solidus.io.

If you want to write an extension for Solidus, you can use the
solidus_dev_support gem.

Contributing

Solidus is an open source project and we encourage contributions. Please read
CONTRIBUTING.md before contributing.


Overview (`admin/README.md`)

Solidus Admin

A Rails engine that provides an administrative interface to the Solidus e-commerce platform.

Overview

  • Based on ViewComponent and TailwindCSS
  • Uses StimulusJS and Turbo for interactivity
  • Works as a separate engine with its own routes
  • Uses the same models as the main Solidus engine
  • Has its own set of controllers

Installation

solidus_admin is included by default in new stores generated with Solidus 4.3 or later, as well as those generated from the main branch.

solidus_admin can be added to existing stores by bundling it in the Gemfile and running the installer generator:

bash
bundle add solidus_admin
bin/rails g solidus_admin:install

If you're using an authentication system other than solidus_auth_devise you'll need to manually configure authentication methods (see api documentation for SolidusAdmin::Configuration).

If you encounter the error couldn't find file 'solidus_admin/tailwind.css' when loading admin pages, you need to manually build the solidus_admin tailwind CSS styles.
This issue typically occurs when you bundle Solidus from a GitHub branch or from the local filesystem, or with the sandbox application.
Please see Customizing tailwind for more information.

Components

See docs/contributing/components.md for more information about components.

Using it alongside `solidus_backend`

solidus_backend is the current admin interface for Solidus. SolidusAdmin is under development, acts as a drop-in replacement for solidus_backend and will eventually replace it. It's designed to gradually take over existing functions.

For now, you can use both solidus_backend and SolidusAdmin in the same application. To do this, mount the SolidusAdmin engine before Spree::Core::Engine.

You can use a route constraint to replace any solidus_backend routes with SolidusAdmin routes.

By default, SolidusAdmin routes are turned off if a cookie named solidus_admin is set to false, or if a query parameter named solidus_admin is set to false. This lets you switch between the two admin interfaces easily.

This constraint is set up in the application's routes file, so you can easily change it:

ruby
# config/routes.rb
mount SolidusAdmin::Engine, at: '/admin', constraints: ->(req) {
  $redis.get('solidus_admin') == 'true' # or any other logic
}

Authentication & Authorization

  • Solidus Amidn delegates authentication to solidus_backend and relies on solidus_core for authorization.

Development

Adding components to Solidus Admin

Solidus Admin components can be generated with the solidus_admin:componentgenerator:

bash
# the `solidus_admin/` namespace is added by default
bin/rails admin g solidus_admin:component foo
      create  app/components/solidus_admin/foo/component.rb
      create  app/components/solidus_admin/foo/component.html.erb
      create  app/components/solidus_admin/foo/component.yml
      create  app/components/solidus_admin/foo/component.js
      create  spec/components/solidus_admin/foo/component_spec.rb

Please note that when using the component generator from within the admin folder it will generate the component in the library
instead of the sandbox application.

Releasing

  1. Update the version in lib/solidus_admin/version.rb
  2. Commit the changes with the message Release solidus_admin/v1.2.3
  3. cd admin; bundle exec rake release
  4. Manually release on GitHub

Acknowledgements

This project uses icons from Remix Icon, licensed
under the Remix Icon License v1.0.


Overview (`api/README.md`)

solidus_api

API contains the controllers and Jbuilder views implementing the REST API of
Solidus.

Testing

Run the API tests:

bash
bundle exec rspec

Documentation

The API documentation is in the openapi directory. It follows the
OpenAPI specification and it is hosted on Stoplight Docs.

If you want to contribute, you can use Stoplight Studio. Simply
follow these steps:

  1. Create a new Stoplight Studio project
  2. Copy-paste the content of openapi/solidus-api.oas.yml into your project
  3. Edit the endpoints and models as needed
  4. Copy-paste the result back into openapi/solidus-api.oas.yml
  5. Open a PR!

Note: Only use embedded models in Stoplight Studio, as Stoplight Docs is
not compatible with externally-defined models!

CircleCI automatically syncs our Git repo with Stoplight Docs when a PR is
merged, and automatically publishes a new version on Docs when a new Solidus
version is released.

  • solidus-sdk: created
    by Joel Saupe at Deseret Book, this is a JS SDK
    that allows you to use the Solidus API. It even supports plug-ins, so you can
    easily extend it with the endpoints provided by your Solidus extensions!

Overview (`backend/README.md`)

solidus\_backend

Backend contains the controllers, views, and assets making up the admin interface of Solidus.

Assets

JavaScript

Can be found in app/assets/javascripts/spree/backend/

Any new files should be in JavaScript (ES5).

Stylesheets

Can be found in app/assets/stylesheets/spree/backend/

The stylesheets are written in SCSS and include all of Bourbon and Bootstrap 4 alpha.

When running the application there is a styleguide available at:

text
/admin/style_guide

Testing

Run the tests

bash
bundle exec rspec

Run the JavaScript tests (must have ChromeDriver installed)

bash
bundle exec teaspoon

Overview (`core/README.md`)

Summary

Solidus Core provides the essential e-commerce data models upon which the
Solidus system depends.

Core models

Solidus implements over 200 models,
and thus a deep inspection of each in this README would be overkill. Instead,
let's take a quick look at the fundamental models upon which all else depend.
Currently, these models remain in the Spree namespace as part of the legacy of
forking Spree.

NOTE: Documentation is a work in progress

The documentation of Solidus Core is still in progress. Contributions following
this form are welcome and encouraged!

Order sub-system

  • Spree::Store - Records store specific configuration such as store name and URL.
  • Spree::Order - The customers cart until completed, then acts as
    permanent record of the transaction.
  • Spree::LineItem - Variants placed in the order at a particular price.

User sub-system

  • Spree::LegacyUser - Default implementation of User.
  • Spree::UserClassHandle - Configuration point for User model implementation.
  • solidus_auth_devise -
    An official, more robust implementation of a User class with Devise
    integration.

Payment sub-system

  • Spree::Payment - Manage and process a payment for an order, from a specific
    source (e.g. Spree::CreditCard) using a specific payment method (e.g
    Solidus::Gateway::Braintree).
  • Spree::PaymentMethod - A base class which is used for implementing payment methods.
  • Spree::PaymentMethod::CreditCard - An implementation of a Spree::PaymentMethod for credit card payments.
  • Spree::CreditCard - The source of a Spree::Payment using Spree::PaymentMethod::CreditCard as payment method.

Inventory sub-system

  • Spree::ReturnAuthorization - Models the return of Inventory Units to
    a Stock Location for an Order.
  • Spree::StockLocation - Records the name and addresses from which stock items
    are fulfilled in cartons.
  • Spree::InventoryUnit - Tracks the state of line items' fulfillment.
  • Spree::ShippingRate - Records the costs of different shipping methods for a
    shipment and which method has been selected to deliver the shipment.
  • Spree::ShippingMethod - Represents a means of having a shipment delivered,
    such as FedEx or UPS.

Shipments sub-system

  • Spree::Shipment - An order's planned shipments including
    tracking and cost. Shipments are fulfilled from Stock Locations.

Developer notes

Testing

Run the tests:

bash
bundle exec rspec

Overview (`i18n/README.md`)

Solidus Internationalization

This is the Internationalization project for Solidus


Changes in Version 2.0

solidus_i18n Version 2.0+ only contains translation files.

Previous versions of solidus_i18n included extra functionality like locale
selectors and which is now built in to Solidus 2.6+. Configuration for
routing-filter has also been removed and must be configured manually
(See Locale in URL).

Installation

Add the following to your Gemfile:

ruby
gem 'solidus_i18n'
gem 'rails-i18n'
gem 'kaminari-i18n'

Locale in URL

Older versions of solidus_i18n included the routing-filter gem and configured routes to include the locale in the URL.
This is still supported (maybe even recommended) but requires some additional configuration.

  1. Add this gem to your Gemfile, then run bundle install
ruby
gem 'routing-filter'
  1. Add filter :locale to your config/routes.rb
ruby
Rails.application.routes.draw do
  filter :locale

  mount Spree::Core::Engine, at: '/'
end
  1. Configure routing-fitler in config/initializers/locale_filter.rb (optional)
ruby
# Do not include the default locale in the URL
RoutingFilter::Locale.include_default_locale = false

Supported languages

We currently support the following locales
by default. If you need a locale that is not in the list you can add a custom
translation file into your application by following the
Rails translations guide.

Updating Translations

If you want to improve the translations on your language, run the tasks:

text
bundle exec rake solidus_i18n:update_default
bundle exec i18n-tasks add-missing --nil-value --locale <LOCALE>

Substitute with your locale code (e.g: it).

This will do a cleanup and prepare <LOCALE>.yml with all the missing keys.
You can then write the translations and open a pull request.

Model Translations

We removed support for translating models into a separate Gem.

Please update your Gemfile if you still need the model translations.

ruby
# Gemfile
gem 'solidus_globalize', github: 'solidusio-contrib/solidus_globalize', branch: 'master'

Localizing country names

You can translate country names by defining spree.country_names in your own locale files. For example, to have countries in Spanish do:

yml
es:
  spree:
    country_names:
      US: Estados Unidos de Amรฉrica
      UK: Reino Unido
      CA: Canadรก
      # ...

Some supported languages already define localized country names. Take a look at this repo's .yml files for your locale to confirm if we already provide translations.

Contributing

Solidus is an open source project and we encourage contributions. Please read
CONTRIBUTING.md before contributing.

Development

Testing the extension

First bundle your dependencies, then run bin/rake. bin/rake will default to building the dummy
app if it does not exist, then it will run specs. The dummy app can be regenerated by using
bin/rake extension:test_app.

shell
bin/rake

To run Rubocop static code analysis run

shell
bundle exec rubocop

When testing your application's integration with this extension you may use its factories.
You can load Solidus core factories along with this extension's factories using this statement:

ruby
SolidusDevSupport::TestingSupport::Factories.load_for(SolidusI18n::Engine)

Running the sandbox

To run this extension in a sandboxed Solidus application, you can run bin/sandbox. The path for
the sandbox app is ./sandbox and bin/rails will forward any Rails commands to
sandbox/bin/rails.

Here's an example:

text
$ bin/rails server
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop

Releasing new versions

Please refer to the dedicated page in the Solidus wiki.

License

Copyright (c) 2026 Thomas von Deyen, released under the New BSD License.


Overview (`legacy_promotions/README.md`)

Solidus Legacy Promotions

A Rails Engine that contains the classic Spree/Solidus promotion system, extracted from the other Solidus gems.

Installation

If your Gemfile contains the line gem "solidus", this gem is automatically installed. If you require the individual parts of the Solidus suite, you need to add this gem to your Gemfile:

rb
gem "solidus_legacy_promotions"

This gem is slated for deprecation, as its name implies. We're working on integrating a new implementation for promotions and shipping it later this year.

Architecture overview

Solidus Legacy Promotions ships with a powerful rule-based promotions system that allows you to grant flexible
discounts to your customers in many different scenarios. You can apply discounts to the entire
order, to a single line item or a set of line items, or to the shipping fees.

In order to achieve this level of flexibility, the promotions system is composed of four concepts:

  • Promotion handlers are responsible for activating a promotion at the right step of the
    customer experience.
  • Promotion rules are responsible for checking whether an order is eligible for a promotion.
  • Promotion actions are responsible for defining the discount(s) to be applied to eligible
    orders.
  • Adjustments are responsible for storing discount information. Promotion adjustments are
    recalculated every time the order is updated, to check if their eligibility persists when the
    state of the order changes. It is possible to
    [customize how this recalculation behaves][how-to-use-a-custom-promotion-adjuster].

[!NOTE]
Adjustments go beyond promotions and apply to other concepts that modify the order amount.
Taxes are another good example.

Let's take the example of the following promotion:

Apply free shipping on any orders whose total is $100 USD or greater.

Here's the flow Solidus follows to apply such a promotion:

  1. When the customer enters their shipping information,
    the Shipping
    promotion handler activates the promotion on the order.
  2. When activated, the promotion will perform
    some basic eligibility checks (
    e.g. usage limit, validity dates) and
    then ensure the defined promotion rules are met.
  3. When called,
    the ItemTotal
    promotion rule will ensure the order's total is $100 USD or greater.
  4. Since the order is eligible for the promotion,
    the FreeShipping
    action is applied to the order's shipment. The action creates an adjustment that cancels the cost
    of the shipment.
  5. The customer gets free shipping!

This is the architecture at a glance. As you can see, Solidus already ships with some useful
handlers, rules, and actions out of the box.

However, you're not limited to using the stock functionality. In fact, the promotions system shows
its full potential when you use it to implement your own logic. In the rest of the guide, we'll use
the promotions system to implement the following requirements:

We want to uphold a partnership with a new payment platform by offering a 50% shipping discount
when customers pay with it during the checkout.

In order to do this, we'll have to implement our own handler, rule, and action. Let's get to work!

Implementing a new handler

There's nothing special about promotion handlers: technically, they're just plain old Ruby objects
that are created and called in the right places during the checkout flow.

There is no unified API for promotion handlers, but we can take inspiration from
the existing ones
and use a similar format:

ruby title="app/models/amazing_store/promotion_handler/payment.rb"
# frozen_string_literal: true

module AmazingStore
  module PromotionHandler
    class Payment
      RULES_TYPE = 'AmazingStore::Promotion::Rules::Payment'

      attr_reader :order

      def initialize(order)
        @order = order
      end

      def activate
        promotions.each do |promotion|
          promotion.activate(order: order) if promotion.eligible?(order)
        end
      end

      private

      def promotions
        ::Spree::Promotion.
          active.
          joins(:promotion_rules).
          where('promotion_rules.type' => RULES_TYPE)
      end
    end
  end
end

Our promotion handler selects a subset of promotions with a specific rule type that we haven't yet
created. Then, it activates the eligible ones, i.e., those who obey its rules.

Remember that promotion handlers simply apply active promotions to the current order at the correct
stage of the order workflow. While other handlers might pick up our promotions, they won't be able
to activate it if they run before the payment step. With the new handler, we want to ensure that
promotions can be activated after a payment method has been selected for the order.

Let's call our handler as a callback after the checkout flow has transitioned from the :payment
state (see
the section on how to customize state machines):

ruby title="app/overrides/amazing_store/load_payment_promotion_handler.rb"
# frozen_string_literal: true

module AmazingStore
  module LoadPaymentPromotionHandler
    def self.prepended(base)
      base.state_machine.after_transition(from: :payment) do |order|
        AmazingStore::PromotionHandler::Payment.new(order).activate
      end
    end

    ::Spree::Order.prepend(self)
  end
end

Implementing a new rule

Now that we have our handler, let's move on and implement the promotion rule that checks whether the
customer is using the promoted payment method.

We'll allow store admins to edit which payment method carries the discount. The best way to do that
is to create a preference for the promotion rule itself:

ruby title="app/models/amazing_store/promotion/rules/payment.rb"
# frozen_string_literal: true

module AmazingStore
  module Promotion
    module Rules
      class Payment < ::Spree::PromotionRule
        DEFAULT_PREFERRED_PAYMENT_TYPE = 'AmazingStore::AmazingPaymentPlatform'

        ALLOWED_PAYMENT_TYPES = [
          DEFAULT_PREFERRED_PAYMENT_TYPE,
          'Spree::PaymentMethod::Check',
          'Spree::PaymentMethod::CreditCard'
        ].freeze

        preference :payment_type, :string, default: DEFAULT_PREFERRED_PAYMENT_TYPE

        validates :preferred_payment_type, inclusion: {
          in: ALLOWED_PAYMENT_TYPES,
          allow_blank: true
        }, on: :update

        def applicable?(promotable)
          promotable.is_a?(::Spree::Order)
        end

        def eligible?(order, _options = {})
          order.payments.any? do |payment|
            payment.payment_method.type == preferred_payment_type
          end
        end
      end
    end
  end
end

[!CAUTION]
You may have noticed that we allow the payment type to be blank on creation. This is because
promotion rules are initially created without any of their preferences, so that the correct form can
be presented to the admin when configuring the rule. If we enforced the presence of a payment type
since the very beginning, Solidus wouldn't be able to create the promotion rule and admins would get
an error.

Now that we have the implementation of our promotion rule, we also need to give admins a nice UI
where they can manage the rule and enter the promoted payment type. We just need to create the right
partial, where we'll have a local variable promotion_rule available to access the current
promotion rule instance:

markup title="app/views/spree/admin/promotions/rules/_payment.html.erb"
<div class="row">
  <div class="col-6">
    <div class="field">
      <%= promotion_rule.class.human_attribute_name(:payment_type) %>
    </div>
  </div>
  <div class="col-6">
    <div class="field">
      <%= select_tag "#{param_prefix}[preferred_payment_type]", options_for_select(promotion_rule.class::ALLOWED_PAYMENT_TYPES, promotion_rule.preferred_payment_type), class: 'fullwidth' %>
    </div>
  </div>
</div>

The last step is to register our new promotion rule in an initializer:

ruby title="config/initializers/promotions.rb"
# ...
Rails.application.config.spree.promotions.rules << 'AmazingStore::Promotion::Rules::Payment'

When you create a new promotion in the backend, we should now see the Payment promotion rule. For
a better experience, we can associate a description so that it's rendered along its form:

yaml title="config/locales/en.yml"
en:
  # ...
  activerecord:
    attributes:
      amazing_store/promotion/rules/payment:
        description: Must use the specified payment method

Implementing a new action

Finally, let's implement the promotion action that will grant customers a 50% shipping discount. In
order to do that, we can take inspiration from the
existing FreeShipping
action:

text
/* Detailed source-code truncated for AI context efficiency. */

As you can see, there's quite a bit going on here, but hopefully, the comments help you with the
flow of the action and the purpose of the methods we implemented.

Just like rules, promotion actions can also have preferences and allow admin to define them via the
UI. However, in this case, we don't need any of that. Still, Solidus will expect a partial for the
action, so we should create an empty ERB file.

erb title="app/views/spree/admin/promotions/actions/_half_shipping.html.erb"

[!TIP]
You can look at
? the CreateQuantityAdjustments
action and
the corresponding view
for an example of actions with preferences.

Finally, we need to register our action by adding the following to an initializer:

ruby title="config/initializers/promotions.rb"
# ...
Rails.application.config.spree.promotions.actions << 'AmazingStore::Promotion::Actions::HalfShipping'

Like before, let's add a human-friendly description:

yaml title="config/locales/en.yml"
en:
  # ...
  activerecord:
    attributes:
      amazing_store/promotion/actions/half_shipping:
        description: Applies 50% discount in shipping

Restart the server and you should now see your new promotion action!

Let's try it out!

First of all, go to the Promotions section on the backend and click New Promotion. In this case,
it makes sense to check the Apply to all orders option, as our promotion doesn't need a code. Once
the promotion has been created, add the Payment rule and the Half shipping action.

You can now go to the frontend and see how the shipment price is dropped by 50% if you select the
configured payment method.


Overview (`promotions/README.md`)

Solidus Promotions

This gem contains Solidus' recommended promotion system. It is slated to replace the promotion system in the legacy_promotions gem.

The basic architecture is very similar to the legacy promotion system, but with a few decisive tweaks, which are explained in the subsequent sections.

[!IMPORTANT]
If you are upgrading from a previous version of Solidus, it is advised that you update also the promotion system to the new gem. Please consult the Migration Guide.
While the current version of Solidus still installs the legacy promotion system, we advise a migration at the earliest convinience to avoid having to rush the migration in the future.

Architecture

This extension centralizes promotion handling in the order updater significantly improving the performance of cart calculations. A service class, the SolidusPromotions::OrderAdjuster applies the current promotion configuration to the order, adjusting or removing adjustments as necessary.

SolidusPromotions::Promotion objects have benefits, and benefits have conditions. For example, a promotion that is "20% off shirts" would have a benefit of type "AdjustLineItem", and that benefit would have a condition of type "LineItemTaxon" that makes sure only line items with the "shirts" taxon will get the benefit.

Promotion lanes

Promotions get applied by "lane". Promotions within a lane conflict with each other, whereas promotions that do not share a lane will apply sequentially in the order of the lanes. By default these are "pre", "default" and "post", but they can be configured using the SolidusPromotions initializer:

rb
SolidusPromotions.configure do |config|
  config.preferred_lanes = {
    pre: 0,
    default: 1,
    grogu: 2,
    post: 3
  }
end

Types of Benefits

The new Solidus Promotions Systems ships with three benefit types: AdjustLineItem, AdjustShipment and CreateDiscountedItem. To allow more efficient processing of returns and reduce the complexity for bookkeping and support departments fixed discount to all line items in an order can now be given through the AdjustLineItem benefit through the DistributedAmount calculator significantly reducing administration issues with refunds.

`AdjustLineItem`

Benefits of this class will create promotion adjustments on line items. By default, they will create a discount on every line item in the order. If you want to restrict which line items get the discount, add line-item level conditions, such as LineItemProduct.

`AdjustShipment`

Benefits of this class will create promotion adjustments on shipments. By default, they will create a discount on every shipment in the order. If you want to restrict which shipments get a discount, add shipment-level conditions, such as ShippingMethod.

Conditions

Every type of benefit has a list of rules that can be applied to them. When calculating adjustments for an order, benefits will only produce adjustments on line items or shipments if all their respective conditions are true.

Connecting promotions to orders

When there is a join record SolidusPromotions::OrderPromotion, the promotion and the order will be "connected", and the promotion will be applied even if it does not apply_automatically (see below). This is different from Solidus' legacy promotion system here in that promotions are not automatically connected to orders when they produce an adjustment.

If you want to create an OrderPromotion record, the usual way to do this is via a promotion handler:

  • SolidusPromotions::PromotionHandler::Coupon: Connects orders to promotions if a customer or service agent enters a matching promotion code.
  • SolidusPromotions::PromotionHandler::Page: Connects orders to promotions if a customer visits a page with the correct path. This handler is not integrated in core Solidus, and must be integrated by you.
  • SolidusPromotions::PromotionHandler::Page: Connects orders to promotions if a customer visits a page with the correct path. This handler is not integrated in core Solidus, and must be integrated by you.

Promotion categories

Promotion categories simply allow admins to group promotions. They have no further significance with regards to the functionality of the promotion system.

Promotion recalculation

Solidus allows changing orders up until when they are shipped. SolidusPromotions therefore will recalculate orders up until when they are shipped as well. If your admins change promotions rather than add new ones and carefully manage the start and end dates of promotions, you might want to disable this feature:

rb
SolidusPromotions.configure do |config|
  config.recalculate_complete_orders = false
end

Coupon Code Normalization

Solidus Promotions provides a configurable coupon code normalizer that controls how coupon codes are processed before saving and lookup. By default, codes are case-insensitive (e.g., "SAVE20" and "save20" are treated as the same).
You can customize this behavior to support case-sensitive codes, remove special characters, apply formatting rules, or implement other normalization strategies based on your business requirements.

See the coupon_code_normalizer_class configuration option for implementation details.

Installation

Add solidus_promotions to your Gemfile:

ruby
gem 'solidus_promotions'

Once this project is out of the research phase, a proper gem release will follow.

Bundle your dependencies and run the installation generator:

shell
bin/rails generate solidus_promotions:install

This will create the tables for this extension. It will also replace the promotion administration system under
/admin/promotions with a new one that needs turbo-rails. It will also create an initializer within which Solidus is configured to use Spree::SimpleOrderContents and this extension's OrderAdjuster classes. Feel free to override with your own implementations!

Usage

Add a promotion using the admin. Add benefits with conditions, and observe promotions getting applied how you'd expect them to.

In the admin screen, you can set a number of attributes on your promotion:

  • Name: The name of the promotion. The name attribute will also be used to generate adjustment labels. In multi-lingual stores, you probably want different promotions per language for this reason.

  • Description: This is purely informative. Some stores use description in order display information about this promotion to customers, but there is nothing in core Solidus that does it.

  • Start and End: Outside of the time range between starts_at and expires_at, the promotion will not be eligible to any order.

  • Usage Limit: usage_limit controls to how many orders this promotion can be applied, independent of promotion code or user. This is most commonly used to limit the risk of losing too much revenue with a particular promotion.

  • Path: path is a URL path that connects the promotion to the order upon visitation. Not currently implemented in either Solidus core or this extension.

  • Per Code Usage Limit: How often each code can be used. Useful for limiting how many orders can be placed with a single promotion code.

  • Apply Automatically: Whether this promotion should apply automatically. This means that the promotion is checked for eligibility every time the customer's order is recalculated. Promotion Codes and automatic applications are incompatible.

  • Promotion Category: Used to group promotions in the admin view.

Development

When testing your application's integration with this extension you may use its factories.
You can load Solidus core factories along with this extension's factories using this statement:

ruby
SolidusDevSupport::TestingSupport::Factories.load_for(SolidusPromotions::Engine)

License

Copyright (c) 2024 Martin Meyerhoff, Solidus Team, released under the BSD-3-Clause license.


Overview (`sample/README.md`)

solidus\_sample

Sample contains a set of test data which is used to generate the sandbox can be
used to test stores.

Applications including the solidus_sample gem are provided a rake task to
load the sample data:

bash
bundle exec rake spree_sample:load

Testing

Run the tests:

bash
bundle exec rspec

Overview (`storefront/README.md`)

solidus\_storefront

Storefront contains a Rails application template that provides a modern,
minimal, semantic, and easy to extend storefront for
Solidus.

DISCLAIMER: some Solidus extensions (the ones that depend on the legacy
Solidus Frontend) will not work with this storefront because they rely on
defacing views that don't exist here.

Installation

Just run:

bash
rails new store
cd store
bundle add solidus

mkdir -p app/assets/config
cat <<MANIFEST > app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
MANIFEST

bin/rails generate solidus:install --frontend=starter

That will create a new Solidus application with this storefront installed.

Please note that Solidus Auth Devise
will also be added to your application as it's required by this storefront.

Considerations

The installation of the storefront copies the views, assets, routes,
controllers, and specs to your project. You can easily change anything
that we created; this gives you a lot of freedom of customization.

Note that since the storefront is a Rails application template, it
doesn't have the capability to automatically update your storefront code
whenever the template is updated.

It is not possible right now to generate a new Rails app with the template,
i.e. run rails new --template=URL, since the template expects Solidus to be
installed on the app.

In addition, please note that it will add Solidus Auth Devise frontend
components to your app. At the moment, you will need to manually remove the
gem and its frontend components if you don't need them.

Compatibility

Because this component copies files over the host application, and it is
intended to be used only when Solidus is installed for the first time, we can
guarantee the latest code only works with the latest available version of
Solidus and Rails.

Each Solidus version ships with its own version of the storefront: the
installer applies the templates coming from the Solidus branch corresponding
to the version being installed. For example, Solidus v4.5 will use the
templates coming from the v4.5 branch of the Solidus repository, while
prereleases will use the ones coming from the main branch.

Testing

Run the storefront test suite, which generates a sandbox application from the
templates (if it doesn't exist yet) and runs the specs against it:

bash
bin/rspec

Development

For information about contributing to this component please refer to
docs/development.md. There you'll find information on
tasks like:

  • Running the sandbox
  • Docker development

Overview (`lib/demo/README.md`)

Solidus Docker Demo

The Dockerfile in this folder is intended for demoing purposes, meaning that a
user can run an installation of Solidus using a Docker container.

How to build the image

Make sure Docker is installed in your local and run the following command:

shell
docker build -t solidusio/solidus-demo:latest -f lib/demo/Dockerfile .

How to run the image

You can either run the image you built locally or run the official image pushed
in Dockerhub.

shell
docker run --rm -it -p 3000:3000 solidusio/solidus-demo:latest

How to push the image

If you want to push the image you can use the following command, just note that
this specific command will push the image to the official Solidus Dockerhub
account.

shell
docker push solidusio/solidus-demo:latest

Admin Docs Components (`admin/docs/components.md`)

Components

Components are the main building blocks of the admin interface. They are implemented as ViewComponents and are rendered directly by controllers.

The following documentation assumes familiarity with ViewComponents. If you are not please refer to the ViewComponent documentation.

There are two types of components:

  • UI components are the building blocks of the interface. Typically, they are small components that are used to build more complex components and are generic enough to be reused in various contexts. UI components are located under the app/components/solidus_admin/ui folder.
  • Page components are the primary components rendered by the controllers. Generally, they are full-page components rendered directly by the controllers. They are located under the app/components/solidus_admin directory following the naming convention of the controller and action names they are used in. For example app/components/solidus_admin/orders/index/component.rb is the component that is rendered by the SolidusAdmin::OrdersController#index action.

Generating components

Components can be generated using the solidus_admin:component generator combined with the bin/rails command from the Solidus repository.

shell
$ bin/rails admin g solidus_admin:component foo
      create  app/components/solidus_admin/foo/component.rb
      create  app/components/solidus_admin/foo/component.html.erb
      create  app/components/solidus_admin/foo/component.yml
      create  app/components/solidus_admin/foo/component.js

Using bin/rails admin will run the generator from the solidus_admin engine, instead of the sandbox application.

Coding style

For UI components in particular, it's preferable to accept only simple Ruby values in the initializer and use alternative constructors to accept more complex objects. This makes components easier to use and test. For example:

ruby
# bad

class SolidusAdmin::UI::OrderStatus::Component < ViewComponent::Base
  def initialize(order:)
    @order = order
  end
end

# good

class SolidusAdmin::UI::OrderStatus::Component < ViewComponent::Base
  def initialize(status:)
    @status = status
  end

  def self.for_order(order)
    new(status: order.status)
  end
end

For style variations within the component we use the term scheme rather than variant to avoid confusion with product variants.

For size variations we use the term size with single letter values, e.g. s, m, l, xl, xxl.

For text content we use the term text rather than name to avoid confusion with the name attribute of the HTML tag.

Component registry

Components are registered in the SolidusAdmin::Config.components registry. This allows replacing components for customization purposes and components deprecation between versions.

To retrieve component classes from the registry, use the component helper within controllers and components that inherit from SolidusAdmin::BaseComponent or include SolidusAdmin::ComponentHelper. For example, component('ui/button') will fetch SolidusAdmin::UI::Button::Component.

When to use UI vs. Page components

Generally new components are built for a specific controller action and are used only within that action. In such cases, it's better to use a Page component and define it under the namespace of the action, e.g. app/components/solidus_admin/orders/index/payment_status/component.rb.

If a component is used by multiple actions of the same controller it can be moved to the controller namespace, e.g. app/components/solidus_admin/orders/payment_status/component.rb.

When a component is used by multiple controllers, you can either duplicate it in multiple places or move it to the ui namespace.

Although it may seem counterintuitive, duplicating the component can often be beneficial. This allows you to modify the component in one place without affecting other components that might be using it. Over time, the two copies may share enough generic code that it can be extracted into a UI component.

UI components should be very generic and reusable. However, they should not try to anticipate all possible use cases. Instead, they should be extracted from existing components that are already used in multiple places. This has proven to be the most effective way to build UI components. We've found that trying to anticipate theoretical use cases often leads to over-engineered code that eventually needs to be adapted to the actual use cases or is never used at all.

Naming conventions

The project uses a naming convention for components that slightly deviates from ViewComponent defaults. This is done to simplify renaming components and moving them around.

All files related to a component have a base name of component, each with its own extension. These files are placed in a folder named after the component class they define.

E.g. app/components/solidus_admin/orders/index/payment_status/component.rb defines the SolidusAdmin::Orders::Index::PaymentStatus::Component class.

With this approach, renaming a component is as simple as renaming the folder and the class name, without the need to change the names of all the files.

Translations

Components can define their own translations in the component.yml file and they're expected to be self contained. This means that translations defined in solidus_core should not be used in components.

Please refer to the ViewComponent documentation for more information.

Previews and Lookbook

For UI components we leverage ViewComponent previews combined with Lookbook to provide a live preview of the component. This approach is highly beneficial for understanding the component's appearance and how it can be modified using different arguments.

Creating previews for page components can be challenging and prone to errors, as they often require a more complex context for rendering. Therefore, we typically don't use previews for page components, except for the most basic ones. However, if a component has a wide range of arguments and we want to cover all combinations, we might create a preview for it.

In order to inspect previews it's enough to visit /lookbook in the browser while the server is running.

Testing

Testing methods for components vary depending on whether they are UI or Page components. UI components are tested in isolation, while Page components, which often require a more complex context, are tested through feature specs.

For UI components, we use previews to achieve maximum coverage. This approach is sufficient for most basic components, but more complex components may require additional specs. This method has proven to minimize maintenance and code churn in the spec code, and it avoids repeating the code needed to render the component with different arguments.

Page components are tested in the context of the controller action they are used in. For example, admin/spec/features/orders_spec.rb covers interactions with the order listing and indirectly tests the SolidusAdmin::Orders::Index::Component class, among others.
We've found this to be the most effective way to test page components, as recreating the context needed for them in isolation can be difficult and prone to errors.
However, this is not a hard rule. If a Page component needs to be tested in isolation, or if a UI component requires a more complex context, you can always write additional specs.


Admin Docs Customizing Components (`admin/docs/customizing_components.md`)

Customizing components

Solidus Admin uses view components to render the views. Components are
a pattern for breaking up the view layer into small, reusable pieces, easy to
reason about and test.

All the components Solidus Admin uses are located in the app/components folder of the
solidus_admin gem. As you can see, they are organized in a particular folder structure:

  • All of them are under the SolidusAdmin namespace.
  • They are grouped in sidecar directories, where the main component file and
    all its related files (assets, i18n files, etc.) live together.

Solidus Admin components are designed to be easily customizable by the host
application. Because of that, if you look at how they are designed, you'll find
a series of patterns are followed consistently:

  • Components are always resolved from a global registry in
    SolidusAdmin::Config.components instead of being referenced by their constant.
  • It's possible to override the registry locally inside a component by redefining
    the #component method. This is useful when you need to use a component that
    is not registered in the global registry or need to address some edge case.
  • In any case, Solidus Admin components initializers only take keyword
    arguments.

A picture is worth a thousand words, so let's depict how this works with an
example:

ruby
# app/components/solidus_admin/foo/component.rb
class SolidusAdmin::Foo::Component < SolidusAdmin::BaseComponent
  def component(key)
    return MyApplication::Bar::Component if key == 'bar'

    super
  end

  erb_template <<~ERB
    <div>
      <%= render component('bar').new %>
    </div>
  ERB
end
# render component('foo').new

Replacing a component's template

In the most typical case, you'll only need to replace the template used by a
component. You can do that by creating a new component with a maching path in
your application, inheriting from the default one. Then, you can create a new
template for it. For example, to replace the menu item template:

rb
# app/components/my_admin/navigation/item/component.rb
class MyAdmin::Navigation::Item::Component < SolidusAdmin::Layout::Navigation::Item::Component
end
erb
<%# app/components/my_admin/navigation/item/component.html.erb %>
<li><%= link_to @item.name, path %></li>

Prepending or appending to a component's template

In some situations, you might only need to add some markup before or after a
component. You can easily do that by rendering the Solidus Admin component and
adding your markup before or after it.

erb
<%# app/components/my_admin/menu_item/component.html.erb %>
<h1>MY STORE ADMINISTRATION</h1>
<%= render SolidusAdmin::MenuItem::Component.new %>

Replacing a component

You can replace a component by creating a new one with a matching path in your
application.

There are two considerations to keep in mind:

  • Be aware that other components might be using the component you're replacing.
    They should only be using its #initialize method, so make sure to keep
    compatibility with it when they're called.
  • Solidus Admin's components always inherit from
    SolidusAdmin::BaseComponent.
    You can consider doing the same if you need to use one of its helpers.

For example, the following replaces the menu item component:

ruby
# app/components/my_admin/menu_item/component.rb
class MyAdmin::MenuItem::Component < SolidusAdmin::BaseComponent
  # Here goes your code
end

If you need more control, you can explicitly register your component in the
Solidus Admin container instead of using an implicit path:

ruby
# config/initalizers/solidus_admin.rb
SolidusAdmin::Config.components['ui/button'] = "MyApplication::Button::Component"

Tweaking a component

If you only need to tweak a component, you can always inherit from it in a
matching path from within your application (or manually add it to the
registry) and override the methods you need to change:

ruby
# app/components/my_admin/menu_item/component.rb
class MyAdmin::MenuItem::Component < SolidusAdmin::MenuItem::Component
  def sorted_items
    super.reverse
  end
end

Be aware that this approach comes with an important trade-off: You'll need to
keep your component in sync with the original one as it changes on future updates.
For instance, in the example above, the component is overriding a private
method, so there's no guarantee that it will continue to exist in the future
without being deprecated, as we only guarantee public API stability.


Admin Docs Index Pages (`admin/docs/index_pages.md`)

Adding index pages

Index pages are common in the admin interface, and they are used to display a list of records for a specific model.

Since these pages often have a similar appearance, we have a dedicated component to build them. This component helps avoid writing repetitive boilerplate code.

The `index` action

The index action is the standard action used to display index pages. It's a standard GET action that renders the index component.

To support search scopes and filters the controller should include the SolidusAdmin::ControllerHelpers::Search module and call
apply_search_to as follows:

ruby
class SolidusAdmin::UsersController < SolidusAdmin::BaseController
  include SolidusAdmin::ControllerHelpers::Search

  def index
    users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
    # ...

For pagination support, the index action should also call the paginate method provided by SolidusAdmin::ControllerHelpers::Pagination (included in SolidusAdmin::BaseController). It paginates the collection with Kaminari, 20 records per page by default (pass per_page: to change it), and returns the page to be displayed. Assign it to @page.

ruby
def index
  users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
  @page = paginate(users)
  # ...

To sort the records, pass ordered_by: with the columns and directions to sort by. The primary key is added as a final sort column when it isn't already included, so records with equal values don't repeat or go missing between pages.

ruby
@page = paginate(products, ordered_by: {name: :asc}) # ORDER BY name ASC, id ASC

Finally, the index action should render the index component passing the @page instance variable as the page prop.

ruby
def index
  users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
  @page = paginate(users)
  render component('users/index').new(page: @page)
end

The `ui/pages/index` component

The ui/pages/index component is an abstract component that provides sensible defaults for index pages. It also offers template methods that can be used to customize the behavior of these pages.

We recommend examining existing index pages and the UI component itself to understand how they work. In this section, we'll cover the most important aspects.

The index component requires only the page argument during initialization, all other parameters are provided through template methods.

ruby
class SolidusAdmin::Users::Index < Solidus::Admin::UI::Pages::Index
  def model_class
    Spree.user_class
  end
end

render component('users/index').new(page: @page)

Batch Actions

Batch actions are operations that can be performed on multiple records simultaneously. The index page internally uses the ui/table component and depends on the batch_actions method to render the batch actions dropdown.

In the component, batch actions are provided as an array of hashes, with each hash representing a single batch action. Each hash must contain the following keys:

  • label: the name of the batch action, this will be used as the label of the dropdown item
  • icon: the remix icon-name to be used as the icon of the dropdown item (see the ui/icon component for more information)
  • action: the name of the action to be performed when the batch action is selected. It can be a URL or a path
  • method: the HTTP method to be used when performing the action, such as :delete

The batch_actions method is called in the context of the controller, so you can use any controller method or helper to build the batch actions.

Batch actions will be submitted to the specified action with an id parameter containing the IDs of the selected records. Using id as the
parameter name allows the same action to support both batch and single-record actions for standard routes.

E.g.

ruby
# in the component
def batch_actions
  [
    {
      label: "Delete",
      icon: "trash",
      action: solidus_admin.delete_admin_users_path,
      method: :delete
    }
  ]
end
ruby
# in the controller
def delete
  @users = Spree.user_class.where(id: params[:id])
  @users.destroy_all
  flash[:notice] = "Admin users deleted"
  redirect_to solidus_admin.users_path, status: :see_other
end

Search Scopes

Search scopes are used to filter the records displayed on the index page. The index page internally uses the ui/table component and relies on the scopes method to render the search scope buttons.

In the component, search scopes are provided as an array of hashes, with each hash representing a single search scope. Each hash must contain the following keys:

  • label: the name of the search scope, used as the label of the button
  • name: the name of the search scope, sent via q[scope] parameter when the button is clicked
  • default: whether this is the default search scope, used to highlight the button when when the page is loaded

On the controller side, search scopes can be defined with the search_scope helper, provided by SolidusAdmin::ControllerHelpers::Search. This helper takes a name, an optional default keyword argument, and a block. The block will be called with the current scope and should return a new ActiveRecord scope.
E.g.

ruby
module SolidusAdmin
  class UsersController < SolidusAdmin::BaseController
    include SolidusAdmin::ControllerHelpers::Search

    search_scope(:customers, default: true) { _1.left_outer_joins(:role_users).where(role_users: { id: nil }) }
    search_scope(:admin) { _1.joins(:role_users).distinct }
    search_scope(:with_orders) { _1.joins(:orders).distinct }
    search_scope(:without_orders) { _1.left_outer_joins(:orders).where(orders: { id: nil }) }
    search_scope(:all)

    def index
      users = apply_search_to(Spree.user_class.order(id: :desc), param: :q)
      # ...

Filters

Filters are used to narrow down the records displayed on the index page. The index page internally uses the ui/table/ransack_filter component and depends on the filters method to render the filters dropdown.

In the component, filters are represented as an array of hashes, with each hash representing a single filter. Each hash must contain the following keys:

  • label: the name of the filter, used as the label in the filter bar
  • attribute: the name of the ransack-enabled attribute to be filtered
  • predicate: the name of the ransack predicate to be used, e.g. eq, in, cont
  • options: an array of options to be used for the filter, in the standard rails form of [["label", "value"], ...]

On the controller side it's enough to add ransack support to the index action by including SolidusAdmin::ControllerHelpers::Search and calling
apply_search_to as explained in the Index action section.

On the controller side, you simply need to add Ransack support to the index action by including the SolidusAdmin::ControllerHelpers::Search module and calling the apply_search_to method, as explained in the Index action section.


Admin Docs Menu Items (`admin/docs/menu_items.md`)

Customizing the Menu

You are allowed to add your custom links to the main navigation. To do so, you can access SolidusAdmin::Config.menu_items in an initializer:

ruby
# config/initializers/solidus_admin.rb
SolidusAdmin::Config.menu_items << {
  key: :my_custom_link,
  route: :my_custom_link_path,
  icon: "24-hours-fill",
  position: 80
}
  • The key you provide will be used to translate the link's label under the
    solidus_admin.menu_item.#{key} key.
  • Icon needs to be an icon name from Remixicon.
  • Position tells Solidus where to place the link in the main navigation. The
    default items are placed with 10 points of difference between them.

For nested links, you can provide a children: option with an array of hashes:

ruby
# config/initializers/solidus_admin.rb
SolidusAdmin::Config.configure do |config|
  config.menu_items << {
    key: :my_custom_link,
    route: :my_custom_link_path,
    icon: "24-hours-fill",
    position: 80,
    children: [
      {
        key: :my_custom_nested_link,
        route: :my_custom_nested_link_path,
        position: 80
      }
    ]
  }
end

Your custom link will be rendered in the active state when its base path (i.e, the path without the query string) matches the one for the current url.


Admin Docs Stimulusjs (`admin/docs/stimulusjs.md`)

StimulusJS

This project uses StimulusJS to add interactivity to the admin interface.

Using the `stimulus_id` helper

All JavaScript files are imported using import-maps, eliminating the need for compilation.

Any component.js file is automatically loaded as a StimulusJS controller. The component path is used as the identifier, which is achieved by using parameterize and replacing / with --.
For example, app/components/solidus_admin/foo/component.js is loaded as solidus-admin--foo..

To simplify the use of StimulusJS controllers in components, a stimulus_id helper is provided.
This helper ensures that the controller identifier is correctly used every time.

erb
<div
  data-controller="<%= stimulus_id %>"
  data-action="click-><%= stimulus_id %>#doSomething"
  data-<%= stimulus_id %>-foo-value="123"
>
  ...
</div>

Coding Style

Besides the standard StimulusJS conventions, we have a few additional tricks to make the code more readable and maintainable.

Separating the state from the DOM

Whenever the controller gets beyond trivial we try to separate the state from DOM updates using a render() method.

js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "details" ]

  connect() {
    this.render()
  }

  show() {
    this.open = true
  }

  render() {
    this.detailsTarget.hidden = !this.open
  }
}

Using values to communicate with the external world

Values are an excellent tool for communicating with the external environment and representing state.
Any change to them will be reflected in the DOM and the change callbacks provided by StimulusJS are a great way to react to state changes.

js
import { Controller } from "stimulus"

export default class extends Controller {
  static values = { open: Boolean }

  connect() {
    this.render()
  }

  show() {
    this.openValue = true
  }

  openValueChanged() {
    this.render()
  }

  render() {
    this.detailsTarget.hidden = !this.openValue
  }
}

Leveraging stimulus-use

Solidus Admin leverages stimulus-use, an external library that offers a collection of composable behaviors for Stimulus Controllers. These mixins greatly simplify the work of developers and can be used when adding new components.
For more information, please refer to the project doc. You can also find examples of usage in the Admin codebase (for instance, useClickOutside and useDebounce mixins are used in admin components).


Admin Docs Tailwindcss (`admin/docs/tailwindcss.md`)

Tailwind CSS for the Admin UI

Solidus Admin uses Tailwind CSS for styling. The
benefit of using Tailwind is that it allows you to customize the look and feel
of the admin without having to write any CSS. By leveraging utility classes,
you can easily change the colors, fonts, and spacing in use.

Solidus Admin provides a precompiled CSS file that includes all the necessary
Tailwind classes for the admin to work out of the box.

In case you need to customize the admin's look and feel, or create custom
components, you can do so by running Tailwind's build process in your host
application.

This process presumes that you have a working knowledge of Tailwind CSS.
If you are not familiar with Tailwind, please refer to the Tailwind documentation for more information.

Coding Style

The provided Tailwind CSS configuration is as vanilla as possible, with little to no customizations.
We aim to stay within the boundaries of the default Tailwind CSS classes and avoid custom classes.
Occasionally, we need to add custom styles and in such cases we try to use dynamic square brackets classes.

CSS classes are all kept in the HTML templates of the components in order to simplify the configuration.

The few customizations we have added, are through plugins as they support not only classes but also variants.

Releasing with a precompiled Tailwind CSS

Solidus Admin Tailwind CSS stylesheets are precompiled and included in Solidus gems released to Rubygems.

This means that there's no need to compile the admin Tailwind CSS file in the host application.

The compiled file is not included in the git repository, but it is generated and incorporated in the gem as part of the rake release task (see releasing instructions). This approach completely removes the potential noise created by committing that file to the repository, and the need to keep it updated in pull requests that should affect it.

Customizing TailwindCSS

In order to customize the admin's look and feel, you'll need to set up a local
Tailwind build. This is a two-step process:

Add Tailwind configuration files to your application:

shell
bin/rails solidus_admin:tailwindcss:install

This will create all the necessary files for you to customize TailwindCSS,
including:

  • A config/solidus_admin/tailwind.config.js configuration file
    that will automatically import the Solidus Admin's default configuration.
  • An app/assets/stylesheets/solidus_admin/application.tailwind.css file
    in which you can add your own customizations.
  • Tasks to build the CSS file once or watch for changes and automatically
    rebuild the target app/assets/builds/solidus_admin/application.css file.

In order to manually build the CSS file, run:

shell
bin/rails solidus_admin:tailwindcss:build

Or, to watch for changes and automatically rebuild the CSS file, run:

shell
bin/rails solidus_admin:tailwindcss:watch

Caveat: Conflict with sassc-rails

Tailwind uses modern CSS features that are not recognized by the sassc-rails extension that was included by default in the Gemfile for Rails 6. In order to avoid any errors like SassC::SyntaxError, you must remove that gem from your Gemfile.

See https://github.com/rails/tailwindcss-rails#conflict-with-sassc-rails.

Development

The main solidus bin/dev command will automatically compile the Tailwind CSS stylesheets and watch for changes.


Storefront Docs Development (`storefront/docs/development.md`)

Solidus Storefront development information

This document aims to give some extra information for developers that are going
to contribute to our solidus_storefront component.

Running the sandbox

You can run bin/dev to create a sandboxed Solidus application, start the server and the watcher process that will update files in the sandbox whenever something
changes in the templates folder.

text
$ bin/dev
18:54:04 web.1   | started with pid 39831
18:54:04 watch.1 | started with pid 39832
18:54:04 watch.1 | 18:54:04 - INFO - Guard is now watching at '/Users/elia/Code/Nebulab/solidus_storefront'
18:54:05 web.1   | => Booting Puma
18:54:05 web.1   | => Rails 7.0.4.1 application starting in development
18:54:05 web.1   | => Run `bin/rails server --help` for more startup options
18:54:06 web.1   | Puma starting in single mode...
18:54:06 web.1   | * Puma version: 5.6.5 (ruby 2.7.6-p219) ("Birdie's Version")
18:54:06 web.1   | *  Min threads: 5
18:54:06 web.1   | *  Max threads: 5
18:54:06 web.1   | *  Environment: development
18:54:06 web.1   | *          PID: 39833
18:54:06 web.1   | * Listening on http://127.0.0.1:3000
18:54:06 web.1   | * Listening on http://[::1]:3000
18:54:06 web.1   | Use Ctrl-C to stop

If you need to recreate the sandbox from scratch you can run bin/sandbox.

Using bin/rails will forward any Rails commands to sandbox/bin/rails.

Default username and password for admin are: [email protected] and test123.

To run the watcher manually please use bin/guard (see the Guardfile for the
configuration).

Docker development

If you are a docker user you can wake up the project as usual with:

bash
docker-compose up -d

Wait for all the dependencies to be installed. You can check progress with docker-compose logs -f app.

Then you can dispatch commands to the app container:

bash
docker-compose exec app bin/rails server

When running the sandbox application, take into account
that you need to bind to 0.0.0.0. By default, port 3000 is exposed but you
can changed it through SANDBOX_PORT environment variable:

bash
SANDBOX_PORT=4000 docker-compose up -d
docker-compose exec app bin/sandbox
docker-compose exec app bin/rails server --binding 0.0.0.0 --port 4000

--- METRICS ---