From 5c07f83e5c1952097675cac01dd002ed337e2ac9 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 9 Jun 2025 18:12:28 +0200 Subject: [PATCH 01/19] Delegate methods Delegate option type methods #name, #presentation, #option_values for easier access in the view. --- core/app/models/spree/product_option_type.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/app/models/spree/product_option_type.rb b/core/app/models/spree/product_option_type.rb index 0dc18316e49..9ea5400295f 100644 --- a/core/app/models/spree/product_option_type.rb +++ b/core/app/models/spree/product_option_type.rb @@ -5,5 +5,7 @@ class ProductOptionType < Spree::Base belongs_to :product, class_name: "Spree::Product", inverse_of: :product_option_types, touch: true belongs_to :option_type, class_name: "Spree::OptionType", inverse_of: :product_option_types acts_as_list scope: :product + + delegate :name, :presentation, :option_values, to: :option_type end end From 830bb455f98100d5a4fe3422b49eaa7d459e78e1 Mon Sep 17 00:00:00 2001 From: benjamin wil Date: Fri, 11 Sep 2026 20:58:31 -0700 Subject: [PATCH 02/19] Unskip feature test Now that the pull request that replaced the referenced PR has been merged, we no longer need to skip this test. --- admin/spec/features/product_spec.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/admin/spec/features/product_spec.rb b/admin/spec/features/product_spec.rb index d4ad0eba2d2..56f30177ee4 100644 --- a/admin/spec/features/product_spec.rb +++ b/admin/spec/features/product_spec.rb @@ -100,9 +100,7 @@ end context "clicking on Edit" do - # skipping test until updated option types UI is merged - # https://github.com/solidusio/solidus/pull/6236 - xit "leads to option type edit page" do + it "leads to option type edit page" do option_type = create(:option_type) product.option_types << option_type visit "/admin/products/just-a-prod" From 94891518042f34b03a140c98269c0b277c3a4f11 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Wed, 11 Jun 2025 18:28:02 +0200 Subject: [PATCH 03/19] Add correct label translation --- core/config/locales/en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index fd2010e341f..b37bd3aef23 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -191,6 +191,7 @@ en: sku: Master SKU slug: Slug tax_category: Tax Category + taxon_ids: Categories weight: Weight width: Width spree/product_property: From 0ba9595dbbd71381f27fffc15a469d8b947e7122 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 16 Jun 2025 19:55:06 +0200 Subject: [PATCH 04/19] Create product_taxons resource --- .../product_taxons_controller.rb | 58 +++++++++++++++++++ admin/config/locales/product_taxons.en.yml | 6 ++ admin/config/routes.rb | 2 + 3 files changed, 66 insertions(+) create mode 100644 admin/app/controllers/solidus_admin/product_taxons_controller.rb create mode 100644 admin/config/locales/product_taxons.en.yml diff --git a/admin/app/controllers/solidus_admin/product_taxons_controller.rb b/admin/app/controllers/solidus_admin/product_taxons_controller.rb new file mode 100644 index 00000000000..2700198e03b --- /dev/null +++ b/admin/app/controllers/solidus_admin/product_taxons_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module SolidusAdmin + class ProductTaxonsController < SolidusAdmin::BaseController + before_action :load_product, only: [:new, :create] + + def new + render component("products/show/categories/new").new(product: @product) + end + + def create + init_taxon + root_taxon! if @taxon.root? + @product.taxons << @taxon + + respond_to do |format| + format.html { redirect_to @product, status: :see_other, notice: t(".success") } + end + rescue ActiveRecord::RecordInvalid + component = component("products/show/categories/new").new(product: @product, taxon: @taxon) + respond_to do |format| + format.html { render component, status: :unprocessable_entity } + format.turbo_stream do + render turbo_stream: turbo_stream.replace(:new_product_category, component), + status: :unprocessable_entity + end + end + end + + private + + def load_product + @product = Spree::Product.friendly.find(params[:product_id]) + end + + def init_taxon + @taxon = Spree::Taxon.new(category_params) + @taxon.taxonomy_id = @taxon.parent&.taxonomy_id + end + + # Parent-less taxons must be associated with a taxonomy of the same name; it's guaranteed that in order to create a + # new parent-less taxon we need to create a new taxonomy. + def root_taxon! + # if Taxonomy.create! fails on the next step, we need validation errors on taxon object + # to display them on the form + @taxon.validate + Spree::Taxonomy.create!(name: @taxon.name, root: @taxon) + end + + def authorization_subject + Spree::Classification + end + + def category_params + params.require(:taxon).permit(:name, :parent_id, :description) + end + end +end diff --git a/admin/config/locales/product_taxons.en.yml b/admin/config/locales/product_taxons.en.yml new file mode 100644 index 00000000000..46959ec0b52 --- /dev/null +++ b/admin/config/locales/product_taxons.en.yml @@ -0,0 +1,6 @@ +en: + solidus_admin: + product_taxons: + title: "Categories" + create: + success: "Product category was successfully added." diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 4234dc6af3e..6b3a6d3c9f0 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -16,6 +16,8 @@ put :discontinue put :activate end + + resources :taxons, only: [:new, :create], controller: :product_taxons end # Needs a constraint to avoid interpreting "new" as a product's slug From 27a0311757fe4ad806a2a48ac067ee5e604eaa6c Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 16 Jun 2025 20:05:37 +0200 Subject: [PATCH 05/19] Create component for new product category --- .../show/categories/new/component.html.erb | 17 +++++++++++++++++ .../products/show/categories/new/component.rb | 17 +++++++++++++++++ .../products/show/categories/new/component.yml | 4 ++++ core/config/locales/en.yml | 1 + 4 files changed, 39 insertions(+) create mode 100644 admin/app/components/solidus_admin/products/show/categories/new/component.html.erb create mode 100644 admin/app/components/solidus_admin/products/show/categories/new/component.rb create mode 100644 admin/app/components/solidus_admin/products/show/categories/new/component.yml diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb b/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb new file mode 100644 index 00000000000..726e1251ca9 --- /dev/null +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb @@ -0,0 +1,17 @@ +<%= turbo_frame_tag :new_product_category, target: :product_organization_frame do %> + <%= render component("ui/modal").new(title: t(".title")) do |modal| %> + <%= form_for @taxon, url: solidus_admin.product_taxons_path(@product), method: :post, html: { id: dom_id(@taxon) } do |f| %> +
+ <%= render component("ui/forms/field").text_field(f, :name, class: "required") %> + <%= render component("ui/forms/field").select(f, :parent_id, parent_taxon_options) %> + <%= render component("ui/forms/field").text_area(f, :description) %> +
+ <% end %> + <% modal.with_actions do %> +
+ <%= render component("ui/button").new(scheme: :secondary, text: t('.cancel')) %> +
+ <%= render component("ui/button").new(type: :submit, text: t('.submit'), form: dom_id(@taxon)) %> + <% end %> + <% end %> +<% end %> diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.rb b/admin/app/components/solidus_admin/products/show/categories/new/component.rb new file mode 100644 index 00000000000..a595710002c --- /dev/null +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class SolidusAdmin::Products::Show::Categories::New::Component < SolidusAdmin::BaseComponent + def initialize(product:, taxon: nil) + @product = product + @taxon = taxon || product.taxons.build + end + + private + + def parent_taxon_options + @parent_taxon_options ||= Spree::Taxon.order(:lft).pluck(:name, :id, :depth).map do + name, id, depth = _1 + ["#{"    " * depth} → #{name}", id] + end + end +end diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.yml b/admin/app/components/solidus_admin/products/show/categories/new/component.yml new file mode 100644 index 00000000000..b9180a77b6d --- /dev/null +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.yml @@ -0,0 +1,4 @@ +en: + cancel: "Cancel" + submit: "Add Category" + title: "New Category" diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index b37bd3aef23..8aa66e72f44 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -412,6 +412,7 @@ en: meta_keywords: Meta Keywords meta_title: Meta Title name: Name + parent_id: Parent Category permalink: Permalink position: Position spree/taxonomy: From a7073899ec92804c8c0110c194a95a4f9f481286 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 16 Jun 2025 19:59:16 +0200 Subject: [PATCH 06/19] Allow alternative item labels in solidus_select By default, a selected option has the same text representation as in the options list. This change allows to customize it. --- .../solidus_admin/ui/forms/select/component.rb | 2 ++ .../solidus_admin/web_components/solidus_select.js | 5 +++++ .../forms/select/component_preview/overview.html.erb | 10 ++++++++++ 3 files changed, 17 insertions(+) diff --git a/admin/app/components/solidus_admin/ui/forms/select/component.rb b/admin/app/components/solidus_admin/ui/forms/select/component.rb index 758ece34949..40d560f1fc8 100644 --- a/admin/app/components/solidus_admin/ui/forms/select/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/select/component.rb @@ -30,6 +30,8 @@ class SolidusAdmin::UI::Forms::Select::Component < SolidusAdmin::BaseComponent # @param choices [Array, Array>] container with options to be rendered # (see `ActionView::Helpers::FormOptionsHelper#options_for_select`). # When +:src+ parameter is provided, use +:choices+ to provide the list of selected options only. + # Include a dataset hash `{ data: { item_label: } }` to change the text displayed in select + # box when option is selected. # @param src [nil, String] URL of a JSON resource with options data to be loaded instead of rendering options in place. # @option attributes [nil, String, Integer, Array] :value which option should be selected # @option attributes [String] :"data-option-value-field" diff --git a/admin/app/javascript/solidus_admin/web_components/solidus_select.js b/admin/app/javascript/solidus_admin/web_components/solidus_select.js index 5dc1a066927..8a360cb7815 100644 --- a/admin/app/javascript/solidus_admin/web_components/solidus_select.js +++ b/admin/app/javascript/solidus_admin/web_components/solidus_select.js @@ -47,6 +47,7 @@ class SolidusSelect extends HTMLSelectElement { allowEmptyOption: true, maxOptions: null, refreshThrottle: 0, + itemLabelField: "itemLabel", plugins: { no_active_items: true, remove_button: { @@ -61,6 +62,10 @@ class SolidusSelect extends HTMLSelectElement { const message = this.input.getAttribute("data-no-results-message"); return `
${message}
`; }, + item: function(data, escape) { + const itemLabel = data[this.settings.itemLabelField] || data[this.settings.labelField]; + return `
${escape(itemLabel)}
`; + } }, }; diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb index 71b48288a94..82567bdbd7e 100644 --- a/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb +++ b/admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb @@ -46,6 +46,16 @@ value: "1" ) %> + +
+
Alternative item label
+ <%= render current_component.new( + id: "single-alt-label", + choices: Spree::State.limit(100).map { [_1.name, _1.id, { data: { item_label: "#{_1.name} (#{_1.country.name})" } }] }, + label: "State", + name: "state" + ) %> +
From 90132b695dd9f68b900eee75d4764f3cd28f43bd Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 16 Jun 2025 20:06:03 +0200 Subject: [PATCH 07/19] Change selected items text in taxons select Indentation with an arrow is only useful when showing the list of taxons in the dropdown, we can just show the name of taxon when selected to save space. --- .../solidus_admin/products/show/categories/new/component.rb | 2 +- admin/app/components/solidus_admin/products/show/component.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.rb b/admin/app/components/solidus_admin/products/show/categories/new/component.rb index a595710002c..4d7dee3c326 100644 --- a/admin/app/components/solidus_admin/products/show/categories/new/component.rb +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.rb @@ -11,7 +11,7 @@ def initialize(product:, taxon: nil) def parent_taxon_options @parent_taxon_options ||= Spree::Taxon.order(:lft).pluck(:name, :id, :depth).map do name, id, depth = _1 - ["#{"    " * depth} → #{name}", id] + ["#{"    " * depth} → #{name}", id, { data: { item_label: name } }] end end end diff --git a/admin/app/components/solidus_admin/products/show/component.rb b/admin/app/components/solidus_admin/products/show/component.rb index bec4d2a2735..c80fdba8066 100644 --- a/admin/app/components/solidus_admin/products/show/component.rb +++ b/admin/app/components/solidus_admin/products/show/component.rb @@ -16,7 +16,7 @@ def form_id def taxon_options @taxon_options ||= Spree::Taxon.order(:lft).pluck(:name, :id, :lft, :depth).map do name, id, _lft, depth = _1 - ["#{"    " * depth} → #{name}", id] + ["#{"    " * depth} → #{name}", id, {data: {item_label: name}}] end end From d7445593abc2b63ec241baba153c501fca5b6efd Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 14 Sep 2026 14:07:40 -0700 Subject: [PATCH 08/19] Update product organization panel Include hidden field to allow emptying categories collection when none selected. Add link and turbo frame for a new category modal. Modal frame targets category select, so on successful request the only part of the form that will be updated is this select field, preserving any other inputs that user might have yet unsaved in the product form. Turbo stream action update will empty the modal essentially closing it. --- .../solidus_admin/base_component.rb | 1 + .../products/show/component.html.erb | 20 +++++++++++++++++-- .../solidus_admin/products/show/component.yml | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/admin/app/components/solidus_admin/base_component.rb b/admin/app/components/solidus_admin/base_component.rb index 49d3f27272b..418d34ceb8d 100644 --- a/admin/app/components/solidus_admin/base_component.rb +++ b/admin/app/components/solidus_admin/base_component.rb @@ -11,6 +11,7 @@ class BaseComponent < ViewComponent::Base include SolidusAdmin::VoidElementsHelper include SolidusAdmin::SolidusFormHelper include Turbo::FramesHelper + include Turbo::StreamsHelper def icon_tag(name, **attrs) render component("ui/icon").new(name:, **attrs) diff --git a/admin/app/components/solidus_admin/products/show/component.html.erb b/admin/app/components/solidus_admin/products/show/component.html.erb index 21e2fd3d589..6ada247c465 100644 --- a/admin/app/components/solidus_admin/products/show/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/component.html.erb @@ -145,8 +145,22 @@ <%= f.checkbox(:promotionable, hint: t(".hints.promotionable_html")) %> <% end %> - <%= render component("ui/panel").new(title: t(".product_organization")) do %> - <%= f.select(:taxon_ids, taxon_options, multiple: true) %> + <%= render component("ui/panel").new(title: t(".product_organization")) do |panel| %> + <%= f.hidden_field :taxon_ids, multiple: true, value: nil %> + <%= turbo_frame_tag :product_organization_frame do %> + <%= render component("ui/forms/field").select( + f, + :taxon_ids, + taxon_options, + multiple: true + ) %> + <%= turbo_stream.update :new_product_category, nil %> + <% end %> + <% panel.with_action( + name: t(".add_new_category"), + href: solidus_admin.new_product_taxon_path(@product), + data: { turbo_frame: :new_product_category } + ) %> <% end %> <% end %> <% end %> @@ -166,3 +180,5 @@ <%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %> <% end %> <% end %> + +<%= turbo_frame_tag :new_product_category, target: :product_organization_frame %> diff --git a/admin/app/components/solidus_admin/products/show/component.yml b/admin/app/components/solidus_admin/products/show/component.yml index 9d9e3855b6f..1c992f78635 100644 --- a/admin/app/components/solidus_admin/products/show/component.yml +++ b/admin/app/components/solidus_admin/products/show/component.yml @@ -1,4 +1,5 @@ en: + add_new_category: "Add new category" back: "Back" delete: "Delete" delete_confirmation: "Are you sure you want to delete this product?" From 4f756d4876e051da0a8fda876a62021ed5f5c40a Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 16 Jun 2025 19:51:31 +0200 Subject: [PATCH 09/19] Display toasts from turbo frame response With turbo frame response only `product_organization_frame` will be targeted for change after successful request, so in order to show flash message we can use turbo stream replace from within the frame to refresh the flashes. --- admin/app/components/solidus_admin/base_component.rb | 1 + .../components/solidus_admin/products/show/component.html.erb | 1 + 2 files changed, 2 insertions(+) diff --git a/admin/app/components/solidus_admin/base_component.rb b/admin/app/components/solidus_admin/base_component.rb index 418d34ceb8d..2ad11adb973 100644 --- a/admin/app/components/solidus_admin/base_component.rb +++ b/admin/app/components/solidus_admin/base_component.rb @@ -10,6 +10,7 @@ class BaseComponent < ViewComponent::Base include SolidusAdmin::StimulusHelper include SolidusAdmin::VoidElementsHelper include SolidusAdmin::SolidusFormHelper + include SolidusAdmin::FlashHelper include Turbo::FramesHelper include Turbo::StreamsHelper diff --git a/admin/app/components/solidus_admin/products/show/component.html.erb b/admin/app/components/solidus_admin/products/show/component.html.erb index 6ada247c465..524d208cc69 100644 --- a/admin/app/components/solidus_admin/products/show/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/component.html.erb @@ -155,6 +155,7 @@ multiple: true ) %> <%= turbo_stream.update :new_product_category, nil %> + <%= turbo_stream.replace :flash_toasts, component("layout/flashes/toasts").new(toasts:) %> <% end %> <% panel.with_action( name: t(".add_new_category"), From 9ad909c58deb6cc1b40ee96f3681a8793fb5ab53 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Mon, 16 Jun 2025 20:50:26 +0200 Subject: [PATCH 10/19] Extract frame with stream actions into component Hides implementation details of the modal "close" and flashes refresh logic, and also allows to reuse it in similar scenarios. --- admin/app/components/solidus_admin/base_component.rb | 4 ++-- .../solidus_admin/products/show/component.html.erb | 4 +--- .../solidus_admin/turbo/target_frame/component.html.erb | 5 +++++ .../solidus_admin/turbo/target_frame/component.rb | 8 ++++++++ 4 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 admin/app/components/solidus_admin/turbo/target_frame/component.html.erb create mode 100644 admin/app/components/solidus_admin/turbo/target_frame/component.rb diff --git a/admin/app/components/solidus_admin/base_component.rb b/admin/app/components/solidus_admin/base_component.rb index 2ad11adb973..8afab764980 100644 --- a/admin/app/components/solidus_admin/base_component.rb +++ b/admin/app/components/solidus_admin/base_component.rb @@ -11,8 +11,8 @@ class BaseComponent < ViewComponent::Base include SolidusAdmin::VoidElementsHelper include SolidusAdmin::SolidusFormHelper include SolidusAdmin::FlashHelper - include Turbo::FramesHelper - include Turbo::StreamsHelper + include ::Turbo::FramesHelper + include ::Turbo::StreamsHelper def icon_tag(name, **attrs) render component("ui/icon").new(name:, **attrs) diff --git a/admin/app/components/solidus_admin/products/show/component.html.erb b/admin/app/components/solidus_admin/products/show/component.html.erb index 524d208cc69..58d489f2e24 100644 --- a/admin/app/components/solidus_admin/products/show/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/component.html.erb @@ -147,15 +147,13 @@ <%= render component("ui/panel").new(title: t(".product_organization")) do |panel| %> <%= f.hidden_field :taxon_ids, multiple: true, value: nil %> - <%= turbo_frame_tag :product_organization_frame do %> + <%= render component("turbo/target_frame").new(:product_organization_frame, source: :new_product_category) do %> <%= render component("ui/forms/field").select( f, :taxon_ids, taxon_options, multiple: true ) %> - <%= turbo_stream.update :new_product_category, nil %> - <%= turbo_stream.replace :flash_toasts, component("layout/flashes/toasts").new(toasts:) %> <% end %> <% panel.with_action( name: t(".add_new_category"), diff --git a/admin/app/components/solidus_admin/turbo/target_frame/component.html.erb b/admin/app/components/solidus_admin/turbo/target_frame/component.html.erb new file mode 100644 index 00000000000..f73e3b094e8 --- /dev/null +++ b/admin/app/components/solidus_admin/turbo/target_frame/component.html.erb @@ -0,0 +1,5 @@ +<%= turbo_frame_tag @id do %> + <%= content %> + <%= turbo_stream.update(@source, nil) if @source %> + <%= turbo_stream.replace :flash_toasts, component("layout/flashes/toasts").new(toasts:) %> +<% end %> diff --git a/admin/app/components/solidus_admin/turbo/target_frame/component.rb b/admin/app/components/solidus_admin/turbo/target_frame/component.rb new file mode 100644 index 00000000000..24fa1e43219 --- /dev/null +++ b/admin/app/components/solidus_admin/turbo/target_frame/component.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class SolidusAdmin::Turbo::TargetFrame::Component < SolidusAdmin::BaseComponent + def initialize(id, source: nil) + @id = id + @source = source + end +end From 4c72c0d23b76b412a32e83d54ca724f0db006d92 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Tue, 17 Jun 2025 15:56:46 +0200 Subject: [PATCH 11/19] Move capybara feature helpers These helpers are related to new admin so it's better to place them in feature_helpers.rb --- .../solidus_admin/testing_support/feature_helpers.rb | 10 ++++++++++ core/lib/spree/testing_support/capybara_ext.rb | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/admin/lib/solidus_admin/testing_support/feature_helpers.rb b/admin/lib/solidus_admin/testing_support/feature_helpers.rb index 4fe4dd88275..9166ba0be6c 100644 --- a/admin/lib/solidus_admin/testing_support/feature_helpers.rb +++ b/admin/lib/solidus_admin/testing_support/feature_helpers.rb @@ -88,6 +88,16 @@ def switch(locator, on: true) checkbox = find(:label, text: locator).find(:checkbox) on ? checkbox.check : checkbox.uncheck end + + def dialog(parent: "body", **options) + within(parent) do + find("dialog", visible: :all, **options) + end + end + + def turbo_frame_modal + dialog(parent: find("turbo-frame", visible: :all)) + end end end end diff --git a/core/lib/spree/testing_support/capybara_ext.rb b/core/lib/spree/testing_support/capybara_ext.rb index 0f3849f6796..e5f7190c6e5 100644 --- a/core/lib/spree/testing_support/capybara_ext.rb +++ b/core/lib/spree/testing_support/capybara_ext.rb @@ -118,16 +118,6 @@ def find_label_by_text(text) # find the original. find("label:not(.select2-offscreen)", text: /#{Regexp.escape(text)}/i, match: :one) end - - def dialog(parent: "body", **options) - within(parent) do - find("dialog", visible: :all, **options) - end - end - - def turbo_frame_modal - dialog(parent: find("turbo-frame", visible: :all)) - end end end end From 15f845516fc7925e0ae090a87e03edcbceb5fb3c Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Tue, 17 Jun 2025 16:17:18 +0200 Subject: [PATCH 12/19] Add feature tests --- admin/spec/features/product_spec.rb | 79 +++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/admin/spec/features/product_spec.rb b/admin/spec/features/product_spec.rb index 56f30177ee4..a4b707813b2 100644 --- a/admin/spec/features/product_spec.rb +++ b/admin/spec/features/product_spec.rb @@ -128,4 +128,83 @@ let(:path) { solidus_admin.product_path(product) } end end + + describe "product organization", :js do + let(:taxonomy) { create(:taxonomy, name: "Apparel") } + let(:root_taxon) { taxonomy.root } + let!(:child_taxon) { create(:taxon, name: "Caps", parent: root_taxon) } + let!(:product) { create(:product, name: "Just a product", slug: "just-a-prod", price: 19.99) } + + describe "assigning categories" do + it "assigns product categories" do + visit "/admin/products/just-a-prod" + expect(solidus_select_control("Categories").text).to be_empty + + solidus_select %w[Apparel Caps], from: "Categories" + within("header") { click_on "Save" } + expect(page).to have_content("Product was successfully updated.") + + visit "/admin/products/just-a-prod" + expect(solidus_select_control("Categories")).to have_content("Apparel") + expect(solidus_select_control("Categories")).to have_content("Caps") + end + + it "unassigns product categories" do + product.taxons << root_taxon + + visit "/admin/products/just-a-prod" + solidus_unselect "Apparel", from: "Categories" + within("header") { click_on "Save" } + expect(page).to have_content("Product was successfully updated.") + + visit "/admin/products/just-a-prod" + expect(solidus_select_control("Categories").text).to be_empty + end + end + + context "adding new category" do + it "creates new category and assigns it to product" do + visit "/admin/products/just-a-prod" + click_on "Add new category" + expect(page).to have_content("New Category") + + within(dialog) do + fill_in "Name", with: "Jackets" + solidus_select "Apparel", from: "Parent Category" + click_on "Add Category" + end + + expect(page).to have_content("Product category was successfully added.") + expect(page).not_to have_css("dialog") + expect(page).not_to have_content("New Category") + expect(solidus_select_control("Categories")).to have_content("Jackets") + end + + context "with invalid attributes" do + context "with blank name" do + it "shows error" do + visit "/admin/products/just-a-prod" + click_on "Add new category" + within(dialog) { click_on "Add Category" } + + expect(dialog).to have_content("can't be blank") + end + end + + context "when taxon with same name already belongs to a parent" do + it "shows error" do + visit "/admin/products/just-a-prod" + click_on "Add new category" + within(dialog) do + fill_in "Name", with: child_taxon.name + solidus_select "Apparel", from: "Parent Category" + click_on "Add Category" + end + + expect(dialog).to have_content("must be unique under the same parent Taxon") + end + end + end + end + end end From 7b9b972d1ff4ecf2a73f05612c17106a40beffb3 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Tue, 17 Jun 2025 19:14:10 +0200 Subject: [PATCH 13/19] Add request tests --- .../solidus_admin/product_taxons_spec.rb | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 admin/spec/requests/solidus_admin/product_taxons_spec.rb diff --git a/admin/spec/requests/solidus_admin/product_taxons_spec.rb b/admin/spec/requests/solidus_admin/product_taxons_spec.rb new file mode 100644 index 00000000000..df13ee4845d --- /dev/null +++ b/admin/spec/requests/solidus_admin/product_taxons_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "SolidusAdmin::ProductTaxonsController", type: :request do + let(:admin_user) { create(:admin_user) } + let!(:product) { create(:product) } + + before do + allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) + end + + describe "GET /new" do + it "renders the new template with a 200 OK status" do + get solidus_admin.new_product_taxon_path(product) + expect(response).to have_http_status(:ok) + end + end + + describe "POST /create" do + context "when taxon belongs to a parent" do + context "with valid parameters" do + let!(:parent_taxon) { create(:taxonomy).root } + let(:valid_attributes) { {name: "Accessories", parent_id: parent_taxon.id} } + + it "creates new taxon and new classification" do + expect(Spree::Taxon.count).to eq(1) + expect(product.classifications.count).to eq(0) + + post solidus_admin.product_taxons_path(product), params: {taxon: valid_attributes} + + expect(Spree::Taxon.count).to eq(2) + expect(product.classifications.count).to eq(1) + end + + it "redirects with a 303 See Other status" do + post solidus_admin.product_taxons_path(product), params: {taxon: valid_attributes} + expect(response).to redirect_to(solidus_admin.product_path(product)) + expect(response).to have_http_status(:see_other) + end + end + + context "with invalid parameters" do + let(:invalid_attributes) { {name: ""} } + + it "does not create a new taxon" do + expect { + post solidus_admin.product_taxons_path(product), params: {taxon: invalid_attributes} + }.not_to change(Spree::Taxon, :count) + end + + it "returns unprocessable_entity status" do + post solidus_admin.product_taxons_path(product), params: {taxon: invalid_attributes} + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + context "when taxon is a root" do + context "with valid parameters" do + let(:valid_attributes) { {name: "Accessories", parent_id: nil} } + + it "creates new taxonomy, new root taxon and new classification" do + expect(Spree::Taxonomy.count).to eq(0) + expect(Spree::Taxon.count).to eq(0) + expect(product.classifications.count).to eq(0) + + post solidus_admin.product_taxons_path(product), params: {taxon: valid_attributes} + + expect(Spree::Taxonomy.count).to eq(1) + expect(Spree::Taxon.count).to eq(1) + expect(product.classifications.count).to eq(1) + end + + it "redirects with a 303 See Other status" do + post solidus_admin.product_taxons_path(product), params: {taxon: valid_attributes} + expect(response).to redirect_to(solidus_admin.product_path(product)) + expect(response).to have_http_status(:see_other) + end + end + + context "with invalid parameters" do + let!(:another_root_taxon) { create(:taxonomy, name: "Apparel").root } + let(:invalid_attributes) { {name: another_root_taxon.name, parent_id: nil} } + + it "does not create new records" do + expect(Spree::Taxonomy.count).to eq(1) + expect(Spree::Taxon.count).to eq(1) + expect(product.classifications.count).to eq(0) + + post solidus_admin.product_taxons_path(product), params: {taxon: invalid_attributes} + + expect(Spree::Taxonomy.count).to eq(1) + expect(Spree::Taxon.count).to eq(1) + expect(product.classifications.count).to eq(0) + end + + it "returns unprocessable_entity status" do + post solidus_admin.product_taxons_path(product), params: {taxon: invalid_attributes} + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + end +end From 13c6dd498626e6b9d667178d9de97cfe27e68102 Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Wed, 25 Jun 2025 13:01:53 +0200 Subject: [PATCH 14/19] Fix rubocop offence --- .../solidus_admin/products/show/categories/new/component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.rb b/admin/app/components/solidus_admin/products/show/categories/new/component.rb index 4d7dee3c326..d0cb563eb2b 100644 --- a/admin/app/components/solidus_admin/products/show/categories/new/component.rb +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.rb @@ -11,7 +11,7 @@ def initialize(product:, taxon: nil) def parent_taxon_options @parent_taxon_options ||= Spree::Taxon.order(:lft).pluck(:name, :id, :depth).map do name, id, depth = _1 - ["#{"    " * depth} → #{name}", id, { data: { item_label: name } }] + ["#{"    " * depth} → #{name}", id, {data: {item_label: name}}] end end end From cb976764dab5499d56d36db4e798f92636e0e30a Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Fri, 27 Jun 2025 23:00:51 +0200 Subject: [PATCH 15/19] Allow empty parent for new category Choosing empty parent category will create a root taxon and a new taxonomy record under the hood. --- .../products/show/categories/new/component.html.erb | 2 +- .../solidus_admin/products/show/categories/new/component.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb b/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb index 726e1251ca9..835105c8def 100644 --- a/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.html.erb @@ -3,7 +3,7 @@ <%= form_for @taxon, url: solidus_admin.product_taxons_path(@product), method: :post, html: { id: dom_id(@taxon) } do |f| %>
<%= render component("ui/forms/field").text_field(f, :name, class: "required") %> - <%= render component("ui/forms/field").select(f, :parent_id, parent_taxon_options) %> + <%= render component("ui/forms/field").select(f, :parent_id, parent_taxon_options, include_blank: t(".none")) %> <%= render component("ui/forms/field").text_area(f, :description) %>
<% end %> diff --git a/admin/app/components/solidus_admin/products/show/categories/new/component.yml b/admin/app/components/solidus_admin/products/show/categories/new/component.yml index b9180a77b6d..6b3ae093e25 100644 --- a/admin/app/components/solidus_admin/products/show/categories/new/component.yml +++ b/admin/app/components/solidus_admin/products/show/categories/new/component.yml @@ -1,4 +1,5 @@ en: cancel: "Cancel" + none: "None" submit: "Add Category" title: "New Category" From 9571cb453bfeda5cea837d98ec870970a1152f8f Mon Sep 17 00:00:00 2001 From: Eugene Chaikin Date: Fri, 27 Jun 2025 23:27:52 +0200 Subject: [PATCH 16/19] Exclude feature_helpers from coverage report --- 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 2ae8ef6a578..a4e14ba7c32 100644 --- a/admin/spec/spec_helper.rb +++ b/admin/spec/spec_helper.rb @@ -14,6 +14,7 @@ SimpleCov.merge_timeout(3600) SimpleCov.start("rails") do add_filter "/shared_examples/" + add_filter "/testing_support/feature_helpers" end end From 0b33e52062ec23606dd8e805e474abb50844fceb Mon Sep 17 00:00:00 2001 From: benjamin wil Date: Fri, 11 Sep 2026 20:46:29 -0700 Subject: [PATCH 17/19] Disambiguate `` in product feature tests These tests were failing due to there being two dialogs in the DOM. We can disambiguate using the heading text of the dialog. --- admin/spec/features/product_spec.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/admin/spec/features/product_spec.rb b/admin/spec/features/product_spec.rb index a4b707813b2..9a6d0e66121 100644 --- a/admin/spec/features/product_spec.rb +++ b/admin/spec/features/product_spec.rb @@ -168,7 +168,7 @@ click_on "Add new category" expect(page).to have_content("New Category") - within(dialog) do + within(dialog(text: "New Category")) do fill_in "Name", with: "Jackets" solidus_select "Apparel", from: "Parent Category" click_on "Add Category" @@ -181,13 +181,17 @@ end context "with invalid attributes" do + let(:new_category_dialog) { dialog text: "New Category" } context "with blank name" do it "shows error" do visit "/admin/products/just-a-prod" click_on "Add new category" - within(dialog) { click_on "Add Category" } - expect(dialog).to have_content("can't be blank") + within new_category_dialog do + click_on "Add Category" + end + + expect(new_category_dialog).to have_content("can't be blank") end end @@ -195,13 +199,15 @@ it "shows error" do visit "/admin/products/just-a-prod" click_on "Add new category" - within(dialog) do + + within new_category_dialog do fill_in "Name", with: child_taxon.name solidus_select "Apparel", from: "Parent Category" click_on "Add Category" end - expect(dialog).to have_content("must be unique under the same parent Taxon") + expect(new_category_dialog) + .to have_content("must be unique under the same parent Taxon") end end end From 8cf131c76a79fe071ef5aa059315eb0cfa24d078 Mon Sep 17 00:00:00 2001 From: benjamin wil Date: Fri, 11 Sep 2026 20:48:36 -0700 Subject: [PATCH 18/19] Fix bug where taxon_ids would not be updated This change more closely mirrors how the working form for product option type IDs works. Feature tests related to adding taxons were failing, and this change resolves the issue. --- .../components/solidus_admin/products/show/component.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/app/components/solidus_admin/products/show/component.html.erb b/admin/app/components/solidus_admin/products/show/component.html.erb index 58d489f2e24..62804b80c6b 100644 --- a/admin/app/components/solidus_admin/products/show/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/component.html.erb @@ -146,7 +146,8 @@ <% end %> <%= render component("ui/panel").new(title: t(".product_organization")) do |panel| %> - <%= f.hidden_field :taxon_ids, multiple: true, value: nil %> + <%= hidden_field_tag "#{f.object_name}[taxon_ids][]", nil %> + <%= render component("turbo/target_frame").new(:product_organization_frame, source: :new_product_category) do %> <%= render component("ui/forms/field").select( f, From d9c7d1a8e531807b7f1ec61147fe41844517ae1a Mon Sep 17 00:00:00 2001 From: benjamin wil Date: Fri, 11 Sep 2026 20:57:42 -0700 Subject: [PATCH 19/19] Provide latest context re: FIXME in feature test I have verified that this is still an issue. I have updated the comment so nobody thinks it's stale (because of the reference to Circle CI). --- admin/spec/features/product_spec.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/admin/spec/features/product_spec.rb b/admin/spec/features/product_spec.rb index 9a6d0e66121..3fc4ff8806d 100644 --- a/admin/spec/features/product_spec.rb +++ b/admin/spec/features/product_spec.rb @@ -77,8 +77,15 @@ visit "/admin/products/just-a-prod" solidus_select(%w[clothing-size:Size clothing-color:Color], from: "Option Types") options_panel = panel(title: "Options") - # for some reason capybara on circle ci does not register a form submit when clicking "Save" within options panel, - # so we have to resort to Save button in the header + + # FIXME: There is some kind of minor issue with this form that prevents + # the headless browser running in CI from successfully clicking "Save" + # within the options panel: + # + # within(options_panel) { click_on "Save" } # Doesn't work. :-( + # + # If you can figure out why, please resolve this FIXME! Until then, + # we can save using the button in the header. within("header") { click_on "Save" } expect(options_panel).to have_content("clothing-size:Size")