Skip to content

Conversation

@chaimann
Copy link
Contributor

@chaimann chaimann commented Apr 30, 2025

Unblocks #6160

Summary

With the release of #6168 (addition of new fields to address form) previous updates to the address form component (#6191) were not enough for stock locations form in #6160 to work correctly, so, in order to fix that, this PR introduces fields composition to the address form component.

Break form into View Component slots, with more granular rendering control of each field. Form composition is decided at initialization, where user can provide fields_preset option (:contact by default) which then will pick relevant fields for given form. I.e. fields :name, :vat_id, :reverse_charge_status are relevant only for "contact" address type (e.g. customers, companies), while not relevant for "location" address type (e.g. stock locations).

Additionally, user can specify further which fields to include or exclude for even more customization, e.g. stock location model does not have email attribute yet, if we were to release new stock location form first and then migrate model to add email column, we can do that by specifying exclude_columns: :email when rendering stock location form and remove it when migration is in place.

Showcase

Screen.Recording.2025-04-30.at.22.40.09.mov

Checklist

Check out our PR guidelines for more details.

The following are mandatory for all PRs:

The following are not always needed:

  • 📖 I have updated the README to account for my changes.
  • 📑 I have documented new code with YARD.
  • 🛣️ I have opened a PR to update the guides.
  • ✅ I have added automated tests to cover my changes.
  • 📸 I have attached screenshots to demo visual changes.

@chaimann chaimann self-assigned this Apr 30, 2025
@chaimann chaimann force-pushed the admin-refactor-address-component-v2 branch from 6056a68 to c3011c9 Compare April 30, 2025 21:13
@codecov
Copy link

codecov bot commented Apr 30, 2025

Codecov Report

Attention: Patch coverage is 79.54545% with 18 lines in your changes missing coverage. Please review.

Project coverage is 88.75%. Comparing base (a85ae3c) to head (e45cec2).
Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
...n/ui/forms/address/fieldsets/location/component.rb 0.00% 12 Missing ⚠️
...ponents/concerns/solidus_admin/slotable_default.rb 77.77% 4 Missing ⚠️
...onents/solidus_admin/ui/forms/address/component.rb 90.00% 1 Missing ⚠️
...s/solidus_admin/ui/forms/address/fieldsets/base.rb 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6225      +/-   ##
==========================================
+ Coverage   86.53%   88.75%   +2.22%     
==========================================
  Files         520      849     +329     
  Lines       11947    18326    +6379     
==========================================
+ Hits        10338    16265    +5927     
- Misses       1609     2061     +452     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chaimann chaimann marked this pull request as ready for review May 1, 2025 13:07
@chaimann chaimann requested a review from a team as a code owner May 1, 2025 13:07
Copy link
Member

@tvdeyen tvdeyen left a comment

Choose a reason for hiding this comment

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

Love it. I have one note about easy customization of this component


