Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ecbcfb2
Change flash on #update success
chaimann Jun 9, 2025
cae0196
Create product_option_types_controller.rb
chaimann Jun 9, 2025
cf6cf64
Delegate methods
chaimann Jun 9, 2025
4350be5
Reorder translations alphabetically
chaimann Jun 9, 2025
f81cc33
Add correct label translation
chaimann Jun 9, 2025
0f4a151
Display list of product option types
chaimann Jun 9, 2025
cd822f1
Allow saving selected option types
chaimann Jun 9, 2025
7a5071d
Change display text for option types in select field
chaimann Jun 9, 2025
7237857
Add panel action to options
chaimann Jun 9, 2025
176c779
Add feature helpers
chaimann Jun 9, 2025
a6d9d00
Add feature specs
chaimann Jun 9, 2025
46780ee
Fix bad copy/paste
chaimann Jun 9, 2025
d837420
Add request specs
chaimann Jun 9, 2025
fd8444b
Update shared examples for sorting
chaimann Jun 10, 2025
0e78e0e
Add feature tests for sorting
chaimann Jun 10, 2025
0f92f37
Update test to pass on CI
chaimann Jun 10, 2025
97c13ec
Skip pending test
chaimann Jun 10, 2025
513ba03
!dropme
chaimann Jun 16, 2025
07bff0e
Remove redundant attribute
chaimann Jun 11, 2025
0c20818
Add correct label translation
chaimann Jun 11, 2025
d58c3d2
Create product_taxons resource
chaimann Jun 16, 2025
80a793b
Create component for new product category
chaimann Jun 16, 2025
b9f4c12
Allow alternative item labels in solidus_select
chaimann Jun 16, 2025
587ebe0
Change selected items text in taxons select
chaimann Jun 16, 2025
142b02f
Update product organization panel
chaimann Jun 16, 2025
367edfe
Display toasts from turbo frame response
chaimann Jun 16, 2025
4c1e371
Extract frame with stream actions into component
chaimann Jun 16, 2025
16304cb
Add feature helper
chaimann Jun 17, 2025
518ca28
Move capybara feature helpers
chaimann Jun 17, 2025
2d19c9e
Add feature tests
chaimann Jun 17, 2025
a855d22
Remove invalid_attributes declaration
chaimann Jun 17, 2025
ef61587
Add request tests
chaimann Jun 17, 2025
b1e8970
Fix rubocop offence
chaimann Jun 25, 2025
cbd793c
Allow empty parent for new category
chaimann Jun 27, 2025
84b72e1
Exclude feature_helpers from coverage report
chaimann Jun 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion admin/app/components/solidus_admin/base_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div id="flash_toasts" class="fixed inset-x-0 bottom-3 flex items-center justify-center flex-col gap-3 pointer-events-none" role="alert">
<% toasts.each do |key, message| %>
<%= render component("ui/toast").new(text: message, scheme: key.to_sym == :error ? :error : :default) %>
<% end %>
</div>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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| %>
<div class="flex flex-col gap-6 pb-4">
<%= 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) %>
</div>
<% end %>
<% modal.with_actions do %>
<form method="dialog">
<%= render component("ui/button").new(scheme: :secondary, text: t('.cancel')) %>
</form>
<%= render component("ui/button").new(type: :submit, text: t('.submit'), form: dom_id(@taxon)) %>
<% end %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
en:
cancel: "Cancel"
none: "None"
submit: "Add Category"
title: "New Category"
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>
<div class="flex flex-col gap-4" data-controller="sortable" data-sortable-handle-value=".handle">
<% @product.product_option_types.includes(option_type: :option_values).order(:position).each do |product_option| %>
<div class="flex gap-2 justify-between" data-sortable-url=<%= solidus_admin.move_product_option_type_path(product_option) %>>
<div class="flex gap-2">
<div class="flex items-center">
<%= render component("ui/icon").new(name: "draggable", class: "w-6 h-6 cursor-grab handle fill-gray-500") %>
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold text-sm"><%= product_option.name %>:<%= product_option.presentation %></span>
<div class="flex gap-2 flex-wrap">
<% product_option.option_values.each do |value| %>
<%= render component("ui/badge").new(name: "#{value.name}:#{value.presentation}") %>
<% end %>
</div>
</div>
</div>
<div class="flex items-center">
<%= render component("ui/button").new(tag: :a, href: spree.edit_admin_option_type_path(product_option.option_type), scheme: :secondary, text: t(".edit")) %>
</div>
</div>
<% end %>
</div>
<% end %>
<% end %>

<div class="flex gap-4 justify-between items-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")) %>
</div>

<% panel.with_action(
name: t(".manage_options"),
href: solidus_admin.option_types_path
) %>
<% end %>

Expand Down Expand Up @@ -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 %>
Expand All @@ -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 %>
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 12 additions & 9 deletions admin/app/components/solidus_admin/products/show/component.yml
Original file line number Diff line number Diff line change
@@ -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.<br> Empty date indicates no availability."
discontinue_on_html: "Product availability ends from the set date.<br> 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"
Expand All @@ -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.<br> Empty date indicates no availability."
discontinue_on_html: "Product availability ends from the set date.<br> 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"
Original file line number Diff line number Diff line change
@@ -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 %>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class SolidusAdmin::UI::Forms::Select::Component < SolidusAdmin::BaseComponent
# @param choices [Array<String>, Array<Array<String>>] 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: <alternative_label_text> } }` 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
Expand Down
1 change: 1 addition & 0 deletions admin/app/controllers/solidus_admin/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class BaseController < ApplicationController

helper 'solidus_admin/components'
helper 'solidus_admin/layout'
helper 'solidus_admin/flash'

private

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class SolidusAdmin::ProductOptionTypesController < SolidusAdmin::BaseController
include SolidusAdmin::Moveable
end
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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(", ")
Expand Down
9 changes: 9 additions & 0 deletions admin/app/helpers/solidus_admin/flash_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class SolidusSelect extends HTMLSelectElement {
allowEmptyOption: true,
maxOptions: null,
refreshThrottle: 0,
itemLabelField: "itemLabel",
plugins: {
no_active_items: true,
remove_button: {
Expand All @@ -61,6 +62,10 @@ class SolidusSelect extends HTMLSelectElement {
const message = this.input.getAttribute("data-no-results-message");
return `<div class='no-results'>${message}</div>`;
},
item: function(data, escape) {
const itemLabel = data[this.settings.itemLabelField] || data[this.settings.labelField];
return `<div>${escape(itemLabel)}</div>`;
}
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@
</main>
</div>

<div class="fixed inset-x-0 bottom-3 flex items-center justify-center flex-col gap-3 pointer-events-none" role="alert">
<% flash.each do |key, message| %>
<%= render component("ui/toast").new(text: message, scheme: key.to_sym == :error ? :error : :default) %>
<% end %>
</div>
<%= render component("layout/flashes/toasts").new(toasts:) %>
</body>
</html>
6 changes: 6 additions & 0 deletions admin/config/locales/product_taxons.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
en:
solidus_admin:
product_taxons:
title: "Categories"
create:
success: "Product category was successfully added."
2 changes: 2 additions & 0 deletions admin/config/locales/products.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ en:
success: "Products were successfully discontinued."
activate:
success: "Products were successfully activated."
update:
success: "Product was successfully updated."
3 changes: 3 additions & 0 deletions admin/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -87,4 +89,5 @@
admin_resources :roles, except: [:show]
admin_resources :adjustment_reasons, except: [:show]
admin_resources :store_credit_reasons, except: [:show]
admin_resources :product_option_types, only: [], sortable: true
end
Loading