Skip to content
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
42 changes: 42 additions & 0 deletions admin/app/components/concerns/solidus_admin/slotable_default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

# ViewComponent v3 provided experimental functionality to define default content for slots
# https://viewcomponent.org/guide/slots.html#default_slot_name, but unfortunately
# it did not quite work: https://github.com/ViewComponent/view_component/issues/2169.
# Good news the issue has been resolved, not so good news - it's targeted to be released in v4,
# so we have to patch the functionality until we upgrade.
# The solution has been copied from here https://github.com/ViewComponent/view_component/pull/2291/files.

require "view_component/version"
if Gem::Version.new(ViewComponent::VERSION::STRING) >= Gem::Version.new("4")
raise "The fix is included in ViewComponent v4, please remove this patch #{__FILE__}"

Check warning on line 12 in admin/app/components/concerns/solidus_admin/slotable_default.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/concerns/solidus_admin/slotable_default.rb#L12

Added line #L12 was not covered by tests
end

module SolidusAdmin
module SlotableDefault
def get_slot(slot_name)
@__vc_set_slots ||= {}
content unless content_evaluated? # ensure content is loaded so slots will be defined

# If the slot is set, return it
return @__vc_set_slots[slot_name] if @__vc_set_slots[slot_name]

# If there is a default method for the slot, call it
if (default_method = registered_slots[slot_name][:default_method])
renderable_value = send(default_method)
slot = ViewComponent::Slot.new(self)

if renderable_value.respond_to?(:render_in)
slot.__vc_component_instance = renderable_value
else
slot.__vc_content = renderable_value

Check warning on line 32 in admin/app/components/concerns/solidus_admin/slotable_default.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/concerns/solidus_admin/slotable_default.rb#L32

Added line #L32 was not covered by tests
end

slot
elsif self.class.registered_slots[slot_name][:collection]

Check warning on line 36 in admin/app/components/concerns/solidus_admin/slotable_default.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/concerns/solidus_admin/slotable_default.rb#L36

Added line #L36 was not covered by tests
# If empty slot is a collection, return an empty array
[]

Check warning on line 38 in admin/app/components/concerns/solidus_admin/slotable_default.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/concerns/solidus_admin/slotable_default.rb#L38

Added line #L38 was not covered by tests
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,64 +1,7 @@
<fieldset class="<%= stimulus_id %>"
data-controller="<%= stimulus_id %>"
<%= :disabled if @disabled %>
>
<div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4">
<%= render component("ui/forms/field").text_field(@form_field_name, :name, object: @addressable) if @include_name_field %>
<%= render component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable) %>
<%= render component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable) %>
<div class="flex gap-4 w-full">
<%= render component("ui/forms/field").text_field(@form_field_name, :city, object: @addressable) %>
<%= render component("ui/forms/field").text_field(@form_field_name, :zipcode, object: @addressable) %>
</div>

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

<%= content_tag(:div,
data: { "#{stimulus_id}-target": "stateNameWrapper" },
class: (@addressable.country&.states&.empty? ? "flex flex-col gap-2 w-full" : "hidden flex flex-col gap-2 w-full")
) do %>
<%= render component("ui/forms/field").text_field(
@form_field_name,
:state_name,
object: @addressable,
value: @addressable.try(:state_name),
"data-#{stimulus_id}-target": "stateName"
) %>
<% end %>
<input autocomplete="off" type="hidden" name=<%= "#{@form_field_name}[state_id]" %>>

<%= content_tag(:div,
data: { "#{stimulus_id}-target": "stateWrapper" },
class: (@addressable.country&.states&.empty? ? "hidden flex flex-col gap-2 w-full" : "flex flex-col gap-2 w-full")
) do %>
<%= render component("ui/forms/field").select(
@form_field_name,
:state_id,
state_options,
object: @addressable,
value: @addressable.try(:state_id),
"data-#{stimulus_id}-target": "state"
) %>
<% end %>

<%= render component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable) %>
<%= render component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable) %>
<% if Spree::Backend::Config.show_reverse_charge_fields %>
<%= render component("ui/forms/field").text_field(@form_field_name, :vat_id, object: @addressable) %>
<%= render component("ui/forms/field").select(
@form_field_name,
:reverse_charge_status,
Spree::Address.reverse_charge_statuses.keys.map { |key| [I18n.t("spree.reverse_charge_statuses.#{key}"), key] },
object: @addressable
) %>
<% end %>
<%= fieldset %>
</div>
</fieldset>
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Component < SolidusAdmin::BaseComponent
def initialize(addressable:, form_field_name:, disabled: false, include_name_field: true)
@addressable = addressable
@form_field_name = form_field_name
DefaultNamedFieldsetNotFound = Class.new(NameError)

include SolidusAdmin::SlotableDefault

renders_one :fieldset

