From 21ed972d5c7f1e6b9431d9323f73e30a26951bbe Mon Sep 17 00:00:00 2001 From: Elia Schito Date: Thu, 2 Oct 2025 19:13:01 +0200 Subject: [PATCH 1/4] Add Spree.admin_user_class and use it across admin/backend; default to user_class. Update initializer template, menu conditions, controllers, views, and helper. --- .../solidus_admin/users/edit/component.html.erb | 2 +- .../solidus_admin/users/index/component.rb | 2 +- .../solidus_admin/users_and_roles/component.rb | 4 ++-- .../solidus_admin/orders_controller.rb | 2 +- .../solidus_admin/store_credits_controller.rb | 2 +- .../solidus_admin/users_controller.rb | 10 +++++----- .../admin/orders/customer_details_controller.rb | 2 +- .../spree/admin/orders_controller.rb | 2 +- .../spree/admin/search_controller.rb | 4 ++-- .../spree/admin/store_credits_controller.rb | 2 +- .../spree/admin/users/api_key_controller.rb | 2 +- .../controllers/spree/admin/users_controller.rb | 4 ++-- .../spree/admin/store_credit_events_helper.rb | 4 ++-- .../admin/store_credits/edit_amount.html.erb | 2 +- .../app/views/spree/admin/users/edit.html.erb | 2 +- .../app/views/spree/admin/users/index.html.erb | 12 ++++++------ backend/lib/spree/backend_configuration.rb | 2 +- .../templates/config/initializers/spree.rb.tt | 6 ++++++ core/lib/spree/core.rb | 17 +++++++++++++++++ 19 files changed, 53 insertions(+), 30 deletions(-) diff --git a/admin/app/components/solidus_admin/users/edit/component.html.erb b/admin/app/components/solidus_admin/users/edit/component.html.erb index 98401c5d7df..6ab0b956feb 100644 --- a/admin/app/components/solidus_admin/users/edit/component.html.erb +++ b/admin/app/components/solidus_admin/users/edit/component.html.erb @@ -18,7 +18,7 @@ <%= page_with_sidebar do %> <%= page_with_sidebar_main do %> - <%= render component('ui/panel').new(title: Spree.user_class.model_name.human) do %> + <%= render component('ui/panel').new(title: Spree.admin_user_class.model_name.human) do %> <%= solidus_form_for @user, url: solidus_admin.user_path(@user), html: { id: form_id, autocomplete: "off" } do |f| %>
<%= f.text_field(:email) %> diff --git a/admin/app/components/solidus_admin/users/index/component.rb b/admin/app/components/solidus_admin/users/index/component.rb index 9a2041936e3..eb02dfa82a5 100644 --- a/admin/app/components/solidus_admin/users/index/component.rb +++ b/admin/app/components/solidus_admin/users/index/component.rb @@ -4,7 +4,7 @@ class SolidusAdmin::Users::Index::Component < SolidusAdmin::UsersAndRoles::Compo include SolidusAdmin::LastLoginHelper def model_class - Spree.user_class + Spree.admin_user_class end def search_key diff --git a/admin/app/components/solidus_admin/users_and_roles/component.rb b/admin/app/components/solidus_admin/users_and_roles/component.rb index 450aa352d0c..e55af008063 100644 --- a/admin/app/components/solidus_admin/users_and_roles/component.rb +++ b/admin/app/components/solidus_admin/users_and_roles/component.rb @@ -10,9 +10,9 @@ def title def tabs [ { - text: Spree.user_class.model_name.human(count: 2), + text: Spree.admin_user_class.model_name.human(count: 2), href: solidus_admin.users_path, - current: model_class == Spree.user_class, + current: model_class == Spree.admin_user_class, }, { text: Spree::Role.model_name.human(count: 2), diff --git a/admin/app/controllers/solidus_admin/orders_controller.rb b/admin/app/controllers/solidus_admin/orders_controller.rb index 0b2df6cb324..a113a64c605 100644 --- a/admin/app/controllers/solidus_admin/orders_controller.rb +++ b/admin/app/controllers/solidus_admin/orders_controller.rb @@ -95,7 +95,7 @@ def variants_for def customers_for load_order - @users = Spree.user_class + @users = Spree.admin_user_class .where.not(id: @order.user_id) .order(created_at: :desc, id: :desc) .ransack(params[:q]) diff --git a/admin/app/controllers/solidus_admin/store_credits_controller.rb b/admin/app/controllers/solidus_admin/store_credits_controller.rb index 5d0ceb5b24b..036c60fe8ce 100644 --- a/admin/app/controllers/solidus_admin/store_credits_controller.rb +++ b/admin/app/controllers/solidus_admin/store_credits_controller.rb @@ -136,7 +136,7 @@ def set_store_credit end def set_user - @user = Spree.user_class.find(params[:user_id]) + @user = Spree.admin_user_class.find(params[:user_id]) end def set_store_credit_reasons diff --git a/admin/app/controllers/solidus_admin/users_controller.rb b/admin/app/controllers/solidus_admin/users_controller.rb index 04eaa09ac1e..893e75da159 100644 --- a/admin/app/controllers/solidus_admin/users_controller.rb +++ b/admin/app/controllers/solidus_admin/users_controller.rb @@ -15,7 +15,7 @@ class UsersController < SolidusAdmin::BaseController def index users = apply_search_to( - Spree.user_class.order(created_at: :desc, id: :desc), + Spree.admin_user_class.order(created_at: :desc, id: :desc), param: :q, ) @@ -73,9 +73,9 @@ def edit end def destroy - @users = Spree.user_class.where(id: params[:id]) + @users = Spree.admin_user_class.where(id: params[:id]) - Spree.user_class.transaction { @users.destroy_all } + Spree.admin_user_class.transaction { @users.destroy_all } flash[:notice] = t('.success') redirect_back_or_to users_path, status: :see_other @@ -84,7 +84,7 @@ def destroy private def set_user - @user = Spree.user_class.find(params[:id]) + @user = Spree.admin_user_class.find(params[:id]) end def user_params @@ -123,7 +123,7 @@ def set_items end def authorization_subject - Spree.user_class + Spree.admin_user_class end end end diff --git a/backend/app/controllers/spree/admin/orders/customer_details_controller.rb b/backend/app/controllers/spree/admin/orders/customer_details_controller.rb index 5306a6dfe94..d794ffcc740 100644 --- a/backend/app/controllers/spree/admin/orders/customer_details_controller.rb +++ b/backend/app/controllers/spree/admin/orders/customer_details_controller.rb @@ -26,7 +26,7 @@ def update if @order.contents.update_cart(order_params) if should_associate_user? - requested_user = Spree.user_class.find(params[:user_id]) + requested_user = Spree.admin_user_class.find(params[:user_id]) @order.associate_user!(requested_user, @order.email.blank?) end diff --git a/backend/app/controllers/spree/admin/orders_controller.rb b/backend/app/controllers/spree/admin/orders_controller.rb index 3d989459af5..34b9fb3cb29 100644 --- a/backend/app/controllers/spree/admin/orders_controller.rb +++ b/backend/app/controllers/spree/admin/orders_controller.rb @@ -61,7 +61,7 @@ def index end def new - user = Spree.user_class.find_by(id: params[:user_id]) if params[:user_id] + user = Spree.admin_user_class.find_by(id: params[:user_id]) if params[:user_id] order_importer_params = order_params order_importer_params[:bill_address] = user&.bill_address order_importer_params[:ship_address] = user&.ship_address diff --git a/backend/app/controllers/spree/admin/search_controller.rb b/backend/app/controllers/spree/admin/search_controller.rb index 60fe94049e7..71729f2cd6c 100644 --- a/backend/app/controllers/spree/admin/search_controller.rb +++ b/backend/app/controllers/spree/admin/search_controller.rb @@ -11,9 +11,9 @@ class SearchController < Spree::Admin::BaseController def users if params[:ids] # split here may be String#split or Array#split, so we must flatten the results - @users = Spree.user_class.where(id: params[:ids].split(',').flatten) + @users = Spree.admin_user_class.where(id: params[:ids].split(',').flatten) else - @users = Spree.user_class.ransack({ + @users = Spree.admin_user_class.ransack({ m: 'or', email_start: params[:q], name_start: params[:q] diff --git a/backend/app/controllers/spree/admin/store_credits_controller.rb b/backend/app/controllers/spree/admin/store_credits_controller.rb index df377ee94aa..88b652193e9 100644 --- a/backend/app/controllers/spree/admin/store_credits_controller.rb +++ b/backend/app/controllers/spree/admin/store_credits_controller.rb @@ -3,7 +3,7 @@ module Spree module Admin class StoreCreditsController < ResourceController - belongs_to 'spree/user', model_class: Spree.user_class + belongs_to 'spree/user', model_class: Spree.admin_user_class before_action :load_categories, only: [:new] before_action :load_reasons, only: [:edit_amount, :edit_validity] before_action :ensure_store_credit_reason, only: [:update_amount, :invalidate] diff --git a/backend/app/controllers/spree/admin/users/api_key_controller.rb b/backend/app/controllers/spree/admin/users/api_key_controller.rb index 894c2ae4705..4752cffa228 100644 --- a/backend/app/controllers/spree/admin/users/api_key_controller.rb +++ b/backend/app/controllers/spree/admin/users/api_key_controller.rb @@ -21,7 +21,7 @@ def destroy private def user - @user ||= Spree.user_class.find(params[:user_id]) + @user ||= Spree.admin_user_class.find(params[:user_id]) end end end diff --git a/backend/app/controllers/spree/admin/users_controller.rb b/backend/app/controllers/spree/admin/users_controller.rb index 177827780af..b53fff86779 100644 --- a/backend/app/controllers/spree/admin/users_controller.rb +++ b/backend/app/controllers/spree/admin/users_controller.rb @@ -21,7 +21,7 @@ def show end def create - @user = Spree.user_class.new(user_params) + @user = Spree.admin_user_class.new(user_params) if @user.save set_roles set_stock_locations @@ -82,7 +82,7 @@ def items end def model_class - Spree.user_class + Spree.admin_user_class end private diff --git a/backend/app/helpers/spree/admin/store_credit_events_helper.rb b/backend/app/helpers/spree/admin/store_credit_events_helper.rb index 6b2188f67db..dad09289e9f 100644 --- a/backend/app/helpers/spree/admin/store_credit_events_helper.rb +++ b/backend/app/helpers/spree/admin/store_credit_events_helper.rb @@ -73,11 +73,11 @@ def store_credit_event_originator_link(store_credit_event) private # Cannot set the value for a user originator - # because Spree.user_class is not defined at that time. + # because Spree.admin_user_class is not defined at that time. # Spree::UserClassHandle does not work here either as # the assignment is evaluated before user_class is set def add_user_originator_link - originator_links[Spree.user_class.to_s] = { + originator_links[Spree.admin_user_class.to_s] = { new_tab: true, href_type: :user, translation_key: 'admin.store_credits.user_originator' diff --git a/backend/app/views/spree/admin/store_credits/edit_amount.html.erb b/backend/app/views/spree/admin/store_credits/edit_amount.html.erb index b3ce218b0fb..24b668e4c55 100644 --- a/backend/app/views/spree/admin/store_credits/edit_amount.html.erb +++ b/backend/app/views/spree/admin/store_credits/edit_amount.html.erb @@ -1,4 +1,4 @@ -<% admin_breadcrumb(link_to plural_resource_name(Spree::LegacyUser), spree.admin_users_path) %> +<% admin_breadcrumb(link_to plural_resource_name(Spree.admin_user_class), spree.admin_users_path) %> <% admin_breadcrumb(link_to @user.email, edit_admin_user_url(@user)) %> <% admin_breadcrumb(link_to plural_resource_name(Spree::StoreCredit), spree.admin_user_store_credits_path(@user)) %> <% admin_breadcrumb(link_to Spree::StoreCredit.model_name.human, admin_user_store_credit_path(@user, @store_credit)) %> diff --git a/backend/app/views/spree/admin/users/edit.html.erb b/backend/app/views/spree/admin/users/edit.html.erb index a8b9c8aa76c..71e5d12b264 100644 --- a/backend/app/views/spree/admin/users/edit.html.erb +++ b/backend/app/views/spree/admin/users/edit.html.erb @@ -6,7 +6,7 @@ <%= render partial: 'spree/admin/users/user_page_actions' %>
- <%= Spree.user_class.model_name.human %> + <%= Spree.admin_user_class.model_name.human %>
<%= render partial: 'spree/shared/error_messages', locals: { target: @user } %> diff --git a/backend/app/views/spree/admin/users/index.html.erb b/backend/app/views/spree/admin/users/index.html.erb index fd922000edf..5a73f2e8083 100644 --- a/backend/app/views/spree/admin/users/index.html.erb +++ b/backend/app/views/spree/admin/users/index.html.erb @@ -1,7 +1,7 @@ -<% admin_breadcrumb(plural_resource_name(Spree::LegacyUser)) %> +<% admin_breadcrumb(plural_resource_name(Spree.admin_user_class)) %> <% content_for :page_actions do %> - <% if can?(:admin, Spree.user_class) && can?(:create, Spree.user_class) %> + <% if can?(:admin, Spree.admin_user_class) && can?(:create, Spree.admin_user_class) %>
  • <%= link_to t('spree.new_user'), new_admin_user_url, id: 'admin_new_user_link', class: 'btn btn-primary' %>
  • @@ -24,7 +24,7 @@
    - <%= f.label :spree_roles_id_in, Spree.user_class.human_attribute_name(:spree_roles) %> + <%= f.label :spree_roles_id_in, Spree.admin_user_class.human_attribute_name(:spree_roles) %> <%= f.collection_select :spree_roles_id_in, @roles, :id, :name, {}, multiple: true, class: 'select2 fullwidth' %>
    @@ -69,10 +69,10 @@ - <%= sort_link @search, :email, Spree.user_class.human_attribute_name(:email), {title: 'users_email_title'} %> - <%= Spree.user_class.human_attribute_name(:spree_roles) %> + <%= sort_link @search, :email, Spree.admin_user_class.human_attribute_name(:email), {title: 'users_email_title'} %> + <%= Spree.admin_user_class.human_attribute_name(:spree_roles) %> <%= t('spree.num_orders') %> - <%= Spree.user_class.human_attribute_name(:lifetime_value) %> + <%= Spree.admin_user_class.human_attribute_name(:lifetime_value) %> <%= sort_link @search, :created_at, t('spree.member_since') %> diff --git a/backend/lib/spree/backend_configuration.rb b/backend/lib/spree/backend_configuration.rb index 5621fb4641a..0307b7d6855 100644 --- a/backend/lib/spree/backend_configuration.rb +++ b/backend/lib/spree/backend_configuration.rb @@ -234,7 +234,7 @@ def menu_items label: :users, icon: admin_updated_navbar ? 'ri-user-line' : 'user', match_path: %r{/(users|store_credits)}, - condition: -> { Spree.user_class && can?(:admin, Spree.user_class) }, + condition: -> { Spree.admin_user_class && can?(:admin, Spree.admin_user_class) }, url: :admin_users_path, ), MenuItem.new( diff --git a/core/lib/generators/solidus/install/templates/config/initializers/spree.rb.tt b/core/lib/generators/solidus/install/templates/config/initializers/spree.rb.tt index ea8a56aab94..cf9a6531452 100644 --- a/core/lib/generators/solidus/install/templates/config/initializers/spree.rb.tt +++ b/core/lib/generators/solidus/install/templates/config/initializers/spree.rb.tt @@ -83,6 +83,12 @@ end Spree.user_class = <%= options[:user_class].inspect %> <% end -%> +# By default, admin users share the same class as storefront users. +# Override this if you use a separate model for admin users. +<% if options[:user_class].present? -%> + Spree.admin_user_class = <%= options[:user_class].inspect %> +<% end -%> + # Rules for avoiding to store the current path into session for redirects # When at least one rule is matched, the request path will not be stored # in session. diff --git a/core/lib/spree/core.rb b/core/lib/spree/core.rb index 0c7418d4f04..c806b93d2ed 100644 --- a/core/lib/spree/core.rb +++ b/core/lib/spree/core.rb @@ -30,6 +30,7 @@ module Spree autoload :Deprecation, 'spree/deprecation' mattr_accessor :user_class, default: 'Spree::LegacyUser' + mattr_accessor :admin_user_class, default: nil def self.user_class if @@user_class.is_a?(Class) @@ -43,6 +44,22 @@ def self.user_class_name @@user_class end + # The class used for admin/backoffice users. + # Falls back to `Spree.user_class` for backwards compatibility. + def self.admin_user_class + return user_class if @@admin_user_class.nil? + + if @@admin_user_class.is_a?(Class) + raise "Spree.admin_user_class MUST be a String or Symbol object, not a Class object." + elsif @@admin_user_class.is_a?(String) || @@admin_user_class.is_a?(Symbol) + @@admin_user_class.to_s.constantize + end + end + + def self.admin_user_class_name + @@admin_user_class || @@user_class + end + # Load the same version defaults for all available Solidus components # # @see Spree::Preferences::Configuration#load_defaults From 235bf5e079d58270afab0d10e40484b955ca481a Mon Sep 17 00:00:00 2001 From: Elia Schito Date: Fri, 3 Oct 2025 10:39:30 +0200 Subject: [PATCH 2/4] Test env: fix ActionMailer preview_paths FrozenError and require fast_sqlite early for sqlite3 v2 --- core/lib/spree/testing_support/dummy_app.rb | 13 ++++++++++++- docker-compose.yml | 6 +++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/lib/spree/testing_support/dummy_app.rb b/core/lib/spree/testing_support/dummy_app.rb index d0c9d7e69de..df91617df96 100644 --- a/core/lib/spree/testing_support/dummy_app.rb +++ b/core/lib/spree/testing_support/dummy_app.rb @@ -3,6 +3,14 @@ ENV['RAILS_ENV'] = 'test' ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] = '1' +# Ensure sqlite3 v2 is supported when running with the default sqlite adapter. +# The `fast_sqlite` gem relaxes ActiveRecord's sqlite3 version constraints. +begin + require 'fast_sqlite' +rescue LoadError + # Gem not available, skip. +end + require 'rails' require 'active_record/railtie' require 'action_controller/railtie' @@ -106,7 +114,10 @@ class Application < ::Rails::Application # Set the preview path within the dummy app: if ActionMailer::Base.respond_to? :preview_paths # Rails 7.1+ - config.action_mailer.preview_paths << File.expand_path('dummy_app/mailer_previews', __dir__) + # Some Rails versions return a frozen array here; assign a new array + # to avoid FrozenError when augmenting the paths. + existing = Array(config.action_mailer.preview_paths) + config.action_mailer.preview_paths = existing + [File.expand_path('dummy_app/mailer_previews', __dir__)] else config.action_mailer.preview_path = File.expand_path('dummy_app/mailer_previews', __dir__) end diff --git a/docker-compose.yml b/docker-compose.yml index 3e5b7009f97..203e0b8f46d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" services: mysql: @@ -18,12 +18,12 @@ services: - postgres:/var/lib/postgresql/data:cached app: - shm_size: '512mb' + shm_size: "512mb" build: context: .dockerdev dockerfile: Dockerfile args: - RUBY_VERSION: "3.1" + RUBY_VERSION: "3.4.6" PG_VERSION: 13 NODE_VERSION: 20 MYSQL_VERSION: "8.0" From 6e7eacada6e1b73bcd090152220ba3bc8cebe3a3 Mon Sep 17 00:00:00 2001 From: Elia Schito Date: Fri, 3 Oct 2025 17:57:20 +0200 Subject: [PATCH 3/4] Silence the puma server for admin specs --- admin/spec/spec_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/spec/spec_helper.rb b/admin/spec/spec_helper.rb index be6b774a657..6598f90ae35 100644 --- a/admin/spec/spec_helper.rb +++ b/admin/spec/spec_helper.rb @@ -53,6 +53,7 @@ Capybara.disable_animation = true Capybara.default_max_wait_time = ENV['DEFAULT_MAX_WAIT_TIME'].to_f if ENV['DEFAULT_MAX_WAIT_TIME'].present? Capybara.enable_aria_label = true +Capybara.server = :puma, { Silent: true } # A workaround for https://github.com/rspec/rspec-rails/issues/1897 # DATABASE CLEANER require 'database_cleaner' From 6d2dfbe5a312e763c593e965201682502b368450 Mon Sep 17 00:00:00 2001 From: Elia Schito Date: Fri, 3 Oct 2025 17:57:57 +0200 Subject: [PATCH 4/4] Specs: make click_icon resilient to intercepted clicks (scroll + JS click) and update admin docs to use Spree.admin_user_class --- admin/docs/index_pages.md | 12 ++++++------ core/lib/spree/testing_support/capybara_ext.rb | 8 +++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/admin/docs/index_pages.md b/admin/docs/index_pages.md index d18c4b93592..7bfefa1d422 100644 --- a/admin/docs/index_pages.md +++ b/admin/docs/index_pages.md @@ -16,7 +16,7 @@ class SolidusAdmin::UsersController < SolidusAdmin::BaseController include SolidusAdmin::ControllerHelpers::Search def index - users = apply_search_to(Spree.user_class.order(id: :desc), param: :q) + users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q) # ... ``` @@ -24,7 +24,7 @@ For pagination support, the index action should also call the `set_page_and_extr ```ruby def index - users = apply_search_to(Spree.user_class.order(id: :desc), param: :q) + users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q) set_page_and_extract_portion_from(users) # ... ``` @@ -33,7 +33,7 @@ Finally, the index action should render the `index` component passing the `@page ```ruby def index - users = apply_search_to(Spree.user_class.order(id: :desc), param: :q) + users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q) set_page_and_extract_portion_from(users) render component('users/index').new(page: @page) end @@ -50,7 +50,7 @@ The index component requires only the `page` argument during initialization, all ```ruby class SolidusAdmin::Users::Index < Solidus::Admin::UI::Pages::Index def model_class - Spree.user_class + Spree.admin_user_class end end @@ -92,7 +92,7 @@ end ```ruby # in the controller def delete - @users = Spree.user_class.where(id: params[:id]) + @users = Spree.admin_user_class.where(id: params[:id]) @users.destroy_all flash[:notice] = "Admin users deleted" redirect_to solidus_admin.users_path, status: :see_other @@ -124,7 +124,7 @@ module SolidusAdmin search_scope(:all) def index - users = apply_search_to(Spree.user_class.order(id: :desc), param: :q) + users = apply_search_to(Spree.admin_user_class.order(id: :desc), param: :q) # ... ``` diff --git a/core/lib/spree/testing_support/capybara_ext.rb b/core/lib/spree/testing_support/capybara_ext.rb index 33dd0ffc694..494b01b9d53 100644 --- a/core/lib/spree/testing_support/capybara_ext.rb +++ b/core/lib/spree/testing_support/capybara_ext.rb @@ -4,7 +4,13 @@ module Spree module TestingSupport module CapybaraExt def click_icon(type) - find(".fa-#{type}").click + el = find(".fa-#{type}", visible: :all) + el.click + rescue Selenium::WebDriver::Error::ElementClickInterceptedError + # When a floating element (eg. tooltips/overlays) intercepts the click, + # scroll the target into view and dispatch a click via JS to keep tests stable. + page.execute_script('arguments[0].scrollIntoView({block: "center"});', el.native) + page.execute_script('arguments[0].click();', el.native) end def eventually_fill_in(field, options = {})