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

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
@@ -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'
@@ -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
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ en:
save: Save
cancel: Cancel
back: Back
select_address: Select address
title:
ship: Edit Shipping Address
bill: Edit Billing Address
Original file line number Diff line number Diff line change
@@ -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 %>
@@ -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>
@@ -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 %>
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
4 changes: 2 additions & 2 deletions admin/config/routes.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ def overview
render_with_template(
locals: {
order: order,
address: order.send("#{type}_address"),
type: type
}
)
@@ -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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<%= render current_component.new(order: order, type: type) %>
<%= render current_component.new(order: order, address: address, type: type) %>
Original file line number Diff line number Diff line change
@@ -5,15 +5,22 @@ class SolidusAdmin::UI::Forms::Address::ComponentPreview < ViewComponent::Previe
include SolidusAdmin::Preview

def overview
render_with_template
render_with_template(locals: { address: fake_address })
end

# @param disabled toggle
def playground(disabled: false)
view = ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
render component("ui/forms/address").new(
form: ActionView::Helpers::FormBuilder.new(:address, Spree::Address.new, view, {}),
name: "",
address: fake_address,
disabled: disabled
)
end

private

def fake_address
country = Spree::Country.find_or_initialize_by(iso: Spree::Config.default_country_iso)
Spree::Address.new(country: country)
end
end
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
<%= form_for Spree::Address.new, url: '#' do |f| %>
<%= render current_component.new(form: f, disabled: false) %>
<% end %>
<%= render current_component.new(name: "", address: address) %>