diff --git a/admin/app/components/solidus_admin/base_component.rb b/admin/app/components/solidus_admin/base_component.rb
index e39c1707a4b..de18ea8859e 100644
--- a/admin/app/components/solidus_admin/base_component.rb
+++ b/admin/app/components/solidus_admin/base_component.rb
@@ -9,7 +9,9 @@ class BaseComponent < ViewComponent::Base
include SolidusAdmin::ComponentsHelper
include SolidusAdmin::StimulusHelper
include SolidusAdmin::VoidElementsHelper
- include Turbo::FramesHelper
+ include SolidusAdmin::FlashHelper
+ 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/layout/flashes/toasts/component.html.erb b/admin/app/components/solidus_admin/layout/flashes/toasts/component.html.erb
new file mode 100644
index 00000000000..14ea6c06657
--- /dev/null
+++ b/admin/app/components/solidus_admin/layout/flashes/toasts/component.html.erb
@@ -0,0 +1,5 @@
+
+ <% toasts.each do |key, message| %>
+ <%= render component("ui/toast").new(text: message, scheme: key.to_sym == :error ? :error : :default) %>
+ <% end %>
+
diff --git a/admin/app/components/solidus_admin/layout/flashes/toasts/component.rb b/admin/app/components/solidus_admin/layout/flashes/toasts/component.rb
new file mode 100644
index 00000000000..3aecd62d0a0
--- /dev/null
+++ b/admin/app/components/solidus_admin/layout/flashes/toasts/component.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class SolidusAdmin::Layout::Flashes::Toasts::Component < SolidusAdmin::BaseComponent
+ attr_reader :toasts
+
+ def initialize(toasts:)
+ @toasts = toasts
+ end
+end
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..835105c8def
--- /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, include_blank: t(".none")) %>
+ <%= render component("ui/forms/field").text_area(f, :description) %>
+
+ <% end %>
+ <% modal.with_actions do %>
+
+ <%= 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..a60bfd51937
--- /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, { data: { item_label: name } }]
+ 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..6b3ae093e25
--- /dev/null
+++ b/admin/app/components/solidus_admin/products/show/categories/new/component.yml
@@ -0,0 +1,5 @@
+en:
+ cancel: "Cancel"
+ none: "None"
+ submit: "Add Category"
+ title: "New Category"
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 f8095763f04..4b7d3b4b6f4 100644
--- a/admin/app/components/solidus_admin/products/show/component.html.erb
+++ b/admin/app/components/solidus_admin/products/show/component.html.erb
@@ -83,13 +83,48 @@
) %>
<% end %>
- <%= render component("ui/panel").new(title: t(".options")) do %>
- <%= render component("ui/forms/field").select(
- f,
- :option_type_ids,
- option_type_options,
- multiple: true,
- "size" => option_type_options.size,
+ <%= render component("ui/panel").new(title: t(".options")) do |panel| %>
+ <% if @product.product_option_types.present? %>
+ <% panel.with_section do %>
+
+ <% @product.product_option_types.includes(option_type: :option_values).order(:position).each do |product_option| %>
+
>
+
+
+ <%= render component("ui/icon").new(name: "draggable", class: "w-6 h-6 cursor-grab handle fill-gray-500") %>
+
+
+
<%= product_option.name %>:<%= product_option.presentation %>
+
+ <% product_option.option_values.each do |value| %>
+ <%= render component("ui/badge").new(name: "#{value.name}:#{value.presentation}") %>
+ <% end %>
+
+
+
+
+ <%= render component("ui/button").new(tag: :a, href: spree.edit_admin_option_type_path(product_option.option_type), scheme: :secondary, text: t(".edit")) %>
+
+
+ <% end %>
+
+ <% end %>
+ <% end %>
+
+
+ <%= hidden_field_tag "#{f.object_name}[option_type_ids][]", nil %>
+ <%= render component("ui/forms/field").select(
+ f,
+ :option_type_ids,
+ option_type_options,
+ multiple: true
+ ) %>
+ <%= render component("ui/button").new(type: :submit, text: t(".save")) %>
+
+
+ <% panel.with_action(
+ name: t(".manage_options"),
+ href: solidus_admin.option_types_path
) %>
<% end %>
@@ -123,13 +158,20 @@
<% end %>
<% end %>
- <%= render component("ui/panel").new(title: t(".product_organization")) do %>
- <%= render component("ui/forms/field").select(
- f,
- :taxon_ids,
- taxon_options,
- multiple: true,
- "size" => taxon_options.size, # use a string key to avoid setting the size of the component
+ <%= render component("ui/panel").new(title: t(".product_organization")) do |panel| %>
+ <%= f.hidden_field :taxon_ids, multiple: true, value: nil %>
+ <%= 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
+ ) %>
+ <% 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 %>
@@ -151,3 +193,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.rb b/admin/app/components/solidus_admin/products/show/component.rb
index 66ae89250fd..f07d66b9cdd 100644
--- a/admin/app/components/solidus_admin/products/show/component.rb
+++ b/admin/app/components/solidus_admin/products/show/component.rb
@@ -16,13 +16,13 @@ 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
def option_type_options
@option_type_options ||= Spree::OptionType.order(:presentation).pluck(:presentation, :name, :id).map do
- ["#{_1} (#{_2})", _3]
+ ["#{_2}:#{_1}", _3]
end
end
diff --git a/admin/app/components/solidus_admin/products/show/component.yml b/admin/app/components/solidus_admin/products/show/component.yml
index 58a100b8332..1c992f78635 100644
--- a/admin/app/components/solidus_admin/products/show/component.yml
+++ b/admin/app/components/solidus_admin/products/show/component.yml
@@ -1,11 +1,18 @@
en:
- save: "Save"
+ add_new_category: "Add new category"
back: "Back"
- duplicate: "Duplicate"
- view: "View online"
delete: "Delete"
delete_confirmation: "Are you sure you want to delete this product?"
+ duplicate: "Duplicate"
+ edit: "Edit"
+ hints:
+ available_on_html: "Product availability starts from the set date.
Empty date indicates no availability."
+ discontinue_on_html: "Product availability ends from the set date.
Empty date indicates continuous availability."
+ promotionable_html: "Promotions can apply to this product"
+ shipping_category_html: "Manage Shipping in Settings"
+ tax_category_html: "Manage Taxes in Settings"
manage_images: "Manage images"
+ manage_options: "Manage option types"
manage_properties: "Manage product specifications"
manage_stock: "Manage stock"
media: "Media"
@@ -14,13 +21,9 @@ en:
pricing: "Pricing"
product_organization: "Product organization"
publishing: "Publishing"
+ save: "Save"
seo: "SEO"
stock: "Stock"
shipping: "Shipping"
specifications: "Specifications"
- hints:
- available_on_html: "Product availability starts from the set date.
Empty date indicates no availability."
- discontinue_on_html: "Product availability ends from the set date.
Empty date indicates continuous availability."
- promotionable_html: "Promotions can apply to this product"
- shipping_category_html: "Manage Shipping in Settings"
- tax_category_html: "Manage Taxes in Settings"
+ view: "View online"
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
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 349e2429d2a..23a7413bdb9 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 [String] :"data-option-value-field"
# @option attributes [String] :"data-option-label-field" when +:src+ param is passed, value and label of loaded options
diff --git a/admin/app/controllers/solidus_admin/base_controller.rb b/admin/app/controllers/solidus_admin/base_controller.rb
index bd7442f4eee..8854547389a 100644
--- a/admin/app/controllers/solidus_admin/base_controller.rb
+++ b/admin/app/controllers/solidus_admin/base_controller.rb
@@ -19,6 +19,7 @@ class BaseController < ApplicationController
helper 'solidus_admin/components'
helper 'solidus_admin/layout'
+ helper 'solidus_admin/flash'
private
diff --git a/admin/app/controllers/solidus_admin/product_option_types_controller.rb b/admin/app/controllers/solidus_admin/product_option_types_controller.rb
new file mode 100644
index 00000000000..640ef324d5b
--- /dev/null
+++ b/admin/app/controllers/solidus_admin/product_option_types_controller.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class SolidusAdmin::ProductOptionTypesController < SolidusAdmin::BaseController
+ include SolidusAdmin::Moveable
+end
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..f33cc6b14d8
--- /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/app/controllers/solidus_admin/products_controller.rb b/admin/app/controllers/solidus_admin/products_controller.rb
index e5f22336ed3..bb5822813f8 100644
--- a/admin/app/controllers/solidus_admin/products_controller.rb
+++ b/admin/app/controllers/solidus_admin/products_controller.rb
@@ -43,11 +43,7 @@ def update
@product = Spree::Product.friendly.find(params[:id])
if @product.update(product_params)
- flash[:success] = t('spree.successfully_updated', resource: [
- Spree::Product.model_name.human,
- @product.name.inspect,
- ].join(' '))
-
+ flash[:success] = t('.success')
redirect_to action: :show, status: :see_other
else
flash.now[:error] = @product.errors.full_messages.join(", ")
diff --git a/admin/app/helpers/solidus_admin/flash_helper.rb b/admin/app/helpers/solidus_admin/flash_helper.rb
new file mode 100644
index 00000000000..d25ee53023a
--- /dev/null
+++ b/admin/app/helpers/solidus_admin/flash_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module SolidusAdmin
+ module FlashHelper
+ def toasts
+ flash.to_hash.with_indifferent_access.except(:alert)
+ end
+ end
+end
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/app/views/layouts/solidus_admin/application.html.erb b/admin/app/views/layouts/solidus_admin/application.html.erb
index 6755eea3c1d..9de687890a4 100644
--- a/admin/app/views/layouts/solidus_admin/application.html.erb
+++ b/admin/app/views/layouts/solidus_admin/application.html.erb
@@ -31,10 +31,6 @@
-
- <% flash.each do |key, message| %>
- <%= render component("ui/toast").new(text: message, scheme: key.to_sym == :error ? :error : :default) %>
- <% end %>
-
+ <%= render component("layout/flashes/toasts").new(toasts:) %>