Skip to content

Commit 56d2024

Browse files
authored
Merge pull request #5507 from nebulab/rainerd/admin/order/select-address-feature
[Admin] Add `Select address` dropdown feature to billing and shipping forms
2 parents 29d130a + c0b9942 commit 56d2024

14 files changed

Lines changed: 177 additions & 56 deletions

File tree

admin/app/components/solidus_admin/orders/show/address/component.html.erb

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,43 @@
1-
<div class="<%= stimulus_id %>">
1+
<div class="<%= stimulus_id %>" data-controller="<%= stimulus_id %>">
22
<%= render component("orders/show").new(order: @order) %>
3+
34
<%= render component("ui/modal").new(title: t(".title.#{@type}"), close_path: solidus_admin.order_path(@order)) do |modal| %>
45
<%= form_for @order, url: solidus_admin.send("order_#{@type}_address_path", @order), html: { id: form_id } do |form| %>
56
<div class="w-full flex flex-col mb-4">
6-
<h2 class="text-sm mb-4 font-semibold"><%= t(".subtitle.#{@type}") %></h2>
7+
<div class="flex justify-between items-center mb-4">
8+
<h2 class="text-sm font-semibold">
9+
<%= t(".subtitle.#{@type}") %>
10+
</h2>
11+
12+
<% if @user&.addresses&.any? %>
13+
<details class="text-black text-sm" data-controller="details-click-outside" data-<%= stimulus_id %>-target="addresses">
14+
<summary
15+
class="text-left flex cursor-pointer select-none"
16+
data-action="keydown.esc-><%= stimulus_id %>#close"
17+
>
18+
<%= t(".select_address") %>
19+
<%= render component("ui/icon").new(name: 'arrow-down-s-fill', class: 'w-5 h-5') %>
20+
</summary>
21+
22+
<div class="absolute mr-4 right-0 bg-white border border-gray-100 rounded-lg py-2 mt-1 shadow-lg z-10 min-w-[16rem] max-h-[26rem] overflow-y-auto">
23+
<% @user.addresses.each do |address| %>
24+
<%= tag.a(
25+
href: solidus_admin.send("order_#{@type}_address_path", @order, address_id: address.id),
26+
class: 'block text-black text-sm hover:bg-gray-50 p-2 mx-2 w-auto rounded-lg',
27+
'data-action': "#{stimulus_id}#close",
28+
'data-turbo-frame': address_frame_id
29+
) do %>
30+
<%= format_address(address) %>
31+
<% end %>
32+
<% end %>
33+
</div>
34+
</details>
35+
<% end %>
36+
</div>
37+
738
<div class="w-full flex gap-4">
8-
<%= form.fields_for :"#{@type}_address" do |address_form| %>
9-
<%= render component('ui/forms/address').new(form: address_form, disabled: false) %>
39+
<%= turbo_frame_tag address_frame_id do %>
40+
<%= render component('ui/forms/address').new(address: @address, name: "order[#{@type}_address_attributes]") %>
1041
<% end %>
1142
</div>
1243

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Controller } from '@hotwired/stimulus'
2+
3+
export default class extends Controller {
4+
static targets = ["addresses"]
5+
6+
close() {
7+
this.addressesTarget.removeAttribute('open')
8+
}
9+
}

admin/app/components/solidus_admin/orders/show/address/component.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ class SolidusAdmin::Orders::Show::Address::Component < SolidusAdmin::BaseCompone
55

66
VALID_TYPES = ['ship', 'bill'].freeze
77

8-
def initialize(order:, type: 'ship')
8+
def initialize(order:, address:, user: nil, type: 'ship')
99
@order = order
10+
@user = user
11+
@address = address
1012
@type = validate_address_type(type)
1113
end
1214

1315
def form_id
1416
@form_id ||= "#{stimulus_id}--form-#{@type}-#{@order.id}"
1517
end
1618

19+
def address_frame_id
20+
@table_frame_id ||= "#{stimulus_id}--#{@type}-address-frame-#{@order.id}"
21+
end
22+
1723
def use_attribute
1824
case @type
1925
when 'ship'
@@ -23,6 +29,23 @@ def use_attribute
2329
end
2430
end
2531