class SolidusAdmin::UI::Forms::Address::Component < SolidusAdmin::BaseComponent
def initialize(addressable:, form_field_name:, disabled: false, include_name_field: true)
DEFAULT_FIELDS = %i[
Copy link
Member

Choose a reason for hiding this comment

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

I think if we make these two constants private methods one could inherit from this component and override this in order to customize its one address form more easily.

Copy link
Member

Choose a reason for hiding this comment

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

I'd love for us to favour composition-based extension approaches rather than inheritance-based ones. I know that SolidusAdmin is pre-1.0, but I've already seen the pain of changes to components breaking my subclasses. I think in this case we should not provide the subclass option and just let people wrap the class if they want.

@tvdeyen
Copy link
Member

tvdeyen commented May 2, 2025 via email

@jarednorman
Copy link
Member

I don't think it necessarily encourages composition, but I would say it encourages subclassing. If we want to make that functionality controllable, perhaps a different approach would be better... but I'm not sure exactly how that would all fit together.

@chaimann
Copy link
Contributor Author

chaimann commented May 5, 2025

hi guys! If by customizing you mean allowing to include completely custom fields along with the default ones provided (or skip defaults at all), it's possible to do so without the need to subclass/override current component, I'll just need to make a few changes to the component.

@chaimann chaimann marked this pull request as draft May 7, 2025 19:26
@chaimann chaimann force-pushed the admin-refactor-address-component-v2 branch from 39c87f1 to bc7ef6a Compare May 8, 2025 10:01
@chaimann chaimann force-pushed the admin-refactor-address-component-v2 branch from bc7ef6a to 35dd1f5 Compare May 8, 2025 10:04
chaimann added 2 commits May 8, 2025 12:37
Break form into View Component slots, with more granular rendering
control of each field. Form composition is decided at initialization,
where user can provide `fields_preset` option (:contact by default)
which then will pick relevant fields for given form. I.e. fields
:name, :vat_id, :reverse_charge_status are relevant only for "contact"
address type (e.g. customers, companies), while not relevant for
"location" address type (e.g. stock locations).

Additionally, user can specify further which fields to include or
exclude for even more customization, e.g. stock location model does
not have `email` attribute yet, if we were to release new stock
location form first and then migrate model to add `email` column, we
can do that by specifying `exclude_columns: :email` when rendering
stock location form and remove it when migration is in place.
An updated iteration of address component:
 - now using default slots
 (https://viewcomponent.org/guide/slots.html#default_slot_name) and allow
 overriding them;
 Note: `SlotableDefault` had to be patched as it has a bug:
 ViewComponent/view_component#2169
 - default address fields are contained in separate components (currently
 "contact" and "location" with a set of relevant fields for both);
 - it is possible to add a new default named fieldset by creating new fieldset
 component and placing it along with two other default ones;
 - if user wants to include more fields to the default fieldset or redefine a
 default field, they can pass extension definitions in `extends` parameter;
 - to opt out of rendering certain fields one can pass `excludes` array, and
 those fields will not be included in the view;
 - users can pass their own fieldsets if default ones do not fit their needs,
 defining fieldset slot `#with_fieldset &block` and `block` evaluated content
 will render instead of a default fieldset;
@chaimann chaimann force-pushed the admin-refactor-address-component-v2 branch from 35dd1f5 to 6d096ba Compare May 8, 2025 10:38
@chaimann
Copy link
Contributor Author

chaimann commented May 8, 2025

@tvdeyen @jarednorman
Updated address form, it is now fully customizable:

  • extracted fieldsets into their own components, with two default ones "contact" and "location". To create a new default fieldset one just needs to create a component in "ui/forms/address/fieldsets" with fields_map method and subclass Fieldsets::Base class.
  • default fieldsets are extensible, user can include custom fields or override default fields using extends parameter;
  • to exclude columns pass excludes parameter with names of fields to exclude from the view;
  • to use completely custom fieldset rather than default one, pass a block to address component with #with_fieldset {<your fieldset>} call, where <your fieldset> can be either inline calls to render fields with render component("ui/forms/field") or your custom component with fields

Some examples

Default fieldset "contact"
image
Default fieldset "location"
image
With extended fields
image
With custom fieldset
image

@chaimann chaimann marked this pull request as ready for review May 8, 2025 11:06
@chaimann chaimann requested review from jarednorman and tvdeyen May 8, 2025 11:06
@chaimann chaimann mentioned this pull request May 8, 2025
4 tasks
Copy link
Member

@tvdeyen tvdeyen left a comment

Choose a reason for hiding this comment

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

Nice idea. I need to play around with it a bit more but it looks like a great way of adding address components. I have some nits about placing the view component extension module.

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

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

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

chaimann added 2 commits May 9, 2025 15:43
Also uses an updated solution from ViewComponent (the issue
has been resolved but will only be released in v4), also adds
a guard clause to remove the patch once we have upgraded
ViewComponent to `>= 4`
@chaimann
Copy link
Contributor Author

@tvdeyen I've addressed your comments, and, as I mentioned in one of the commits, the issue with ViewComponent default_slot has been fixed, but it won't be around until v4, so the patch is now using an updated solution from their PR and a guard has been added to remove the patch once we have upgraded VC to v4

@chaimann chaimann requested a review from tvdeyen May 12, 2025 14:41
Copy link
Member

@tvdeyen tvdeyen left a comment

Choose a reason for hiding this comment

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

Amazing. Thanks

@tvdeyen tvdeyen merged commit 12faa88 into solidusio:main May 13, 2025
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants