diff --git a/app/views/rails_admin/main/_form_filtering_select.html.erb b/app/views/rails_admin/main/_form_filtering_select.html.erb
index 7cb7098129..e527b3a9d8 100644
--- a/app/views/rails_admin/main/_form_filtering_select.html.erb
+++ b/app/views/rails_admin/main/_form_filtering_select.html.erb
@@ -1,34 +1,21 @@
<%
config = field.associated_model_config
- source_abstract_model = RailsAdmin.config(form.object.class).abstract_model
-
- current_action = params[:action].in?(['create', 'new']) ? 'create' : 'update'
-
- edit_url = authorized?(:edit, config.abstract_model) ? edit_path(model_name: config.abstract_model.to_param, modal: true, id: '__ID__') : ''
-
- xhr = !field.associated_collection_cache_all
-
- collection = xhr ? [[field.formatted_value, field.selected_id]] : controller.list_entries(config, :index, field.associated_collection_scope, false).map { |o| [o.send(field.associated_object_label_method), o.send(field.associated_primary_key).to_s] }
-
- js_data = {
- xhr: xhr,
- remote_source: index_path(config.abstract_model.to_param, source_object_id: form.object.id, source_abstract_model: source_abstract_model.to_param, associated_collection: field.name, current_action: current_action, compact: true),
- scopeBy: field.dynamic_scope_relationships
- }
%>
- <% selected_id = (hdv = field.form_default_value).nil? ? field.selected_id : hdv %>
- <%= form.select field.method_name, collection, { selected: selected_id, include_blank: true }, field.html_attributes.reverse_merge({ data: { filteringselect: true, options: js_data.to_json }, placeholder: t('admin.misc.search') }) %>
+ <%=
+ form.select field.method_name, field.collection, { selected: field.form_value, include_blank: true },
+ field.html_attributes.reverse_merge({ data: { filteringselect: true, options: field.widget_options.to_json }, placeholder: t('admin.misc.search') })
+ %>
<% if authorized?(:new, config.abstract_model) && field.inline_add %>
<% path_hash = { model_name: config.abstract_model.to_param, modal: true }.merge!(field.associated_prepopulate_params) %>
<%= link_to " ".html_safe + wording_for(:link, :new, config.abstract_model), '#', data: { link: new_path(path_hash) }, class: "btn btn-info create" %>
<% end %>
- <% if edit_url.present? && field.inline_edit %>
- <%= link_to " ".html_safe + wording_for(:link, :edit, config.abstract_model), '#', data: { link: edit_url }, class: "btn btn-info update ms-2#{' disabled' if field.value.nil?}" %>
+ <% if authorized?(:edit, config.abstract_model) && field.inline_edit %>
+ <%= link_to " ".html_safe + wording_for(:link, :edit, config.abstract_model), '#', data: { link: edit_path(model_name: config.abstract_model.to_param, modal: true, id: '__ID__') }, class: "btn btn-info update ms-2#{' disabled' if field.value.nil?}" %>
<% end %>
\ No newline at end of file
diff --git a/app/views/rails_admin/main/_form_polymorphic_association.html.erb b/app/views/rails_admin/main/_form_polymorphic_association.html.erb
index 067bd4397a..9805a7fdfb 100644
--- a/app/views/rails_admin/main/_form_polymorphic_association.html.erb
+++ b/app/views/rails_admin/main/_form_polymorphic_association.html.erb
@@ -1,32 +1,23 @@
<%
- type_collection = field.polymorphic_type_collection
- type_column = field.association.foreign_type.to_s
- selected_type = field.bindings[:object].send(type_column)
- selected = field.bindings[:object].send(field.association.name)
- collection = selected ? [[field.formatted_value, selected.id]] : [[]]
- column_type_dom_id = form.dom_id(field).sub(field.method_name.to_s, type_column)
- current_action = params[:action].in?(['create', 'new']) ? 'create' : 'update'
-
- default_options = { float_left: false }
-
- js_data = type_collection.inject({}) do |options, model|
- source_abstract_model = RailsAdmin.config(form.object.class).abstract_model
- options.merge(model.second.downcase.gsub('::', '-') => {
- xhr: true,
- remote_source: index_path(model.second.underscore, source_object_id: form.object.id, source_abstract_model: source_abstract_model.to_param, current_action: current_action, compact: true),
- float_left: false
- })
- end
+ column_type_dom_id = form.dom_id(field).sub(field.method_name.to_s, field.type_column)
%>
- <% js_data.each do |model, value| %>
+ <% field.widget_options_for_types.each do |model, value| %>
<% end %>
- <%= form.select type_column, type_collection, {include_blank: true, selected: selected_type}, class: "form-select", id: column_type_dom_id, data: { polymorphic: true, urls: field.polymorphic_type_urls.to_json }, style: "float: left; margin-right: 10px;" %>
+ <%=
+ form.select field.type_column, field.type_collection, {include_blank: true, selected: field.selected_type},
+ class: "form-select", id: column_type_dom_id, data: { polymorphic: true, urls: field.type_urls.to_json },
+ style: "float: left; margin-right: 10px;"
+ %>
- <%= form.select field.method_name, collection, {include_blank: true, selected: selected.try(:id)}, class: "form-control", data: { filteringselect: true, options: js_data[selected_type.try(:downcase)] || default_options }, placeholder: 'Search' %>
+ <%=
+ form.select field.method_name, field.collection, {include_blank: true, selected: field.selected_id},
+ class: "form-control", data: { filteringselect: true, options: field.widget_options },
+ placeholder: 'Search'
+ %>
diff --git a/lib/rails_admin/config/actions/index.rb b/lib/rails_admin/config/actions/index.rb
index 546099bee3..366a15b57c 100644
--- a/lib/rails_admin/config/actions/index.rb
+++ b/lib/rails_admin/config/actions/index.rb
@@ -50,9 +50,11 @@ class Index < RailsAdmin::Config::Actions::Base
format.json do
output =
if params[:compact]
- primary_key_method = @association ? @association.associated_primary_key : @model_config.abstract_model.primary_key
- label_method = @model_config.object_label_method
- @objects.collect { |o| {id: o.send(primary_key_method).to_s, label: o.send(label_method).to_s} }
+ if @association
+ @association.collection(@objects).collect { |(label, id)| {id: id, label: label} }
+ else
+ @objects.collect { |object| {id: object.id.to_s, label: object.send(@model_config.object_label_method).to_s} }
+ end
else
@objects.to_json(@schema)
end
diff --git a/lib/rails_admin/config/fields/association.rb b/lib/rails_admin/config/fields/association.rb
index 654337dd8c..fbde570c82 100644
--- a/lib/rails_admin/config/fields/association.rb
+++ b/lib/rails_admin/config/fields/association.rb
@@ -13,7 +13,7 @@ def association
end
def method_name
- association.key_accessor
+ nested_form ? :"#{name}_attributes" : association.key_accessor
end
register_instance_option :pretty_value do
@@ -134,6 +134,12 @@ def value
bindings[:object].send(association.name)
end
+ # Returns collection of all selectable records
+ def collection(scope = nil)
+ (scope || bindings[:controller].list_entries(associated_model_config, :index, associated_collection_scope, false)).
+ map { |o| [o.send(associated_object_label_method), o.send(associated_primary_key).to_s] }
+ end
+
# has many?
def multiple?
true
diff --git a/lib/rails_admin/config/fields/collection_association.rb b/lib/rails_admin/config/fields/collection_association.rb
new file mode 100644
index 0000000000..b6c91ffcef
--- /dev/null
+++ b/lib/rails_admin/config/fields/collection_association.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'rails_admin/config/fields/association'
+
+module RailsAdmin
+ module Config
+ module Fields
+ class CollectionAssociation < Association
+ # orderable associated objects
+ register_instance_option :orderable do
+ false
+ end
+
+ register_instance_option :partial do
+ nested_form ? :form_nested_many : :form_filtering_multiselect
+ end
+
+ def collection(scope = nil)
+ if scope
+ super
+ elsif associated_collection_cache_all
+ selected = selected_ids
+ i = 0
+ super.sort_by { |a| [selected.index(a[1]) || selected.size, i += 1] }
+ else
+ value.map { |o| [o.send(associated_object_label_method), o.send(associated_primary_key)] }
+ end
+ end
+
+ def associated_prepopulate_params
+ {associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}}
+ end
+
+ def multiple?
+ true
+ end
+
+ def selected_ids
+ value.map { |s| s.send(associated_primary_key).to_s }
+ end
+
+ def form_default_value
+ (default_value if bindings[:object].new_record? && value.empty?)
+ end
+
+ def form_value
+ form_default_value.nil? ? selected_ids : form_default_value
+ end
+
+ def widget_options
+ {
+ xhr: !associated_collection_cache_all,
+ 'edit-url': (inline_edit && bindings[:view].authorized?(:edit, associated_model_config.abstract_model) ? bindings[:view].edit_path(model_name: associated_model_config.abstract_model.to_param, id: '__ID__') : ''),
+ remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true),
+ scopeBy: dynamic_scope_relationships,
+ sortable: !!orderable,
+ removable: !!removable,
+ cacheAll: !!associated_collection_cache_all,
+ regional: {
+ add: ::I18n.t('admin.misc.add_new'),
+ chooseAll: ::I18n.t('admin.misc.chose_all'),
+ clearAll: ::I18n.t('admin.misc.clear_all'),
+ down: ::I18n.t('admin.misc.down'),
+ remove: ::I18n.t('admin.misc.remove'),
+ search: ::I18n.t('admin.misc.search'),
+ up: ::I18n.t('admin.misc.up'),
+ },
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rails_admin/config/fields/singular_association.rb b/lib/rails_admin/config/fields/singular_association.rb
new file mode 100644
index 0000000000..644ea015c8
--- /dev/null
+++ b/lib/rails_admin/config/fields/singular_association.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rails_admin/config/fields/association'
+
+module RailsAdmin
+ module Config
+ module Fields
+ class SingularAssociation < Association
+ register_instance_option :filter_operators do
+ %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank])
+ end
+
+ register_instance_option :formatted_value do
+ (o = value) && o.send(associated_model_config.object_label_method)
+ end
+
+ register_instance_option :partial do
+ nested_form ? :form_nested_one : :form_filtering_select
+ end
+
+ def collection(scope = nil)
+ if associated_collection_cache_all || scope
+ super
+ else
+ [[formatted_value, selected_id]]
+ end
+ end
+
+ def multiple?
+ false
+ end
+
+ def selected_id
+ raise NoMethodError # abstract
+ end
+
+ def form_value
+ form_default_value.nil? ? selected_id : form_default_value
+ end
+
+ def widget_options
+ {
+ xhr: !associated_collection_cache_all,
+ remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true),
+ scopeBy: dynamic_scope_relationships,
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rails_admin/config/fields/types/belongs_to_association.rb b/lib/rails_admin/config/fields/types/belongs_to_association.rb
index 4e469e2ad4..4c8f255151 100644
--- a/lib/rails_admin/config/fields/types/belongs_to_association.rb
+++ b/lib/rails_admin/config/fields/types/belongs_to_association.rb
@@ -1,22 +1,14 @@
# frozen_string_literal: true
-require 'rails_admin/config/fields/association'
+require 'rails_admin/config/fields/singular_association'
module RailsAdmin
module Config
module Fields
module Types
- class BelongsToAssociation < RailsAdmin::Config::Fields::Association
+ class BelongsToAssociation < RailsAdmin::Config::Fields::SingularAssociation
RailsAdmin::Config::Fields::Types.register(self)
- register_instance_option :filter_operators do
- %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank])
- end
-
- register_instance_option :formatted_value do
- (o = value) && o.send(associated_model_config.object_label_method)
- end
-
register_instance_option :sortable do
@sortable ||= abstract_model.adapter_supports_joins? && associated_model_config.abstract_model.properties.collect(&:name).include?(associated_model_config.object_label_method) ? associated_model_config.object_label_method : {abstract_model.table_name => method_name}
end
@@ -25,10 +17,6 @@ class BelongsToAssociation < RailsAdmin::Config::Fields::Association
@searchable ||= associated_model_config.abstract_model.properties.collect(&:name).include?(associated_model_config.object_label_method) ? [associated_model_config.object_label_method, {abstract_model.model => method_name}] : {abstract_model.model => method_name}
end
- register_instance_option :partial do
- nested_form ? :form_nested_one : :form_filtering_select
- end
-
register_instance_option :eager_load do
true
end
@@ -36,14 +24,6 @@ class BelongsToAssociation < RailsAdmin::Config::Fields::Association
def selected_id
bindings[:object].safe_send(association.key_accessor)
end
-
- def method_name
- nested_form ? :"#{name}_attributes" : super
- end
-
- def multiple?
- false
- end
end
end
end
diff --git a/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb b/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb
index cb897c5653..1f63c2cf75 100644
--- a/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb
+++ b/lib/rails_admin/config/fields/types/has_and_belongs_to_many_association.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
-require 'rails_admin/config/fields/types/has_many_association'
+require 'rails_admin/config/fields/collection_association'
module RailsAdmin
module Config
module Fields
module Types
- class HasAndBelongsToManyAssociation < RailsAdmin::Config::Fields::Types::HasManyAssociation
+ class HasAndBelongsToManyAssociation < RailsAdmin::Config::Fields::CollectionAssociation
# Register field type for the type loader
RailsAdmin::Config::Fields::Types.register(self)
end
diff --git a/lib/rails_admin/config/fields/types/has_many_association.rb b/lib/rails_admin/config/fields/types/has_many_association.rb
index a2d51d6215..dd5609d100 100644
--- a/lib/rails_admin/config/fields/types/has_many_association.rb
+++ b/lib/rails_admin/config/fields/types/has_many_association.rb
@@ -1,36 +1,14 @@
# frozen_string_literal: true
-require 'rails_admin/config/fields/association'
+require 'rails_admin/config/fields/collection_association'
module RailsAdmin
module Config
module Fields
module Types
- class HasManyAssociation < RailsAdmin::Config::Fields::Association
+ class HasManyAssociation < RailsAdmin::Config::Fields::CollectionAssociation
# Register field type for the type loader
RailsAdmin::Config::Fields::Types.register(self)
-
- register_instance_option :partial do
- nested_form ? :form_nested_many : :form_filtering_multiselect
- end
-
- # orderable associated objects
- register_instance_option :orderable do
- false
- end
-
- def method_name
- nested_form ? :"#{name}_attributes" : super
- end
-
- # Reader for validation errors of the bound object
- def errors
- bindings[:object].errors[name]
- end
-
- def associated_prepopulate_params
- {associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}}
- end
end
end
end
diff --git a/lib/rails_admin/config/fields/types/has_one_association.rb b/lib/rails_admin/config/fields/types/has_one_association.rb
index 5f5f648730..ca91ed8396 100644
--- a/lib/rails_admin/config/fields/types/has_one_association.rb
+++ b/lib/rails_admin/config/fields/types/has_one_association.rb
@@ -1,44 +1,19 @@
# frozen_string_literal: true
-require 'rails_admin/config/fields/association'
+require 'rails_admin/config/fields/singular_association'
module RailsAdmin
module Config
module Fields
module Types
- class HasOneAssociation < RailsAdmin::Config::Fields::Association
+ class HasOneAssociation < RailsAdmin::Config::Fields::SingularAssociation
# Register field type for the type loader
RailsAdmin::Config::Fields::Types.register(self)
- register_instance_option :filter_operators do
- %w[_discard like not_like is starts_with ends_with] + (required? ? [] : %w[_separator _present _blank])
- end
-
- register_instance_option :partial do
- nested_form ? :form_nested_one : :form_filtering_select
- end
-
- # Accessor for field's formatted value
- register_instance_option :formatted_value do
- (o = value) && o.send(associated_model_config.object_label_method)
- end
-
register_instance_option :allowed_methods do
nested_form ? [method_name] : [name]
end
- def selected_id
- value.try(:id).try(:to_s)
- end
-
- def method_name
- nested_form ? :"#{name}_attributes" : super
- end
-
- def multiple?
- false
- end
-
def associated_prepopulate_params
{associated_model_config.abstract_model.param_key => {association.foreign_key => bindings[:object].try(:id)}}
end
@@ -49,6 +24,10 @@ def parse_input(params)
id = params.delete(method_name)
params[name] = associated_model_config.abstract_model.get(id) if id
end
+
+ def selected_id
+ value.try(:id).try(:to_s)
+ end
end
end
end
diff --git a/lib/rails_admin/config/fields/types/polymorphic_association.rb b/lib/rails_admin/config/fields/types/polymorphic_association.rb
index 327053f51a..7632df92f9 100644
--- a/lib/rails_admin/config/fields/types/polymorphic_association.rb
+++ b/lib/rails_admin/config/fields/types/polymorphic_association.rb
@@ -51,26 +51,29 @@ class PolymorphicAssociation < RailsAdmin::Config::Fields::Types::BelongsToAssoc
false
end
- def associated_collection(type)
- return [] if type.blank?
+ def associated_model_config
+ @associated_model_config ||= association.klass.collect { |type| RailsAdmin.config(type) }.reject(&:excluded?)
+ end
- config = RailsAdmin.config(type)
- config.abstract_model.all.collect do |object|
- [object.send(config.object_label_method), object.id]
+ def collection(_scope = nil)
+ if value
+ [[formatted_value, selected_id]]
+ else
+ [[]]
end
end
- def associated_model_config
- @associated_model_config ||= association.klass.collect { |type| RailsAdmin.config(type) }.reject(&:excluded?)
+ def type_column
+ association.foreign_type.to_s
end
- def polymorphic_type_collection
+ def type_collection
associated_model_config.collect do |config|
[config.label, config.abstract_model.model.name]
end
end
- def polymorphic_type_urls
+ def type_urls
types = associated_model_config.collect do |config|
[config.abstract_model.model.name, config.abstract_model.to_param]
end
@@ -82,6 +85,26 @@ def value
bindings[:object].send(association.name)
end
+ def widget_options_for_types
+ type_collection.inject({}) do |options, model|
+ options.merge(
+ model.second.downcase.gsub('::', '-') => {
+ xhr: true,
+ remote_source: bindings[:view].index_path(model.second.underscore, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, current_action: bindings[:view].current_action, compact: true),
+ float_left: false,
+ },
+ )
+ end
+ end
+
+ def widget_options
+ widget_options_for_types[selected_type.try(:downcase)] || {float_left: false}
+ end
+
+ def selected_type
+ bindings[:object].send(type_column)
+ end
+
def parse_input(params)
if (type_value = params[association.foreign_type.to_sym]).present?
config = associated_model_config.find { |c| type_value == c.abstract_model.model.name }
diff --git a/spec/controllers/rails_admin/main_controller_spec.rb b/spec/controllers/rails_admin/main_controller_spec.rb
index 24caac9b00..c9f1b61c1f 100644
--- a/spec/controllers/rails_admin/main_controller_spec.rb
+++ b/spec/controllers/rails_admin/main_controller_spec.rb
@@ -331,8 +331,8 @@ def get(action, params)
it "uses target model's primary key" do
@user = FactoryBot.create :managing_user
@team = FactoryBot.create :managed_team, user: @user
- get :index, model_name: 'managing_user', source_object_id: @team.id, source_abstract_model: 'managing_user', associated_collection: 'teams', current_action: :create, compact: true, format: :json
- expect(response.body).to match(/"id":"#{@user.id}"/)
+ get :index, model_name: 'managed_team', source_object_id: @user.id, source_abstract_model: 'managing_user', associated_collection: 'teams', current_action: :create, compact: true, format: :json
+ expect(response.body).to match(/"id":"#{@team.id}"/)
end
context 'as JSON' do
diff --git a/spec/integration/fields/belongs_to_association_spec.rb b/spec/integration/fields/belongs_to_association_spec.rb
index 9a03a93fa1..87436915e9 100644
--- a/spec/integration/fields/belongs_to_association_spec.rb
+++ b/spec/integration/fields/belongs_to_association_spec.rb
@@ -12,14 +12,29 @@
end
describe 'on create' do
- before do
- FactoryBot.create :draft
- visit new_path(model_name: 'player')
- end
+ let!(:draft) { FactoryBot.create :draft }
+ let(:team) { FactoryBot.create :team }
it 'shows selects' do
+ visit new_path(model_name: 'player')
is_expected.to have_selector('select#player_team_id')
end
+
+ context 'with default_value' do
+ before do
+ id = team.id
+ RailsAdmin.config Player do
+ configure :team do
+ default_value id
+ end
+ end
+ end
+
+ it 'shows the value as selected' do
+ visit new_path(model_name: 'player')
+ expect(find('select#player_team_id').value).to eq team.id.to_s
+ end
+ end
end
describe 'on show' do
diff --git a/spec/integration/fields/has_many_association_spec.rb b/spec/integration/fields/has_many_association_spec.rb
index 34a552e21c..1fba4fd729 100644
--- a/spec/integration/fields/has_many_association_spec.rb
+++ b/spec/integration/fields/has_many_association_spec.rb
@@ -47,6 +47,22 @@
expect(@league.divisions).not_to include(@divisions[1])
expect(@league.divisions).not_to include(@divisions[2])
end
+
+ context 'with default_value' do
+ before do
+ ids = [@divisions[2].id]
+ RailsAdmin.config League do
+ configure :divisions do
+ default_value ids
+ end
+ end
+ end
+
+ it 'shows the value as selected' do
+ visit new_path(model_name: 'league')
+ expect(find('select#league_division_ids').value).to eq [@divisions[2].id.to_s]
+ end
+ end
end
context 'on update' do
diff --git a/spec/integration/fields/has_one_association_spec.rb b/spec/integration/fields/has_one_association_spec.rb
index 4c16bd7ffa..8834921f13 100644
--- a/spec/integration/fields/has_one_association_spec.rb
+++ b/spec/integration/fields/has_one_association_spec.rb
@@ -14,14 +14,15 @@
context 'on create' do
before do
@draft = FactoryBot.create :draft
- visit new_path(model_name: 'player')
end
it 'shows selects' do
+ visit new_path(model_name: 'player')
is_expected.to have_selector('select#player_draft_id')
end
it 'creates an object with correct associations' do
+ visit new_path(model_name: 'player')
fill_in 'Name', with: 'Jackie Robinson'
fill_in 'Number', with: @draft.player.number + 1
select("Draft ##{@draft.id}", from: 'Draft')
@@ -31,6 +32,22 @@
@draft.reload
expect(@player.draft).to eq(@draft)
end
+
+ context 'with default_value' do
+ before do
+ id = @draft.id
+ RailsAdmin.config Player do
+ configure :draft do
+ default_value id
+ end
+ end
+ end
+
+ it 'shows the value as selected' do
+ visit new_path(model_name: 'player')
+ expect(find('select#player_draft_id').value).to eq @draft.id.to_s
+ end
+ end
end
context 'on update' do
diff --git a/spec/rails_admin/config/fields/base_spec.rb b/spec/rails_admin/config/fields/base_spec.rb
index f9c5ef8b1a..195b17d03e 100644
--- a/spec/rails_admin/config/fields/base_spec.rb
+++ b/spec/rails_admin/config/fields/base_spec.rb
@@ -520,12 +520,6 @@ class CommentReversed < Tableless
end
end
- describe '#associated_collection' do
- it 'returns [] when type is blank?' do
- expect(RailsAdmin.config(Comment).fields.detect { |f| f.name == :commentable }.associated_collection('')).to be_empty
- end
- end
-
describe '#visible?' do
it 'is false when fields have specific name ' do
class FieldVisibilityTest < Tableless