avo-hq/avo

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:

  1. Init model, resource, and nested form for resource
  2. Add a field to a resource's extra_params
  3. Render extra field as a tag in nested form
  4. Select a few tags on the new/edit page of the resource
  5. Save

Expected behavior & Actual behavior

Actual behavior: tag field value saved as a string: "en,es".
Image

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

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:

before_action :fill_record, only: [:create, :update]

This method is defined in BaseApplicationController. What this method does is just call fill_record on the corresponding resource:

def fill_record
# We have to skip filling the the record if this is an attach action
return if is_attach_action?
@record = @resource.fill_record(@record_to_fill, cast_nullable(model_params), extra_params: extra_params)
assign_default_value_to_disabled_fields if @view.create?
end

Resource, in turn, calls fill_record on each field defined in resource:

def fill_record(record, permitted_params, extra_params: [], fields: nil)
# Write the field values
permitted_params.each do |key, value|
field = if fields.present?
fields.find { |f| f.id == key.to_sym }
else
fields_by_database_id[key]
end
next unless field.present?
record = field.fill_field record, key, value, permitted_params
end

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:

def fill_field(record, key, value, params)
return fill_acts_as_taggable(record, key, value, params) if acts_as_taggable_on.present?
value = if value.is_a?(String)
value.split(delimiters[0])
else
value
end
record.send(:"#{key}=", apply_update_using(record, key, value, resource))

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:

if extra_params.present?
# Pick only the extra params
# params at this point are already permited, only need the keys to access them
extra_attributes = permitted_params.slice(*flatten_keys(extra_params))
# Let Rails fill in the rest of the params
record.assign_attributes extra_attributes
end

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!