Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Admin] Add Select address dropdown feature to billing and shipping forms #5507

Merged
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
<div class="<%= stimulus_id %>">
<div class="<%= stimulus_id %>" data-controller="<%= stimulus_id %>">
<%= 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| %>
<div class="w-full flex flex-col mb-4">
<h2 class="text-sm mb-4 font-semibold"><%= t(".subtitle.#{@type}") %></h2>
<div class="flex justify-between items-center mb-4">
<h2 class="text-sm font-semibold">
<%= t(".subtitle.#{@type}") %>
</h2>

<% if @user&.addresses&.any? %>
<details class="text-black text-sm" data-controller="details-click-outside" data-<%= stimulus_id %>-target="addresses">
<summary
class="text-left flex cursor-pointer select-none"
data-action="keydown.esc-><%= stimulus_id %>#close"
>
<%= t(".select_address") %>
<%= render component("ui/icon").new(name: 'arrow-down-s-fill', class: 'w-5 h-5') %>
</summary>

<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">
<% @user.addresses.each do |address| %>
<%= tag.a(
href: solidus_admin.send("order_#{@type}_address_path", @order, address_id: address.id),
class: 'block text-black text-sm hover:bg-gray-50 p-2 mx-2 w-auto rounded-lg',
'data-action': "#{stimulus_id}#close",
'data-turbo-frame': address_frame_id
) do %>
<%= format_address(address) %>
<% end %>
<% end %>
</div>
</details>
<% end %>
</div>

<div class="w-full flex gap-4">
<%= form.fields_for :"#{@type}_address" do |address_form| %>
<%= render component('ui/forms/address').new(form: address_form, disabled: false) %>
<%= turbo_frame_tag address_frame_id do %>
<%= render component('ui/forms/address').new(address: @address, name: "order[#{@type}_address_attributes]") %>
<% end %>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static targets = ["addresses"]