32+
def format_address(address)
33+
safe_join([
34+
address.name,
35+
tag.br,
36+
address.address1,
37+
tag.br,
38+
address.address2,
39+
address.city,
40+
address.zipcode,
41+
address.state&.name,
42+
tag.br,
43+
address.country.name,
44+
tag.br,
45+
address.phone,
46+
], " ")
47+
end
48+
2649
def validate_address_type(type)
2750
VALID_TYPES.include?(type) ? type : raise(ArgumentError, "Invalid address type: #{type}")
2851
end

admin/app/components/solidus_admin/orders/show/address/component.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ en:
44
save: Save
55
cancel: Cancel
66
back: Back
7+
select_address: Select address
78
title:
89
ship: Edit Shipping Address
910
bill: Edit Billing Address

admin/app/components/solidus_admin/orders/show/component.html.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
<%= page_with_sidebar_aside do %>
1717
<%= render component('ui/panel').new do |panel| %>
1818
<% panel.with_menu t(".edit_email"), solidus_admin.order_customer_path(@order) %>
19-
<% panel.with_menu t(".edit_shipping"), solidus_admin.new_order_ship_address_path(@order) %>
20-
<% panel.with_menu t(".edit_billing"), solidus_admin.new_order_bill_address_path(@order) %>
19+
<% panel.with_menu t(".edit_shipping"), solidus_admin.edit_order_ship_address_path(@order) %>
20+
<% panel.with_menu t(".edit_billing"), solidus_admin.edit_order_bill_address_path(@order) %>
2121
<% panel.with_menu t(".remove_customer"), solidus_admin.order_customer_path(@order), method: :delete, class: "text-red-500" if @order.user %>
2222

2323
<% panel.with_section(class: 'flex flex-col gap-6') do %>
@@ -49,7 +49,7 @@
4949
<% if @order.ship_address %>
5050
<%= format_address @order.ship_address %>
5151
<% else %>
52-
<%= link_to t(".add_shipping"), solidus_admin.new_order_ship_address_path(@order), class: 'body-link' %>
52+
<%= link_to t(".add_shipping"), solidus_admin.edit_order_ship_address_path(@order), class: 'body-link' %>
5353
<% end %>
5454
</div>
5555
</div>
@@ -58,7 +58,7 @@
5858
<span class="body-small-bold"><%= @order.class.human_attribute_name(:bill_address) %></span>
5959
<div class="body-small">
6060
<% if @order.bill_address.blank? %>
61-
<%= link_to t(".add_billing"), solidus_admin.new_order_bill_address_path(@order), class: 'body-link' %>
61+
<%= link_to t(".add_billing"), solidus_admin.edit_order_bill_address_path(@order), class: 'body-link' %>
6262
<% elsif @order.bill_address == @order.ship_address %>
6363
<span class="text-gray-500"><%= t('.same_as_shipping') %></span>
6464
<% else %>

admin/app/components/solidus_admin/ui/forms/address/component.html.erb

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,34 @@
33
<%= :disabled if @disabled %>
44
>
55
<div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4">
6-
<%= render component("ui/forms/field").text_field(@form, :name) %>
7-
<%= render component("ui/forms/field").text_field(@form, :address1) %>
8-
<%= render component("ui/forms/field").text_field(@form, :address2) %>
6+
<%= render component("ui/forms/field").text_field(@name, :name, object: @address) %>
7+
<%= render component("ui/forms/field").text_field(@name, :address1, object: @address) %>
8+
<%= render component("ui/forms/field").text_field(@name, :address2, object: @address) %>
99
<div class="flex gap-4 w-full">
10-
<%= render component("ui/forms/field").text_field(@form, :city) %>
11-
<%= render component("ui/forms/field").text_field(@form, :zipcode) %>
10+
<%= render component("ui/forms/field").text_field(@name, :city, object: @address) %>
11+
<%= render component("ui/forms/field").text_field(@name, :zipcode, object: @address) %>
1212
</div>
1313

