- <%= 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