TagField value is saved as a string instead of an array when used as an extra_param in a nested form
Closed this issue · 1 comments
Describe the bug
When a field from extra_params is rendered inside a nested form with a "tag" type, the value of this field is saved as a string instead of array.
Steps to Reproduce
Steps to reproduce the behavior:
- Init model, resource, and nested form for resource
- Add a field to a resource's extra_params
- Render extra field as a tag in nested form
- Select a few tags on the new/edit page of the resource
- Save
Expected behavior & Actual behavior
Actual behavior: tag field value saved as a string: "en,es"
.
Expected behavior: tag field value saved as array: ["en", "es"]
.
Models and resource files
To store this field I use store_model gem and all values are stored inside a single jsonb
column. Gem provides a compatible API with Rails Attributes API, so work with arrays is the same.
Model:
attribute :exclude_locales, :array_of_strings, default: -> { [] }
Resource:
class Avo::Resources::Tenant < Avo::BaseResource
self.model_class = ::Tenant
self.extra_params = [
settings_attributes: [:billing_tier, :leave_version, :exclude_locales]
]
end
Part of nested form:
<%= form.fields_for :settings_attributes, form.object.settings do |ff| %>
<%= avo_edit_field :exclude_locales,
form: ff,
as: :tags,
enforce_suggestions: true,
suggestions: %w[en uk pl es pt-BR de-DE ru].map { |code|
{
value: code,
label: "#{code}: #{I18n.t("locales.#{code}", locale: :en)}"
}
}
%>
<% end %>
The controller for this resource is blank.
System configuration
Avo version: 3.13.7
Rails version: 7.2.2
Ruby version: 3.3.4
License type:
- Community
- Pro
- Advanced
Are you using Avo monkey patches, overriding views or view components?
- Yes. If so, please post code samples.
- No
Screenshots or screen recordings
Additional context
My investigation:
I found that value from the client-side/frontend being sent as a string:
In the documentation, it's stated that tags correctly work with any Rails arrays. I checked the implementation of tags_field
to understand how it works.
I found that BaseController
calls method fill_record
:
This method is defined in BaseApplicationController.
What this method does is just call fill_record
on the corresponding resource:
avo/app/controllers/avo/base_application_controller.rb
Lines 196 to 202 in 15836c2
Resource, in turn, calls fill_record
on each field defined in resource:
Lines 463 to 475 in 15836c2
So, I checked TagsField
source, expecting that it sets a value from params as is, without conversion into an array. But I found completely opposite logic from what I expected - it works as it should, and the string is converted to an array:
avo/lib/avo/fields/tags_field.rb
Lines 53 to 62 in 54c7ca2
I started thinking that the problem was related to the store_model
gem, but after some tests in the rails console, I was unable to reproduce this error.
I reviewed sources again and found that fill_record
method in the resource has separate logic for extra_params
. It assigns values to them as is, without any processing from Field
, ignoring converting from string to array:
Lines 478 to 485 in 15836c2
I didn't investigate further, but I think the problem is related to this place.
Impact
- High impact (It makes my app un-usable.)
- Medium impact (I'm annoyed, but I'll live.)
- Low impact (It's really a tiny thing that I could live with.)
Urgency
- High urgency (I can't continue development without it.)
- Medium urgency (I found a workaround, but I'd love to have it fixed.)
- Low urgency (It can wait. I just wanted you to know about it.)
Great investigation, kudos for the effort! You were definitely on the right track.
The issue is that, since you're rendering the tags within the nested form, they bypass that entire process. Instead, the extra parameters are directly inserted into the record and end up raw in the DB.
I recommend using a before or after callback in that specific Avo controller to manipulate the parameters manually before they're saved.
Below, I've provided some untested and pseudo-code snippets to illustrate what I mean:
class Avo::UsersController < Avo::ResourcesController
def update
manipulate_params
super
end
def create
manipulate_params
super
end
def manipulate_params
params[...] = params[...].somehing
end
end
class Avo::UsersController < Avo::ResourcesController
def update
super
change_tags_thing
end
def create
super
change_tags_thing
end
def change_tags_thing
@record.... = params[...].something
end
end
Keep an eye on this issue #1583 it will support this use-case natively and ensure that extra params are properly processed for the field.
I'll close this one as completed but feel free to re-open it if something is unclear!