Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -10,7 +10,9 @@ class BaseComponent < ViewComponent::Base
include SolidusAdmin::StimulusHelper
include SolidusAdmin::VoidElementsHelper
include SolidusAdmin::SolidusFormHelper
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,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
Comment thread
tvdeyen marked this conversation as resolved.
["#{"    " * 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 @@ -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| %>
<%= 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,
: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 %>
<% end %>
Expand All @@ -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 %>
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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?"
Expand Down
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 [nil, String, Integer, Array<String, Integer>] :value which option should be selected
# @option attributes [String] :"data-option-value-field"
Expand Down
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 @@ -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
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/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
10 changes: 10 additions & 0 deletions admin/lib/solidus_admin/testing_support/feature_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
value: "1"
) %>
</div>

<div class="mb-8">
<h6 class="text-gray-500 mb-3 mt-0">Alternative item label</h6>
<%= 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"
) %>
</div>
</div>

<div class="flex flex-col flex-grow gap-2">
Expand Down
100 changes: 95 additions & 5 deletions admin/spec/features/product_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -100,9 +107,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"
Expand Down Expand Up @@ -130,4 +135,89 @@
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(text: "New Category")) 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
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 new_category_dialog do
click_on "Add Category"
end

expect(new_category_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 new_category_dialog do
fill_in "Name", with: child_taxon.name
solidus_select "Apparel", from: "Parent Category"
click_on "Add Category"
end

expect(new_category_dialog)
.to have_content("must be unique under the same parent Taxon")
end
end
end
end
end
end
Loading
Loading