diff --git a/admin/app/components/solidus_admin/orders/show/address/component.html.erb b/admin/app/components/solidus_admin/orders/show/address/component.html.erb new file mode 100644 index 00000000000..f8a547ac963 --- /dev/null +++ b/admin/app/components/solidus_admin/orders/show/address/component.html.erb @@ -0,0 +1,44 @@ +
+ <%= render component("orders/show").new(order: @order) %> + <%= render component("ui/modal").new(title: t(".title.#{@type}"), close_path: solidus_admin.order_path(@order)) do |modal| %> + <%= form_for @order, url: solidus_admin.send("order_#{@type}_address_path", @order), html: { id: form_id } do |form| %> +
+

<%= t(".subtitle.#{@type}") %>

+
+ <%= form.fields_for :"#{@type}_address" do |address_form| %> + <%= render component('ui/forms/address').new(form: address_form, disabled: false) %> + <% end %> +
+ + +
+ <% end %> + + <% modal.with_actions do %> + <%= render component("ui/button").new( + tag: :a, + scheme: :secondary, + text: t(".cancel"), + href: solidus_admin.order_path(@order) + ) %> + + <%= render component("ui/button").new( + tag: :button, + text: t(".save"), + form: form_id + ) %> + <% end %> + <% end %> +
diff --git a/admin/app/components/solidus_admin/orders/show/address/component.rb b/admin/app/components/solidus_admin/orders/show/address/component.rb new file mode 100644 index 00000000000..6bb5970074b --- /dev/null +++ b/admin/app/components/solidus_admin/orders/show/address/component.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class SolidusAdmin::Orders::Show::Address::Component < SolidusAdmin::BaseComponent + include SolidusAdmin::Layout::PageHelpers + + VALID_TYPES = ['ship', 'bill'].freeze + + def initialize(order:, type: 'ship') + @order = order + @type = validate_address_type(type) + end + + def form_id + @form_id ||= "#{stimulus_id}--form-#{@type}-#{@order.id}" + end + + def use_attribute + case @type + when 'ship' + 'use_shipping' + when 'bill' + 'use_billing' + end + end + + def validate_address_type(type) + VALID_TYPES.include?(type) ? type : raise(ArgumentError, "Invalid address type: #{type}") + end +end diff --git a/admin/app/components/solidus_admin/orders/show/address/component.yml b/admin/app/components/solidus_admin/orders/show/address/component.yml new file mode 100644 index 00000000000..e406088f45f --- /dev/null +++ b/admin/app/components/solidus_admin/orders/show/address/component.yml @@ -0,0 +1,15 @@ +# Add your component translations here. +# Use the translation in the example in your template with `t(".hello")`. +en: + save: Save + cancel: Cancel + back: Back + title: + ship: Edit Shipping Address + bill: Edit Billing Address + subtitle: + ship: Shipping Address + bill: Billing Address + use_this_address: + ship: Use this address also for Billing + bill: Use this address also for Shipping diff --git a/admin/app/components/solidus_admin/orders/show/component.html.erb b/admin/app/components/solidus_admin/orders/show/component.html.erb index cfd54df8481..9d677251898 100644 --- a/admin/app/components/solidus_admin/orders/show/component.html.erb +++ b/admin/app/components/solidus_admin/orders/show/component.html.erb @@ -16,8 +16,8 @@ <%= page_with_sidebar_aside do %> <%= render component('ui/panel').new(title: panel_title_with_more_links(t(".customer"), [ link_to(t(".edit_email"), solidus_admin.order_customer_path(@order), class: "p-2 hover:bg-gray-25 rounded-sm text-black"), - link_to(t(".edit_shipping"), "#", class: "p-2 hover:bg-gray-25 rounded-sm text-black"), - link_to(t(".edit_billing"), "#", class: "p-2 hover:bg-gray-25 rounded-sm text-black"), + link_to(t(".edit_shipping"), solidus_admin.new_order_ship_address_path(@order), class: "p-2 hover:bg-gray-25 rounded-sm text-black"), + link_to(t(".edit_billing"), solidus_admin.new_order_bill_address_path(@order), class: "p-2 hover:bg-gray-25 rounded-sm text-black"), link_to(t(".remove_customer"), solidus_admin.order_customer_path(@order), 'data-turbo-method': :delete, class: "p-2 hover:bg-gray-25 rounded-sm text-red-500"), ])) do %>
diff --git a/admin/app/components/solidus_admin/orders/show/component.js b/admin/app/components/solidus_admin/orders/show/component.js index 6c6056ac26d..567f287fd66 100644 --- a/admin/app/components/solidus_admin/orders/show/component.js +++ b/admin/app/components/solidus_admin/orders/show/component.js @@ -2,6 +2,6 @@ import { Controller } from '@hotwired/stimulus' export default class extends Controller { closeMenus() { - this.event.querySelectorAll('details').forEach(details => details.removeAttribute('open')); + this.element.querySelectorAll('details').forEach(details => details.removeAttribute('open')); } } diff --git a/admin/app/components/solidus_admin/orders/show/component.rb b/admin/app/components/solidus_admin/orders/show/component.rb index eddc4c4ac10..36a672d06a0 100644 --- a/admin/app/components/solidus_admin/orders/show/component.rb +++ b/admin/app/components/solidus_admin/orders/show/component.rb @@ -21,7 +21,7 @@ def format_address(address) address.address2, address.city, address.zipcode, - address.state.name, + address.state&.name, tag.br, address.country.name, tag.br, diff --git a/admin/app/components/solidus_admin/ui/forms/address/component.html.erb b/admin/app/components/solidus_admin/ui/forms/address/component.html.erb index f13f88284cf..e4c66ad6057 100644 --- a/admin/app/components/solidus_admin/ui/forms/address/component.html.erb +++ b/admin/app/components/solidus_admin/ui/forms/address/component.html.erb @@ -6,9 +6,9 @@ <%= render component("ui/forms/field").text_field(@form, :name) %> <%= render component("ui/forms/field").text_field(@form, :address1) %> <%= render component("ui/forms/field").text_field(@form, :address2) %> -
- <%= render component("ui/forms/field").text_field(@form, :city, class: "flex-1") %> - <%= render component("ui/forms/field").text_field(@form, :zipcode, class: "flex-1") %> +
+ <%= render component("ui/forms/field").text_field(@form, :city) %> + <%= render component("ui/forms/field").text_field(@form, :zipcode) %>
<%= render component("ui/forms/field").select( @@ -23,8 +23,9 @@ <%= render component("ui/forms/field").select( @form, :state_id, - [], + state_options, value: @form.object.try(:state_id), + disabled: @form.object.country&.states&.empty?, "data-#{stimulus_id}-target": "state" ) %> diff --git a/admin/app/components/solidus_admin/ui/forms/address/component.js b/admin/app/components/solidus_admin/ui/forms/address/component.js index 87a5948a7b4..1b255ee39a4 100644 --- a/admin/app/components/solidus_admin/ui/forms/address/component.js +++ b/admin/app/components/solidus_admin/ui/forms/address/component.js @@ -3,10 +3,6 @@ import { Controller } from '@hotwired/stimulus' export default class extends Controller { static targets = ["country", "state"] - connect() { - this.loadStates() - } - loadStates() { const countryId = this.countryTarget.value @@ -22,12 +18,17 @@ export default class extends Controller { stateSelect.innerHTML = "" - data.forEach(state => { - const option = document.createElement("option") + if (data.length === 0) { + stateSelect.disabled = true + } else { + stateSelect.disabled = false - option.value = state.id - option.innerText = state.name - stateSelect.appendChild(option) - }) + data.forEach((state) => { + const option = document.createElement("option") + option.value = state.id + option.innerText = state.name + stateSelect.appendChild(option) + }) + } } } diff --git a/admin/app/components/solidus_admin/ui/forms/address/component.rb b/admin/app/components/solidus_admin/ui/forms/address/component.rb index 37c85cbf4d0..5aa67048967 100644 --- a/admin/app/components/solidus_admin/ui/forms/address/component.rb +++ b/admin/app/components/solidus_admin/ui/forms/address/component.rb @@ -5,4 +5,9 @@ def initialize(form:, disabled: false) @form = form @disabled = disabled end + + def state_options + return [] unless @form.object.country + @form.object.country.states.map { |s| [s.name, s.id] } + end end diff --git a/admin/app/components/solidus_admin/ui/modal/component.html.erb b/admin/app/components/solidus_admin/ui/modal/component.html.erb index 64319131df5..ec23a4cfb35 100644 --- a/admin/app/components/solidus_admin/ui/modal/component.html.erb +++ b/admin/app/components/solidus_admin/ui/modal/component.html.erb @@ -23,7 +23,7 @@ ) %> -
+
<%= content %>
diff --git a/admin/app/controllers/solidus_admin/addresses_controller.rb b/admin/app/controllers/solidus_admin/addresses_controller.rb new file mode 100644 index 00000000000..8cbb7bfd3e2 --- /dev/null +++ b/admin/app/controllers/solidus_admin/addresses_controller.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module SolidusAdmin + class AddressesController < BaseController + include Spree::Core::ControllerHelpers::StrongParameters + + before_action :load_order + before_action :validate_address_type + + def new + address = @order.send("#{address_type}_address") + @order.send("build_#{address_type}_address", country_id: default_country_id) if address.nil? + address ||= @order.send("#{address_type}_address") + address.country_id ||= default_country_id if address.country.nil? + + respond_to do |format| + format.html { render component('orders/show/address').new(order: @order, type: address_type) } + end + end + + def update + if @order.contents.update_cart(order_params) + redirect_to order_path(@order), status: :see_other, notice: t('.success') + else + flash.now[:error] = @order.errors[:base].join(", ") if @order.errors[:base].any? + + respond_to do |format| + format.html { render component('orders/show/address').new(order: @order, type: address_type), status: :unprocessable_entity } + end + end + end + + private + + def address_type + params[:type].presence_in(%w[bill ship]) + end + + def validate_address_type + unless address_type + flash[:error] = t('.errors.address_type_invalid') + redirect_to spree.admin_order_url(@order) + end + end + + def default_country_id + @default_country_id ||= begin + country = Spree::Country.default + country.id if Spree::Country.available.exists?(id: country.id) + end + end + + def load_order + @order = Spree::Order.find_by!(number: params[:order_id]) + authorize! action_name, @order + end + + def order_params + params.require(:order).permit( + :use_billing, + :use_shipping, + bill_address_attributes: permitted_address_attributes, + ship_address_attributes: permitted_address_attributes + ) + end + end +end diff --git a/admin/config/locales/orders.en.yml b/admin/config/locales/orders.en.yml index 09682ccbfc7..798fb7ea183 100644 --- a/admin/config/locales/orders.en.yml +++ b/admin/config/locales/orders.en.yml @@ -4,3 +4,9 @@ en: title: "Orders" update: success: "Order was updated successfully" + addresses: + title: "Addresses" + update: + success: "The address has been successfully updated." + errors: + address_type_invalid: "Invalid address type. Please select either billing or shipping address." diff --git a/admin/config/routes.rb b/admin/config/routes.rb index e2364fbc9ca..9322532bd18 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -21,6 +21,8 @@ resources :orders, only: [:index, :show, :edit, :update] do resources :line_items, only: [:destroy, :create, :update] resource :customer + resource :ship_address, only: [:new, :update], controller: "addresses", type: "ship" + resource :bill_address, only: [:new, :update], controller: "addresses", type: "bill" member do get :variants_for diff --git a/admin/spec/components/previews/solidus_admin/orders/show/address/component_preview.rb b/admin/spec/components/previews/solidus_admin/orders/show/address/component_preview.rb new file mode 100644 index 00000000000..2120f317a95 --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/orders/show/address/component_preview.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# @component "orders/show/address" +class SolidusAdmin::Orders::Show::Address::ComponentPreview < ViewComponent::Preview + include SolidusAdmin::Preview + + def overview + type = "ship" + order = fake_order(type) + + render_with_template( + locals: { + order: order, + type: type + } + ) + end + + # @param type select :type_options + def playground(type: "ship") + order = fake_order(type) + render current_component.new(order: order, type: type) + end + + private + + def fake_order(type) + order = Spree::Order.new + country = Spree::Country.find_or_initialize_by(iso: Spree::Config.default_country_iso) + + order.define_singleton_method(:id) { 1 } + order.define_singleton_method(:persisted?) { true } + order.define_singleton_method(:to_param) { id.to_s } + order.send("build_#{type}_address", { country: country }) + order + end + + def type_options + current_component::VALID_TYPES + end +end diff --git a/admin/spec/components/previews/solidus_admin/orders/show/address/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/orders/show/address/component_preview/overview.html.erb new file mode 100644 index 00000000000..1f2f4e68235 --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/orders/show/address/component_preview/overview.html.erb @@ -0,0 +1 @@ +<%= render current_component.new(order: order, type: type) %> diff --git a/admin/spec/components/solidus_admin/orders/show/address/component_spec.rb b/admin/spec/components/solidus_admin/orders/show/address/component_spec.rb new file mode 100644 index 00000000000..bb0cc9a95e2 --- /dev/null +++ b/admin/spec/components/solidus_admin/orders/show/address/component_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::Orders::Show::Address::Component, type: :component do + it "renders the overview preview" do + render_preview(:overview) + end +end diff --git a/api/openapi/solidus-api.oas.yml b/api/openapi/solidus-api.oas.yml index 123e734eb83..70bc4bf7587 100644 --- a/api/openapi/solidus-api.oas.yml +++ b/api/openapi/solidus-api.oas.yml @@ -6754,6 +6754,8 @@ components: type: string use_billing: type: boolean + use_shipping: + type: boolean bill_address_attributes: $ref: '#/components/schemas/address-input' ship_address_attributes: diff --git a/core/app/models/spree/order.rb b/core/app/models/spree/order.rb index 7c036f32898..8587165b2f2 100644 --- a/core/app/models/spree/order.rb +++ b/core/app/models/spree/order.rb @@ -135,7 +135,9 @@ def states before_validation :set_currency before_validation :generate_order_number, on: :create before_validation :assign_billing_to_shipping_address, if: :use_billing? + before_validation :assign_shipping_to_billing_address, if: :use_shipping? attr_accessor :use_billing + attr_accessor :use_shipping before_create :create_token before_create :link_by_email @@ -271,6 +273,11 @@ def assign_billing_to_shipping_address true end + def assign_shipping_to_billing_address + self.bill_address = ship_address if ship_address + true + end + def allow_cancel? return false unless completed? && state != 'canceled' shipment_state.nil? || %w{ready backorder pending}.include?(shipment_state) @@ -860,6 +867,10 @@ def use_billing? use_billing.in?([true, 'true', '1']) end + def use_shipping? + use_shipping.in?([true, 'true', '1']) + end + def set_currency self.currency = Spree::Config[:currency] if self[:currency].nil? end diff --git a/core/lib/spree/permitted_attributes.rb b/core/lib/spree/permitted_attributes.rb index 7a9d6360f5c..90e4a7d18ec 100644 --- a/core/lib/spree/permitted_attributes.rb +++ b/core/lib/spree/permitted_attributes.rb @@ -140,6 +140,7 @@ module PermittedAttributes @@checkout_address_attributes = [ :use_billing, + :use_shipping, :email, bill_address_attributes: address_attributes, ship_address_attributes: address_attributes diff --git a/core/spec/models/spree/order/address_spec.rb b/core/spec/models/spree/order/address_spec.rb index 5fefb4584b9..92fefa62185 100644 --- a/core/spec/models/spree/order/address_spec.rb +++ b/core/spec/models/spree/order/address_spec.rb @@ -6,45 +6,36 @@ let(:order) { Spree::Order.new } context 'validation' do - context "when @use_billing is populated" do - before do - order.bill_address = stub_model(Spree::Address) - order.ship_address = nil - end - - context "with true" do - before { order.use_billing = true } - - it "clones the bill address to the ship address" do - order.valid? - expect(order.ship_address).to eq(order.bill_address) + address_scenarios = { + 'use_billing' => { source: :bill_address, target: :ship_address }, + 'use_shipping' => { source: :ship_address, target: :bill_address } + } + + address_scenarios.each do |use_attribute, addresses| + context "when #{use_attribute} is populated" do + before do + order.send("#{addresses[:source]}=", stub_model(Spree::Address)) + order.send("#{addresses[:target]}=", nil) end - end - context "with 'true'" do - before { order.use_billing = 'true' } + ['true', true, '1'].each do |truthy_value| + context "with #{truthy_value.inspect}" do + before { order.send("#{use_attribute}=", truthy_value) } - it "clones the bill address to the shipping" do - order.valid? - expect(order.ship_address).to eq(order.bill_address) + it "clones the #{addresses[:source]} to the #{addresses[:target]}" do + order.valid? + expect(order.send(addresses[:target])).to eq(order.send(addresses[:source])) + end + end end - end - - context "with '1'" do - before { order.use_billing = '1' } - - it "clones the bill address to the shipping" do - order.valid? - expect(order.ship_address).to eq(order.bill_address) - end - end - context "with something other than a 'truthful' value" do - before { order.use_billing = '0' } + context "with something other than a 'truthful' value" do + before { order.send("#{use_attribute}=", '0') } - it "does not clone the bill address to the shipping" do - order.valid? - expect(order.ship_address).to be_nil + it "does not clone the #{addresses[:source]} to the #{addresses[:target]}" do + order.valid? + expect(order.send(addresses[:target])).to be_nil + end end end end