# @param fieldset [Symbol] use a default named fieldset, component of the same name must be defined
# in "ui/forms/address/fieldsets"
# @param extends [Array<Symbol, Hash{Symbol => #call}>] extend default fieldset,
# see +SolidusAdmin::UI::Forms::Address::Fieldsets::Base+
# @param excludes [Array<Symbol>, Symbol] optionally exclude fields that are present in a default fieldset
# @raise [DefaultNamedFieldsetNotFound] if the provided +:fieldset+ option does not correspond to a defined component
# in "ui/forms/address/fieldsets"
def initialize(addressable:, form_field_name:, disabled: false, fieldset: :contact, extends: [], excludes: [])
@disabled = disabled
@include_name_field = include_name_field
@default_fieldset = fieldset_component(fieldset).new(
addressable:,
form_field_name:,
extends:,
excludes:,
)
end

def state_options
return [] unless @addressable.country
@addressable.country.states.map { |s| [s.name, s.id] }
attr_reader :default_fieldset

private

def fieldset_component(fieldset)
component("ui/forms/address/fieldsets/#{fieldset}")
rescue SolidusAdmin::ComponentRegistry::ComponentNotFoundError
raise DefaultNamedFieldsetNotFound,

Check warning on line 34 in admin/app/components/solidus_admin/ui/forms/address/component.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/solidus_admin/ui/forms/address/component.rb#L34

Added line #L34 was not covered by tests
"to use a default named fieldset `#{fieldset}` you must implement a component in 'ui/forms/address/fieldsets/#{fieldset}'"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="flex gap-4 w-full">
<%= render component("ui/forms/field").text_field(@form_field_name, :city, object: @addressable) %>
<%= render component("ui/forms/field").text_field(@form_field_name, :zipcode, object: @addressable) %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Fields::CityAndZipcode::Component < SolidusAdmin::BaseComponent
def initialize(addressable:, form_field_name:)
@addressable = addressable
@form_field_name = form_field_name
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div class="flex flex-col gap-4 w-full" data-controller="<%= stimulus_id %>">
<%= render component("ui/forms/field").select(
@form_field_name,
:country_id,
Spree::Country.all.map { |c| [c.name, c.id] },
object: @addressable,
value: @addressable.try(:country_id),
"data-#{stimulus_id}-target": "country",
"data-action": "change->#{stimulus_id}#loadStates"
) %>

<%= content_tag(:div,
data: { "#{stimulus_id}-target": "stateNameWrapper" },
class: (@addressable.country&.states&.empty? ? "flex flex-col gap-2 w-full" : "hidden flex flex-col gap-2 w-full")
) do %>
<%= render component("ui/forms/field").text_field(
@form_field_name,
:state_name,
object: @addressable,
value: @addressable.try(:state_name),
"data-#{stimulus_id}-target": "stateName"
) %>
<% end %>
<input autocomplete="off" type="hidden" name=<%= "#{@form_field_name}[state_id]" %>>

<%= content_tag(:div,
data: { "#{stimulus_id}-target": "stateWrapper" },
class: (@addressable.country&.states&.empty? ? "hidden flex flex-col gap-2 w-full" : "flex flex-col gap-2 w-full")
) do %>
<%= render component("ui/forms/field").select(
@form_field_name,
:state_id,
state_options,
object: @addressable,
value: @addressable.try(:state_id),
"data-#{stimulus_id}-target": "state"
) %>
<% end %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Fields::CountryAndState::Component < SolidusAdmin::BaseComponent
def initialize(addressable:, form_field_name:)
@addressable = addressable
@form_field_name = form_field_name
end

private

def state_options
return [] unless @addressable.country
@addressable.country.states.map { |s| [s.name, s.id] }
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= render component("ui/forms/field").text_field(@form_field_name, :vat_id, object: @addressable) %>
<%= render component("ui/forms/field").select(
@form_field_name,
:reverse_charge_status,
Spree::Address.reverse_charge_statuses.keys.map { |key| [I18n.t("spree.reverse_charge_statuses.#{key}"), key] },
object: @addressable
) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Fields::ReverseChargeFields::Component < SolidusAdmin::BaseComponent
def initialize(addressable:, form_field_name:)
@addressable = addressable
@form_field_name = form_field_name
end

def render?
Spree::Backend::Config.show_reverse_charge_fields
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Fieldsets::Base < SolidusAdmin::BaseComponent
renders_many :fields

# @param extends [Array<Hash{Symbol => #call}, Symbol>] Pass an array of extensions to modify existing default
# fieldset with custom fields or override existing fields.
# If extension is a Hash, its key should be the name of the field and its value should be an object that responds
# to #call (e.g. proc or lambda) and returns a ViewComponent instance (or any object that responds to #render_in).
#
# Since text inputs are often used as form fields, pass your field name as a Symbol and the component will render
# a text input for that field.
# @example
# component("ui/forms/address/fieldsets/contact").new(
# extends: [
# title: -> { component("ui/forms/field").select(...) }, # this will add a custom :title select field
# name: -> { component("path/to/component").new }, # this will override existing default :name field
# :company, # this will add a text field for :company
# ],
# excludes: %i[phone reverse_charge], # this will exclude :phone and :reverse_charge from the fieldset
# )
def initialize(addressable:, form_field_name:, extends: [], excludes: [])
@addressable = addressable
@form_field_name = form_field_name
excludes = Array.wrap(excludes).map(&:to_sym)