1414
<%= render component("ui/forms/field").select(
15-
@form,
15+
@name,
1616
:country_id,
1717
Spree::Country.all.map { |c| [c.name, c.id] },
18-
value: @form.object.try(:country_id),
18+
object: @address,
19+
value: @address.try(:country_id),
1920
"data-#{stimulus_id}-target": "country",
2021
"data-action": "change->#{stimulus_id}#loadStates"
2122
) %>
2223

2324
<%= render component("ui/forms/field").select(
24-
@form,
25+
@name,
2526
:state_id,
2627
state_options,
27-
value: @form.object.try(:state_id),
28-
disabled: @form.object.country&.states&.empty?,
28+
object: @address,
29+
value: @address.try(:state_id),
30+
disabled: @address.country&.states&.empty?,
2931
"data-#{stimulus_id}-target": "state"
3032
) %>
3133

32-
<%= render component("ui/forms/field").text_field(@form, :phone) %>
34+
<%= render component("ui/forms/field").text_field(@name, :phone, object: @address) %>
3335
</div>
3436
</fieldset>
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# frozen_string_literal: true
22

33
class SolidusAdmin::UI::Forms::Address::Component < SolidusAdmin::BaseComponent
4-
def initialize(form:, disabled: false)
5-
@form = form
4+
def initialize(address:, name:, disabled: false)
5+
@address = address
6+
@name = name
67
@disabled = disabled
78
end
89

910
def state_options
10-
return [] unless @form.object.country
11-
@form.object.country.states.map { |s| [s.name, s.id] }
11+
return [] unless @address.country
12+
@address.country.states.map { |s| [s.name, s.id] }
1213
end
1314
end

