Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,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 ViewComponent::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,9 @@
<% if Spree::Backend::Config.show_reverse_charge_fields %>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a huge friend of having surprising behaviour like this in a template. We could make use of the render? callback in the component class instead.

<%= 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 %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 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
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
6 changes: 6 additions & 0 deletions admin/lib/solidus_admin/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,11 @@ class Engine < ::Rails::Engine
SolidusAdmin::BaseComponent.include app.routes.mounted_helpers
end
end

initializer "solidus_admin.load_ext" do
Dir[root.join("lib/solidus_admin/ext/**/*.rb")].sort.each do |file|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be another way of loading concerns/monkeypatches. I am not sure we need that if we make use of Rails autoloading

require file
end
end
end
end
29 changes: 29 additions & 0 deletions admin/lib/solidus_admin/ext/view_component/slotable_default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we move the file somewhere into app/** it would be auto loaded by Rails's autoloader.

Since this is a module anyway, why not put it into app/components/concerns to match Rails' default module paths?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason why I decided not to put it into components concerns folder is that it's a monkey-patch of ViewComponent::SlotableDefault and I usually like to place monkey-patches/extensions separately from other classes/modules (hence the ext folder), how about we place it in app/lib/ext? so that it is still separate and we make use of Rails autoload

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood that this is a monkey patch. There is already a place in solidus (app/decorators) for them, but since you explicitly include this anyway, we can simply use the concerns folder. I order to not forget about this file it is good practice to build a version guard into your monkey patch

if Gem::Version.new("3.22.0") < Gem::Version.new(ViewComponent::Version)
  raise "remove me"
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And FYI: We will migrate from decorators to patches soonish https://github.com/friendlycart/flickwerk

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to know thanks!
just out of interest, I can't find app/decorators folder anywhere

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in core, true. But all extensions and many apps use this pattern.

Thats also a reason why I dont want to introduce it and make use of the concerns folder.


# ViewComponent provides experimental functionality to define default content for slots
# https://viewcomponent.org/guide/slots.html#default_slot_name, but unfortunately
# it does not quite work: https://github.com/ViewComponent/view_component/issues/2169.
# Here we use a suggested fix from the issue, until it is fixed in ViewComponent.

module ViewComponent
module SlotableDefault
def get_slot(slot_name)
content unless content_evaluated? # ensure content is loaded so slots will be defined

@__vc_set_slots ||= {}

return super unless !@__vc_set_slots[slot_name] && (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 23 in admin/lib/solidus_admin/ext/view_component/slotable_default.rb

View check run for this annotation

Codecov / codecov/patch

admin/lib/solidus_admin/ext/view_component/slotable_default.rb#L23

Added line #L23 was not covered by tests
end

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