extended_fields_map = extends.reduce({}) do |acc, extension|
if extension.is_a?(Hash)
acc.merge!(extension)
else
acc[extension.to_sym] = -> { text_field_component(extension) }
acc
end
end

fields_map.merge(extended_fields_map).each do |field_name, renderable|
with_field { render renderable.call } unless field_name.in?(excludes)
end
end

def fields_map
raise NotImplementedError, "fields_map must be implemented in #{self.class}"

Check warning on line 42 in admin/app/components/solidus_admin/ui/forms/address/fieldsets/base.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/solidus_admin/ui/forms/address/fieldsets/base.rb#L42

Added line #L42 was not covered by tests
end

def call
safe_join(fields)
end

private

def text_field_component(field_name)
component("ui/forms/field").text_field(@form_field_name, field_name.to_sym, object: @addressable)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Fieldsets::Contact::Component < SolidusAdmin::UI::Forms::Address::Fieldsets::Base
def fields_map
{
name: -> { component("ui/forms/field").text_field(@form_field_name, :name, object: @addressable) },
street: -> { component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable) },
street_contd: -> { component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable) },
city_and_zipcode: -> { component("ui/forms/address/fields/city_and_zipcode").new(form_field_name: @form_field_name, addressable: @addressable) },
country_and_state: -> { component("ui/forms/address/fields/country_and_state").new(form_field_name: @form_field_name, addressable: @addressable) },
phone: -> { component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable) },
email: -> { component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable) },
reverse_charge: -> { component("ui/forms/address/fields/reverse_charge_fields").new(form_field_name: @form_field_name, addressable: @addressable) },
}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Address::Fieldsets::Location::Component < SolidusAdmin::UI::Forms::Address::Fieldsets::Base
def fields_map
{
street: -> { component("ui/forms/field").text_field(@form_field_name, :address1, object: @addressable) },
street_contd: -> { component("ui/forms/field").text_field(@form_field_name, :address2, object: @addressable) },
city_and_zipcode: -> { component("ui/forms/address/fields/city_and_zipcode").new(form_field_name: @form_field_name, addressable: @addressable) },
country_and_state: -> { component("ui/forms/address/fields/country_and_state").new(form_field_name: @form_field_name, addressable: @addressable) },
phone: -> { component("ui/forms/field").text_field(@form_field_name, :phone, object: @addressable) },
email: -> { component("ui/forms/field").text_field(@form_field_name, :email, object: @addressable) },
}
end
end

Check warning on line 14 in admin/app/components/solidus_admin/ui/forms/address/fieldsets/location/component.rb

View check run for this annotation

Codecov / codecov/patch

admin/app/components/solidus_admin/ui/forms/address/fieldsets/location/component.rb#L3-L14

Added lines #L3 - L14 were not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,36 @@
class SolidusAdmin::UI::Forms::Address::ComponentPreview < ViewComponent::Preview
include SolidusAdmin::Preview

def overview
render_with_template(locals: { addressable: fake_address })
# @param fieldset [Symbol] select { choices: [contact, location] }
def overview(fieldset: :contact)
render_with_template(locals: { addressable: fake_address, fieldset: })
end

# @param fieldset [Symbol] select { choices: [contact, location] }
def with_extended_fields(fieldset: :contact)
render_with_template(locals: { addressable: fake_address, fieldset: })
end

def with_custom_fieldset
addressable = Struct.new(:firstname, :lastname, :company, :vat_id) do
def self.human_attribute_name(attribute)
attribute.to_s.humanize
end
end.new

render_with_template(locals: { addressable: })
end

# @param disabled toggle
def playground(disabled: false)
# @param fieldset [Symbol] select { choices: [contact, location] }
# @param excludes select { choices: [name, street, street_contd, city_and_zipcode, country_and_state, phone, email, reverse_charge], multiple: true }
def playground(disabled: false, fieldset: :contact, excludes: "")
render component("ui/forms/address").new(
form_field_name: "",
addressable: fake_address,
disabled:
disabled:,
fieldset:,
excludes: excludes.present? ? excludes.split(",") : [],
)
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<%= render current_component.new(form_field_name: "", addressable:) %>
<%= render current_component.new(form_field_name: "", addressable:, fieldset:) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= render current_component.new(form_field_name: "", addressable:) do |component| %>
<% component.with_fieldset do %>
<div class="flex gap-4 w-full">
<%= render component("ui/forms/field").text_field("", :firstname, object: addressable) %>
<%= render component("ui/forms/field").text_field("", :lastname, object: addressable) %>
</div>
<%= render component("ui/forms/field").select("", :company, ["Rockstar Games", "CD Projekt Red", "Bethesda Softworks"], object: addressable) %>
<%= render component("ui/forms/field").text_field("", :vat_id, object: addressable) %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= render current_component.new(
form_field_name: "",
addressable:,
fieldset:,
extends: [
{ street_contd: -> { component("ui/forms/field").text_area("", :address2, object: addressable) } },
{ company: -> { component("ui/forms/field").select("", :company, ["Rockstar Games", "CD Projekt Red", "Bethesda Softworks"], object: addressable) } },
:alternative_phone,
],
excludes: %i[phone email reverse_charge]
) %>
Loading