close() {
this.addressesTarget.removeAttribute('open')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ class SolidusAdmin::Orders::Show::Address::Component < SolidusAdmin::BaseCompone

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

def initialize(order:, type: 'ship')
def initialize(order:, address:, user: nil, type: 'ship')
@order = order
@user = user
@address = address
@type = validate_address_type(type)
end

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

def address_frame_id
@table_frame_id ||= "#{stimulus_id}--#{@type}-address-frame-#{@order.id}"
end

def use_attribute
case @type
when 'ship'
Expand All @@ -23,6 +29,23 @@ def use_attribute
end
end

def format_address(address)
safe_join([
address.name,
tag.br,
address.address1,
tag.br,
address.address2,
address.city,
address.zipcode,
address.state&.name,
tag.br,
address.country.name,
tag.br,
address.phone,
], " ")
end

def validate_address_type(type)
VALID_TYPES.include?(type) ? type : raise(ArgumentError, "Invalid address type: #{type}")
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ en:
save: Save
cancel: Cancel
back: Back
select_address: Select address
title:
ship: Edit Shipping Address
bill: Edit Billing Address
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<%= page_with_sidebar_aside do %>
<%= render component('ui/panel').new do |panel| %>
<% panel.with_menu t(".edit_email"), solidus_admin.order_customer_path(@order) %>
<% panel.with_menu t(".edit_shipping"), solidus_admin.new_order_ship_address_path(@order) %>
<% panel.with_menu t(".edit_billing"), solidus_admin.new_order_bill_address_path(@order) %>
<% panel.with_menu t(".edit_shipping"), solidus_admin.edit_order_ship_address_path(@order) %>
<% panel.with_menu t(".edit_billing"), solidus_admin.edit_order_bill_address_path(@order) %>
<% panel.with_menu t(".remove_customer"), solidus_admin.order_customer_path(@order), method: :delete, class: "text-red-500" if @order.user %>

<% panel.with_section(class: 'flex flex-col gap-6') do %>
Expand Down Expand Up @@ -49,7 +49,7 @@
<% if @order.ship_address %>
<%= format_address @order.ship_address %>
<% else %>
<%= link_to t(".add_shipping"), solidus_admin.new_order_ship_address_path(@order), class: 'body-link' %>
<%= link_to t(".add_shipping"), solidus_admin.edit_order_ship_address_path(@order), class: 'body-link' %>
<% end %>
</div>
</div>
Expand All @@ -58,7 +58,7 @@
<span class="body-small-bold"><%= @order.class.human_attribute_name(:bill_address) %></span>
<div class="body-small">
<% if @order.bill_address.blank? %>
<%= link_to t(".add_billing"), solidus_admin.new_order_bill_address_path(@order), class: 'body-link' %>
<%= link_to t(".add_billing"), solidus_admin.edit_order_bill_address_path(@order), class: 'body-link' %>
<% elsif @order.bill_address == @order.ship_address %>
<span class="text-gray-500"><%= t('.same_as_shipping') %></span>
<% else %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,34 @@
<%= :disabled if @disabled %>
>
<div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4">
<%= 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(@name, :name, object: @address) %>
<%= render component("ui/forms/field").text_field(@name, :address1, object: @address) %>
<%= render component("ui/forms/field").text_field(@name, :address2, object: @address) %>
<div class="flex gap-4 w-full">
<%= render component("ui/forms/field").text_field(@form, :city) %>
<%= render component("ui/forms/field").text_field(@form, :zipcode) %>
<%= render component("ui/forms/field").text_field(@name, :city, object: @address) %>
<%= render component("ui/forms/field").text_field(@name, :zipcode, object: @address) %>
</div>

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

<%= render component("ui/forms/field").select(
@form,
@name,
:state_id,
state_options,
value: @form.object.try(:state_id),
disabled: @form.object.country&.states&.empty?,
object: @address,
value: @address.try(:state_id),
disabled: @address.country&.states&.empty?,
"data-#{stimulus_id}-target": "state"
) %>

<%= render component("ui/forms/field").text_field(@form, :phone) %>
<%= render component("ui/forms/field").text_field(@name, :phone, object: @address) %>
</div>
</fieldset>
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Component < SolidusAdmin::BaseComponent
def initialize(form:, disabled: false)
@form = form
def initialize(address:, name:, disabled: false)
@address = address
@name = name
@disabled = disabled
end

def state_options
return [] unless @form.object.country
@form.object.country.states.map { |s| [s.name, s.id] }
return [] unless @address.country
@address.country.states.map { |s| [s.name, s.id] }
end
end
47 changes: 32 additions & 15 deletions admin/app/components/solidus_admin/ui/forms/field/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,61 +12,78 @@ def initialize(label:, hint: nil, tip: nil, error: nil, input_attributes: nil, *
raise ArgumentError, "provide either a block or input_attributes" if content? && input_attributes
end

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

new(
label: form.object.class.human_attribute_name(method),
label: label,
hint: hint,
tip: tip,
error: errors,
input_attributes: {
name: "#{form.object_name}[#{method}]",
name: "#{object_name}[#{method}]",
tag: :input,
size: size,
value: form.object.public_send(method),
value: object.public_send(method),
error: (errors.to_sentence.capitalize if errors),
**attributes,
}
)
end

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

new(
label: form.object.class.human_attribute_name(method),
label: label,
hint: hint,
tip: tip,
error: errors,
input_attributes: {
name: "#{form.object_name}[#{method}]",
name: "#{object_name}[#{method}]",
tag: :select,
choices: choices,
size: size,
value: form.object.public_send(method),
value: object.public_send(method),
error: (errors.to_sentence.capitalize if errors),
**attributes,
}
)
end

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

new(
label: form.object.class.human_attribute_name(method),
label: label,
hint: hint,
tip: tip,
error: errors,
input_attributes: {
name: "#{form.object_name}[#{method}]",
name: "#{object_name}[#{method}]",
size: size,
tag: :textarea,
value: form.object.public_send(method),
value: object.public_send(method),
error: (errors.to_sentence.capitalize if errors),
**attributes,
}
)
end

def self.extract_form_details(form, object, method)
if form.is_a?(String)
object_name = form
raise ArgumentError, "Object must be provided when form name is a string" unless object
elsif form.respond_to?(:object)
object_name = form.object_name
object = form.object
else
raise ArgumentError, "Invalid arguments: expected a form object or form.object_name and form.object"
end

errors = object.errors.messages_for(method).presence if object.respond_to?(:errors)
label = object.class.human_attribute_name(method)

[object_name, object, label, errors]
end
end
45 changes: 38 additions & 7 deletions admin/app/controllers/solidus_admin/addresses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,62 @@ class AddressesController < BaseController
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?
def show
address = find_address || build_new_address

respond_to do |format|
format.html { render component('orders/show/address').new(order: @order, type: address_type) }
format.html do
render component('orders/show/address').new(
order: @order,
user: @order.user,
address: address,
type: address_type,
)
end
end
end

def edit
redirect_to action: :show
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 }
format.html do
render component('orders/show/address').new(
order: @order,
user: @order.user,
address: @order.send("#{address_type}_address"),
type: address_type,
status: :unprocessable_entity,
)
end
end
end
end

private

def find_address
if params[:address_id].present? && @order.user
address = @order.user.addresses.find_by(id: params[:address_id])
@order.send("#{address_type}_address=", address) if address
else
@order.send("#{address_type}_address")
end
end

def build_new_address
@order.send("build_#{address_type}_address", country_id: default_country_id).tap do |address|
address.country_id ||= default_country_id if address.country.nil?
end
end

def address_type
params[:type].presence_in(%w[bill ship])
end
Expand Down
4 changes: 2 additions & 2 deletions admin/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +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"
resource :ship_address, only: [:show, :edit, :update], controller: "addresses", type: "ship"
resource :bill_address, only: [:show, :edit, :update], controller: "addresses", type: "bill"

member do
get :variants_for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def overview
render_with_template(
locals: {
order: order,
address: order.send("#{type}_address"),
type: type
}
)
Expand All @@ -19,7 +20,7 @@ def overview
# @param type select :type_options
def playground(type: "ship")
order = fake_order(type)
render current_component.new(order: order, type: type)
render current_component.new(order: order, address: order.send("#{type}_address"), type: type)
end

private
Expand Down
Loading