admin/app/components/solidus_admin/ui/forms/field/component.rb

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,61 +12,78 @@ def initialize(label:, hint: nil, tip: nil, error: nil, input_attributes: nil, *
1212
raise ArgumentError, "provide either a block or input_attributes" if content? && input_attributes
1313
end
1414

15-
def self.text_field(form, method, hint: nil, tip: nil, size: :m, **attributes)
16-
errors = form.object.errors.messages_for(method).presence
15+
def self.text_field(form, method, object: nil, hint: nil, tip: nil, size: :m, **attributes)
16+
object_name, object, label, errors = extract_form_details(form, object, method)
1717

1818
new(
19-
label: form.object.class.human_attribute_name(method),
19+
label: label,
2020
hint: hint,
2121
tip: tip,
2222
error: errors,
2323
input_attributes: {
24-
name: "#{form.object_name}[#{method}]",
24+
name: "#{object_name}[#{method}]",
2525
tag: :input,
2626
size: size,
27-
value: form.object.public_send(method),
27+
value: object.public_send(method),
2828
error: (errors.to_sentence.capitalize if errors),
2929
**attributes,
3030
}
3131
)
3232
end
3333

34-
def self.select(form, method, choices, hint: nil, tip: nil, size: :m, **attributes)
35-
errors = form.object.errors.messages_for(method).presence
34+
def self.select(form, method, choices, object: nil, hint: nil, tip: nil, size: :m, **attributes)
35+
object_name, object, label, errors = extract_form_details(form, object, method)
3636

3737
new(
38-
label: form.object.class.human_attribute_name(method),
38+
label: label,
3939
hint: hint,
4040
tip: tip,
4141
error: errors,
4242
input_attributes: {
43-
name: "#{form.object_name}[#{method}]",
43+
name: "#{object_name}[#{method}]",
4444
tag: :select,
4545
choices: choices,
4646
size: size,
47-
value: form.object.public_send(method),
47+
value: object.public_send(method),
4848
error: (errors.to_sentence.capitalize if errors),
4949
**attributes,
5050
}
5151
)
5252
end
5353

54-
def self.text_area(form, method, hint: nil, tip: nil, size: :m, **attributes)
55-
errors = form.object.errors.messages_for(method).presence
54+
def self.text_area(form, method, object: nil, hint: nil, tip: nil, size: :m, **attributes)
55+
object_name, object, label, errors = extract_form_details(form, object, method)
5656

5757
new(
58-
label: form.object.class.human_attribute_name(method),
58+
label: label,
5959
hint: hint,
6060
tip: tip,
6161
error: errors,
6262
input_attributes: {
63-
name: "#{form.object_name}[#{method}]",
63+
name: "#{object_name}[#{method}]",
6464
size: size,
6565
tag: :textarea,
66-
value: form.object.public_send(method),
66+
value: object.public_send(method),
6767
error: (errors.to_sentence.capitalize if errors),
6868
**attributes,
6969
}
7070
)
7171
end
72+
73+
def self.extract_form_details(form, object, method)
74+
if form.is_a?(String)
75+
object_name = form
76+
raise ArgumentError, "Object must be provided when form name is a string" unless object
77+
elsif form.respond_to?(:object)
78+
object_name = form.object_name
79+
object = form.object
80+
else
81+
raise ArgumentError, "Invalid arguments: expected a form object or form.object_name and form.object"
82+
end
83+
84+
errors = object.errors.messages_for(method).presence if object.respond_to?(:errors)
85+
label = object.class.human_attribute_name(method)
86+
87+
[object_name, object, label, errors]
88+
end
7289
end

admin/app/controllers/solidus_admin/addresses_controller.rb

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,62 @@ class AddressesController < BaseController
77
before_action :load_order
88
before_action :validate_address_type
99

10-
def new
11-
address = @order.send("#{address_type}_address")
12-
@order.send("build_#{address_type}_address", country_id: default_country_id) if address.nil?
13-
address ||= @order.send("#{address_type}_address")
14-
address.country_id ||= default_country_id if address.country.nil?
10+
def show
11+
address = find_address || build_new_address
1512

1613
respond_to do |format|
17-
format.html { render component('orders/show/address').new(order: @order, type: address_type) }
14+
format.html do
15+
render component('orders/show/address').new(
16+
order: @order,
17+
user: @order.user,
18+
address: address,
19+
type: address_type,
20+
)
21+
end
1822
end
1923
end
2024

25+
def edit
26+
redirect_to action: :show
27+
end
28+
2129
def update
2230
if @order.contents.update_cart(order_params)
2331
redirect_to order_path(@order), status: :see_other, notice: t('.success')
2432
else
2533
flash.now[:error] = @order.errors[:base].join(", ") if @order.errors[:base].any?
2634

2735
respond_to do |format|
28-
format.html { render component('orders/show/address').new(order: @order, type: address_type), status: :unprocessable_entity }
36+
format.html do
37+
render component('orders/show/address').new(
38+
order: @order,
39+
user: @order.user,
40+
address: @order.send("#{address_type}_address"),
41+
type: address_type,
42+
status: :unprocessable_entity,
43+
)
44+
end
2945
end
3046
end
3147
end
3248

3349
private
3450

51+
def find_address
52+
if params[:address_id].present? && @order.user
53+
address = @order.user.addresses.find_by(id: params[:address_id])
54+
@order.send("#{address_type}_address=", address) if address
55+
else
56+
@order.send("#{address_type}_address")
57+
end
58+
end
59+
60+
def build_new_address
61+
@order.send("build_#{address_type}_address", country_id: default_country_id).tap do |address|
62+
address.country_id ||= default_country_id if address.country.nil?
63+
end
64+
end
65+
3566
def address_type
3667
params[:type].presence_in(%w[bill ship])
3768
end

admin/config/routes.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
resources :orders, only: [:index, :show, :edit, :update] do
2222
resources :line_items, only: [:destroy, :create, :update]
2323
resource :customer
24-
resource :ship_address, only: [:new, :update], controller: "addresses", type: "ship"
25-
resource :bill_address, only: [:new, :update], controller: "addresses", type: "bill"
24+
resource :ship_address, only: [:show, :edit, :update], controller: "addresses", type: "ship"
25+
resource :bill_address, only: [:show, :edit, :update], controller: "addresses", type: "bill"
2626

2727
member do
2828
get :variants_for

0 commit comments

Comments
 (0)