diff --git a/admin/app/components/solidus_admin/calculators/form/component.html.erb b/admin/app/components/solidus_admin/calculators/form/component.html.erb new file mode 100644 index 00000000000..332fb711497 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/form/component.html.erb @@ -0,0 +1,44 @@ +
+
+
+ <%= render component("ui/forms/field").select( + @form.object_name, + :calculator_type, + @calculators.map { |c| [c.description, c.name] }, + object: @form.object, + value: @form.object.calculator_type, + label: 'Calculator', + class: "fullwidth", + data: { + "calculators--form-target": "select", + action: "change->calculators--form#toggle" + } + ) %> +
+ + <% @calculators.each do |calculator_class| %> + <% calculator = @form.object.calculator.class == calculator_class ? @form.object.calculator : calculator_class.new %> +
+ <%= @form.fields_for :calculator, calculator do |calculator_form| %> + <% calculator.admin_form_preference_names.each do |name| %> +
+ <%= render component("calculators/preference_fields/#{calculator.preference_type(name)}").new( + form: calculator_form, + attribute: "preferred_#{name}", + label: t(name, scope: 'spree', default: name.to_s.humanize) + ) %> +
+ <% end %> + <% end %> +
+ <% end %> +
+
diff --git a/admin/app/components/solidus_admin/calculators/form/component.js b/admin/app/components/solidus_admin/calculators/form/component.js new file mode 100644 index 00000000000..0991903af13 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/form/component.js @@ -0,0 +1,23 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["select", "preferences"] + + connect() { + this.toggle() + } + + toggle() { + const selectedType = this.selectTarget.value + + this.preferencesTargets.forEach((el) => { + const isActive = el.dataset.calculatorType === selectedType + + el.classList.toggle("hidden", !isActive) + + el.querySelectorAll("input, select, textarea, button").forEach(input => { + input.disabled = !isActive + }) + }) + } +} diff --git a/admin/app/components/solidus_admin/calculators/form/component.rb b/admin/app/components/solidus_admin/calculators/form/component.rb new file mode 100644 index 00000000000..5b76cff9144 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/form/component.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class SolidusAdmin::Calculators::Form::Component < SolidusAdmin::BaseComponent + def initialize(form:, calculators:) + @form = form + @calculators = calculators + end +end diff --git a/admin/app/components/solidus_admin/calculators/preference_fields/decimal/component.html.erb b/admin/app/components/solidus_admin/calculators/preference_fields/decimal/component.html.erb new file mode 100644 index 00000000000..ce5574f38d5 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/preference_fields/decimal/component.html.erb @@ -0,0 +1,7 @@ +<%= render component("ui/forms/field").text_field( + @form, + @attribute, + type: :number, + step: '0.01', + class: 'fullwidth' +) %> diff --git a/admin/app/components/solidus_admin/calculators/preference_fields/decimal/component.rb b/admin/app/components/solidus_admin/calculators/preference_fields/decimal/component.rb new file mode 100644 index 00000000000..f90b2b58da4 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/preference_fields/decimal/component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SolidusAdmin::Calculators::PreferenceFields::Decimal::Component < SolidusAdmin::BaseComponent + def initialize(form:, attribute:, label:) + @form = form + @attribute = attribute + @label = label + end +end diff --git a/admin/app/components/solidus_admin/calculators/preference_fields/integer/component.html.erb b/admin/app/components/solidus_admin/calculators/preference_fields/integer/component.html.erb new file mode 100644 index 00000000000..29d84b394ff --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/preference_fields/integer/component.html.erb @@ -0,0 +1,7 @@ +<%= render component("ui/forms/field").text_field( + @form, + @attribute, + type: :number, + step: '1', + class: 'fullwidth' +) %> diff --git a/admin/app/components/solidus_admin/calculators/preference_fields/integer/component.rb b/admin/app/components/solidus_admin/calculators/preference_fields/integer/component.rb new file mode 100644 index 00000000000..118a168b8ad --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/preference_fields/integer/component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SolidusAdmin::Calculators::PreferenceFields::Integer::Component < SolidusAdmin::BaseComponent + def initialize(form:, attribute:, label:) + @form = form + @attribute = attribute + @label = label + end +end diff --git a/admin/app/components/solidus_admin/calculators/preference_fields/string/component.html.erb b/admin/app/components/solidus_admin/calculators/preference_fields/string/component.html.erb new file mode 100644 index 00000000000..574b8952ca9 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/preference_fields/string/component.html.erb @@ -0,0 +1,5 @@ +<%= render component("ui/forms/field").text_field( + @form, + @attribute, + class: 'fullwidth' +) %> diff --git a/admin/app/components/solidus_admin/calculators/preference_fields/string/component.rb b/admin/app/components/solidus_admin/calculators/preference_fields/string/component.rb new file mode 100644 index 00000000000..4ee7c9ddcd1 --- /dev/null +++ b/admin/app/components/solidus_admin/calculators/preference_fields/string/component.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class SolidusAdmin::Calculators::PreferenceFields::String::Component < SolidusAdmin::BaseComponent + def initialize(form:, attribute:, label:) + @form = form + @attribute = attribute + @label = label + end +end diff --git a/admin/app/components/solidus_admin/shipping_methods/edit/component.html.erb b/admin/app/components/solidus_admin/shipping_methods/edit/component.html.erb new file mode 100644 index 00000000000..efd30b50033 --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/edit/component.html.erb @@ -0,0 +1,16 @@ +<%= page do %> + <%= page_header do %> + <%= page_header_back(back_url) %> + <%= page_header_title(@shipping_method.name) %> + <%= page_header_actions do %> + <%= render component("ui/button").new( + tag: :a, + text: t(".discard"), + scheme: :secondary, + href: back_url + ) %> + <%= render component("ui/button").new(text: t(".save"), form: form_id) %> + <% end %> + <% end %> + <%= render component("shipping_methods/form").new(shipping_method: @shipping_method, url: form_url, id: form_id) %> +<% end %> diff --git a/admin/app/components/solidus_admin/shipping_methods/edit/component.rb b/admin/app/components/solidus_admin/shipping_methods/edit/component.rb new file mode 100644 index 00000000000..42398a1c5c3 --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/edit/component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SolidusAdmin::ShippingMethods::Edit::Component < SolidusAdmin::Resources::Edit::Component + include SolidusAdmin::Layout::PageHelpers +end diff --git a/admin/app/components/solidus_admin/shipping_methods/edit/component.yml b/admin/app/components/solidus_admin/shipping_methods/edit/component.yml new file mode 100644 index 00000000000..a30790a2faa --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/edit/component.yml @@ -0,0 +1,6 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + back: Back + discard: Discard + save: Save diff --git a/admin/app/components/solidus_admin/shipping_methods/form/component.html.erb b/admin/app/components/solidus_admin/shipping_methods/form/component.html.erb new file mode 100644 index 00000000000..76a94f66e6b --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/form/component.html.erb @@ -0,0 +1,139 @@ +<%= form_for @shipping_method, url: @url, html: { id: @id } do |f| %> + <%= page_with_sidebar do %> + <%= page_with_sidebar_main do %> + <%= render component("ui/panel").new(title: t(".details")) do |panel| %> + <% panel.with_section(class: "flex flex-col gap-4") do %> +
+ <%= render component("ui/forms/field").text_field(f, :name) %> + <%= render component("ui/forms/field").text_field(f, :admin_name) %> +
+
+ <%= render component("ui/forms/field").text_field(f, :code) %> + <%= render component("ui/forms/field").text_field(f, :carrier) %> + <%= render component("ui/forms/field").text_field(f, :service_level) %> +
+ <%= render component("ui/forms/field").text_field( + f, + :tracking_url, + placeholder: t('spree.tracking_url_placeholder') + ) %> + <% end %> + <% end %> + + <%= render component("ui/panel").new(title: t('.pricing')) do |panel| %> + <% panel.with_section do %> + <%= render component("calculators/form").new( + form: f, + calculators: @calculators + ) %> + <% end %> + <% end %> + + <%= render component("ui/panel").new(title: t(".taxation")) do |panel| %> + <% panel.with_section do %> + <%= render component("ui/forms/field").select( + f.object_name, + :tax_category_id, + Spree::TaxCategory.all.map { |tc| [tc.name, tc.id] }, + object: @shipping_method, + value: @shipping_method.tax_category_id, + class: "fullwidth" + ) %> + <% end %> + <% end %> + <% end %> + + <%= page_with_sidebar_aside do %> + <%= render component("ui/panel").new(title: t('.availability')) do |panel| %> + <% panel.with_section do %> +
+ <%= hidden_field_tag "shipping_method[store_ids][]", "" %> + <%= render component("ui/forms/field").select( + f.object_name, + :store_ids, + Spree::Store.all.map { |tc| [tc.name, tc.id] }, + object: @shipping_method, + value: @shipping_method.store_ids, + class: "fullwidth", + multiple: true, + label: t(".stores") + ) %> + + <%= hidden_field_tag "shipping_method[zone_ids][]", "" %> + <%= render component("ui/forms/field").select( + f.object_name, + :zone_ids, + Spree::Zone.all.map { |tc| [tc.name, tc.id] }, + object: @shipping_method, + value: @shipping_method.zone_ids, + class: "fullwidth", + multiple: true, + label: t(".zones") + ) %> + +
+ + +
+ <%= hidden_field_tag "shipping_method[stock_location_ids][]", "" %> + <%= render component("ui/forms/field").select( + f.object_name, + :stock_location_ids, + Spree::StockLocation.order_default.map { |tc| [tc.name, tc.id] }, + object: @shipping_method, + value: @shipping_method.stock_location_ids, + class: "fullwidth", + multiple: true, + label: nil, + placeholder: t('.stock_locations_placeholder') + ) %> +
+
+ + +
+ <% end %> + <% end %> + + <%= render component("ui/panel").new(title: t(".organization")) do |panel| %> + <% panel.with_section do %> + <%= hidden_field_tag "shipping_method[shipping_category_ids][]", "" %> + <%= render component("ui/forms/field").select( + f.object_name, + :shipping_category_ids, + Spree::ShippingCategory.all.map { |tc| [tc.name, tc.id] }, + object: @shipping_method, + value: @shipping_method.shipping_category_ids, + class: "fullwidth", + multiple: true, + label: t(".shipping_categories") + ) %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/admin/app/components/solidus_admin/shipping_methods/form/component.js b/admin/app/components/solidus_admin/shipping_methods/form/component.js new file mode 100644 index 00000000000..5e7d735061a --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/form/component.js @@ -0,0 +1,17 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["stockLocationCheckbox", "stockLocations"] + + connect() { + this.toggle(); + } + + toggle() { + if (this.stockLocationCheckboxTarget.checked) { + this.stockLocationsTarget.classList.add("hidden"); + } else { + this.stockLocationsTarget.classList.remove("hidden"); + } + } +} diff --git a/admin/app/components/solidus_admin/shipping_methods/form/component.rb b/admin/app/components/solidus_admin/shipping_methods/form/component.rb new file mode 100644 index 00000000000..0baa9a53d0a --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/form/component.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SolidusAdmin::ShippingMethods::Form::Component < SolidusAdmin::BaseComponent + include SolidusAdmin::Layout::PageHelpers + + def initialize(shipping_method:, id:, url:) + @shipping_method = shipping_method + @id = id + @url = url + @calculators = Rails.application.config.spree.calculators.shipping_methods + end +end diff --git a/admin/app/components/solidus_admin/shipping_methods/form/component.yml b/admin/app/components/solidus_admin/shipping_methods/form/component.yml new file mode 100644 index 00000000000..94425f3d5ef --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/form/component.yml @@ -0,0 +1,16 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + details: "Details" + zones: "Zones" + shipping_categories: "Shipping Categories" + tax_category: "Tax Category" + availability: "Availability" + stock_locations: "Stock Locations" + stores: "Stores" + Calculator: "Calculator" + stock_locations_placeholder: "Choose stock locations" + organization: "Organization" + taxation: "Taxation" + pricing: "Pricing" + diff --git a/admin/app/components/solidus_admin/shipping_methods/index/component.rb b/admin/app/components/solidus_admin/shipping_methods/index/component.rb index 69a3f3fbe3f..0c8355f73b4 100644 --- a/admin/app/components/solidus_admin/shipping_methods/index/component.rb +++ b/admin/app/components/solidus_admin/shipping_methods/index/component.rb @@ -5,8 +5,8 @@ def model_class Spree::ShippingMethod end - def row_url(shipping_method) - spree.edit_admin_shipping_method_path(shipping_method) + def edit_path(shipping_method) + solidus_admin.edit_shipping_method_path(shipping_method) end def search_url @@ -21,7 +21,7 @@ def page_actions render component("ui/button").new( tag: :a, text: t('.add'), - href: spree.new_admin_shipping_method_path, + href: solidus_admin.new_shipping_method_path, icon: "add-line", class: "align-self-end w-full", ) @@ -40,22 +40,49 @@ def batch_actions def columns [ - { - header: :name, - data: -> { [_1.admin_name.presence, _1.name].compact.join(' / ') }, - }, - { - header: :zone, - data: -> { _1.zones.pluck(:name).to_sentence }, - }, - { - header: :calculator, - data: -> { _1.calculator&.description }, - }, - { - header: :available_to_users, - data: -> { _1.available_to_users? ? component('ui/badge').yes : component('ui/badge').no }, - }, + name_column, + zone_column, + calculator_column, + available_to_users_column, ] end + + private + + def name_column + { + header: :name, + data: ->(shipping_method) do + name = [shipping_method.admin_name.presence, shipping_method.name].compact.join(' / ') + link_to name, edit_path(shipping_method), class: 'body-link' + end + } + end + + def zone_column + { + header: :zone, + data: ->(shipping_method) do + shipping_method.zones.pluck(:name).to_sentence + end + } + end + + def calculator_column + { + header: :calculator, + data: ->(shipping_method) do + shipping_method.calculator&.description + end + } + end + + def available_to_users_column + { + header: :available_to_users, + data: ->(shipping_method) do + shipping_method.available_to_users? ? component('ui/badge').yes : component('ui/badge').no + end + } + end end diff --git a/admin/app/components/solidus_admin/shipping_methods/new/component.html.erb b/admin/app/components/solidus_admin/shipping_methods/new/component.html.erb new file mode 100644 index 00000000000..874f4191fdd --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/new/component.html.erb @@ -0,0 +1,11 @@ +<%= page do %> + <%= page_header do %> + <%= page_header_back(back_url) %> + <%= page_header_title(t('.title')) %> + <%= page_header_actions do %> + <%= render component("ui/button").new(tag: :a, text: t(".discard"), scheme: :secondary, href: back_url) %> + <%= render component("ui/button").new(text: t(".save"), form: form_id) %> + <% end %> + <% end %> + <%= render component("shipping_methods/form").new(shipping_method: @shipping_method, url: form_url, id: form_id) %> +<% end %> diff --git a/admin/app/components/solidus_admin/shipping_methods/new/component.rb b/admin/app/components/solidus_admin/shipping_methods/new/component.rb new file mode 100644 index 00000000000..37a18a7a981 --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/new/component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class SolidusAdmin::ShippingMethods::New::Component < SolidusAdmin::Resources::New::Component + include SolidusAdmin::Layout::PageHelpers +end diff --git a/admin/app/components/solidus_admin/shipping_methods/new/component.yml b/admin/app/components/solidus_admin/shipping_methods/new/component.yml new file mode 100644 index 00000000000..57dd26e3c5b --- /dev/null +++ b/admin/app/components/solidus_admin/shipping_methods/new/component.yml @@ -0,0 +1,7 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + title: "New Shipping Method" + discard: "Discard" + save: "Save" + back: Back diff --git a/admin/app/components/solidus_admin/ui/forms/field/component.rb b/admin/app/components/solidus_admin/ui/forms/field/component.rb index 3bfaf0b1045..31cfa4e5973 100644 --- a/admin/app/components/solidus_admin/ui/forms/field/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/field/component.rb @@ -41,7 +41,7 @@ def self.select(form, method, choices, object: nil, hint: nil, tip: nil, size: : hint:, tip:, size: size, - name: "#{object_name}[#{method}]", + name: "#{object_name}[#{method}]#{'[]' if attributes[:multiple].present?}", choices:, value: (object.public_send(method) if object.respond_to?(method)), error: (errors.to_sentence.capitalize if errors), diff --git a/admin/app/controllers/solidus_admin/shipping_methods_controller.rb b/admin/app/controllers/solidus_admin/shipping_methods_controller.rb index 6b079943e29..7552c47cf52 100644 --- a/admin/app/controllers/solidus_admin/shipping_methods_controller.rb +++ b/admin/app/controllers/solidus_admin/shipping_methods_controller.rb @@ -1,35 +1,42 @@ # frozen_string_literal: true module SolidusAdmin - class ShippingMethodsController < SolidusAdmin::BaseController - include SolidusAdmin::ControllerHelpers::Search - - def index - shipping_methods = apply_search_to( - Spree::ShippingMethod.order(id: :desc), - param: :q, - ) - - set_page_and_extract_portion_from(shipping_methods) - - respond_to do |format| - format.html { render component('shipping_methods/index').new(page: @page) } - end - end - - def destroy - @shipping_methods = Spree::ShippingMethod.where(id: params[:id]) - - Spree::ShippingMethod.transaction { @shipping_methods.destroy_all } - - flash[:notice] = t('.success') - redirect_back_or_to shipping_methods_path, status: :see_other - end - + class ShippingMethodsController < SolidusAdmin::ResourcesController private - def shipping_method_params - params.require(:shipping_method).permit(:shipping_method_id, permitted_shipping_method_attributes) + def resource_class = Spree::ShippingMethod + + def resources_collection = Spree::ShippingMethod.unscoped + + def permitted_resource_params + params.require(:shipping_method).permit( + :name, + :admin_name, + :code, + :carrier, + :service_level, + :tracking_url, + :available_to_all, + :available_to_users, + :calculator_type, + :tax_category_id, + store_ids: [], + stock_location_ids: [], + zone_ids: [], + shipping_category_ids: [], + calculator_attributes: [ + :id, + :preferred_amount, + :preferred_currency, + :preferred_flat_percent, + :preferred_first_item, + :preferred_additional_item, + :preferred_max_items, + :preferred_minimal_amount, + :preferred_normal_amount, + :preferred_discount_amount + ] + ) end end end diff --git a/admin/config/locales/shipping_methods.en.yml b/admin/config/locales/shipping_methods.en.yml index d830287e984..d59ce404f4a 100644 --- a/admin/config/locales/shipping_methods.en.yml +++ b/admin/config/locales/shipping_methods.en.yml @@ -4,3 +4,7 @@ en: title: "Shipping Methods" destroy: success: "Shipping methods were successfully removed." + create: + success: "Shipping method was successfully created." + update: + success: "Shipping method was successfully updated." diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 4f70c26e766..568928c8dbb 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -74,7 +74,7 @@ admin_resources :tax_rates, only: [:index, :destroy] admin_resources :payment_methods, only: [:index, :destroy], sortable: true admin_resources :stock_items, only: [:index, :edit, :update] - admin_resources :shipping_methods, only: [:index, :destroy] + admin_resources :shipping_methods, except: [:show] admin_resources :shipping_categories, except: [:show] admin_resources :stock_locations, only: [:index, :destroy] admin_resources :stores, only: [:index, :destroy] diff --git a/admin/spec/features/shipping_methods_spec.rb b/admin/spec/features/shipping_methods_spec.rb index 28471fd9e4c..fc4fc469b32 100644 --- a/admin/spec/features/shipping_methods_spec.rb +++ b/admin/spec/features/shipping_methods_spec.rb @@ -27,4 +27,42 @@ expect(page).to have_content("Add new") expect(page).to have_selector(:css, 'a[href="/admin/shipping_methods/new"]') end + + context "when creating a new shipping method" do + before do + create(:shipping_category, name: "Default") + + visit "/admin/shipping_methods/new" + end + + it "creates the shipping method", :js do + fill_in "Name", with: "Super Saver" + fill_in "Code", with: "super-saver" + fill_in "Carrier", with: "CarrierX" + fill_in "Tracking URL", with: "https://track.example.com/:tracking" + + check "Available to all stock locations" + check "Available to users" + solidus_select "Default", from: "Shipping Categories" + + click_on "Save" + + expect(page).to have_content("Shipping method was successfully created.") + expect(Spree::ShippingMethod.find_by(name: "Super Saver")).to be_present + end + end + + context "when editing an existing shipping method" do + let!(:shipping_method) { create(:shipping_method, name: "Old Name") } + + before { visit spree.edit_admin_shipping_method_path(shipping_method) } + + it "updates the shipping method", :js do + fill_in "Name", with: "New Name" + click_on "Save" + + expect(page).to have_content("Shipping method was successfully updated.") + expect(page).to have_content("New Name") + end + end end