diff --git a/app/components/avo/base_component.rb b/app/components/avo/base_component.rb index a8ed488130..cefdfcd20b 100644 --- a/app/components/avo/base_component.rb +++ b/app/components/avo/base_component.rb @@ -3,6 +3,7 @@ class Avo::BaseComponent < ViewComponent::Base extend Literal::Properties include Turbo::FramesHelper + include Avo::Concerns::FindAssociationField def has_with_trial(ability) Avo.license.has_with_trial(ability) @@ -12,8 +13,7 @@ def has_with_trial(ability) # Use the @parent_resource to fetch the field using the @reflection name. def field - reflection_name = params[:related_name]&.to_sym || @reflection.name - @parent_resource.get_field(reflection_name) + find_association_field(resource: @parent_resource, association: params[:related_name] || @reflection.name) rescue nil end diff --git a/app/controllers/avo/application_controller.rb b/app/controllers/avo/application_controller.rb index f28cf540a2..76f8e1f3f5 100644 --- a/app/controllers/avo/application_controller.rb +++ b/app/controllers/avo/application_controller.rb @@ -11,6 +11,7 @@ class ApplicationController < ::ActionController::Base include Avo::ApplicationHelper include Avo::UrlHelpers include Avo::Concerns::Breadcrumbs + include Avo::Concerns::FindAssociationField protect_from_forgery with: :exception around_action :set_avo_locale @@ -85,7 +86,7 @@ def resource def related_resource # Find the field from the parent resource - field = @resource.get_field params[:related_name] + field = find_association_field(resource: @resource, association: params[:related_name]) return field.use_resource if field&.use_resource.present? diff --git a/app/controllers/avo/associations_controller.rb b/app/controllers/avo/associations_controller.rb index cfd8f0e504..d16c9d0cb7 100644 --- a/app/controllers/avo/associations_controller.rb +++ b/app/controllers/avo/associations_controller.rb @@ -26,7 +26,7 @@ def index @parent_resource.hydrate(record: @parent_record) association_name = BaseResource.valid_association_name(@parent_record, association_from_params) @query = @related_authorization.apply_policy @parent_record.send(association_name) - @association_field = @parent_resource.get_field params[:related_name] + @association_field = find_association_field(resource: @parent_resource, association: params[:related_name]) if @association_field.present? && @association_field.scope.present? @query = Avo::ExecutionContext.new(target: @association_field.scope, query: @query, parent: @parent_record).handle @@ -126,7 +126,7 @@ def set_attachment_record end def set_reflection_field - @field = @resource.get_field(@related_resource_name.to_sym) + @field = find_association_field(resource: @resource, association: @related_resource_name) @field.hydrate(resource: @resource, record: @record, view: Avo::ViewInquirer.new(:new)) rescue end diff --git a/lib/avo/concerns/find_association_field.rb b/lib/avo/concerns/find_association_field.rb new file mode 100644 index 0000000000..bf76279556 --- /dev/null +++ b/lib/avo/concerns/find_association_field.rb @@ -0,0 +1,21 @@ +module Avo + module Concerns + module FindAssociationField + # The supported association types are defined in the ASSOCIATIONS constant. + unless defined?(ASSOCIATIONS) + ASSOCIATIONS = ["belongs_to", "has_one", "has_many", "has_and_belongs_to_many"] + end + + # This method is used to find an association field for a given resource. + # Ideally, the exact type of the association should be known in advance. + # However, there are cases where the type is unknown + # and the method will return the first matching association field + # based on the provided association name. + def find_association_field(resource:, association:) + resource.get_field_definitions.find do |field| + (field.id == association.to_sym) && field.type.in?(ASSOCIATIONS) + end + end + end + end +end diff --git a/spec/dummy/app/avo/resources/store.rb b/spec/dummy/app/avo/resources/store.rb index a4f9baf447..9ca49c36ad 100644 --- a/spec/dummy/app/avo/resources/store.rb +++ b/spec/dummy/app/avo/resources/store.rb @@ -12,6 +12,12 @@ def fields field :location, as: :has_one end + # Intentionally use the same ID as the :has_many field to test whether the correct association field + # is retrieved during rendering of the association. + field :patrons, as: :tags do + record.patrons.map(&:name) + end + field :patrons, as: :has_many, through: :patronships, diff --git a/spec/system/avo/has_many_spec.rb b/spec/system/avo/has_many_spec.rb index c8ba4fd72f..b0fea61314 100644 --- a/spec/system/avo/has_many_spec.rb +++ b/spec/system/avo/has_many_spec.rb @@ -105,4 +105,22 @@ def destroy end end end + + describe "duplicated field id" do + let!(:store) { create(:store) } + + it "render tags and has many field" do + StorePatron.create!(user:, store:, review: "some review") + visit avo.resources_store_path(store) + + # Find user name on tags field + expect(page).to have_css('div[data-field-id="patrons"] div[data-target="tag-component"]', text: user.name) + + # Find user name on has many field + within("tr[data-record-id='#{user.id}']") do + expect(page).to have_text(user.first_name) + expect(page).to have_text(user.last_name) + end + end + end end