From 526d663f22249670d672ae4a2fd272850763bc2a Mon Sep 17 00:00:00 2001 From: Grayson Wright Date: Fri, 15 Apr 2016 08:49:53 -0700 Subject: [PATCH] Support nested forms for has_many relationships Closes #192 Feature: When I create or edit a model that has_many nested models, I want to view and edit the attributes of the nested models so I can set up all the relationships with a single form. Implementation: Introduce a new field type, `NestedHasMany`. I considered building the feature into the existing `HasMany` field, but this would get in the way of `HasMany` relationships that aren't directly nested, such as in many-to-many relationships. The `NestedHasMany` field builds off of the [Cocoon] gem. It renders fields from the nested model, based on the fields defined in the nested model's dashboard class. Cocoon provides helpers and javascript to easily add and remove nested form fields from the page. [Cocoon]: https://github.com/nathanvda/cocoon --- administrate.gemspec | 1 + .../javascripts/administrate/application.js | 7 ++- .../fields/nested_has_many/_fields.html.erb | 12 +++++ .../fields/nested_has_many/_form.html.erb | 24 ++++++++++ .../fields/nested_has_many/_index.html.erb | 19 ++++++++ .../fields/nested_has_many/_show.html.erb | 40 ++++++++++++++++ lib/administrate/base_dashboard.rb | 1 + lib/administrate/engine.rb | 1 + lib/administrate/field/nested_has_many.rb | 46 +++++++++++++++++++ 9 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 app/views/fields/nested_has_many/_fields.html.erb create mode 100644 app/views/fields/nested_has_many/_form.html.erb create mode 100644 app/views/fields/nested_has_many/_index.html.erb create mode 100644 app/views/fields/nested_has_many/_show.html.erb create mode 100644 lib/administrate/field/nested_has_many.rb diff --git a/administrate.gemspec b/administrate.gemspec index 73023a8c6d..c1a0f898bc 100644 --- a/administrate.gemspec +++ b/administrate.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |s| s.add_dependency "rails", "~> 4.2" s.add_dependency "sass-rails", "~> 5.0" s.add_dependency "selectize-rails", "~> 0.6" + s.add_dependency "cocoon", "~> 1.2" s.description = <<-DESCRIPTION Administrate is heavily inspired by projects like Rails Admin and ActiveAdmin, diff --git a/app/assets/javascripts/administrate/application.js b/app/assets/javascripts/administrate/application.js index 1742ae435c..a5c65edd26 100644 --- a/app/assets/javascripts/administrate/application.js +++ b/app/assets/javascripts/administrate/application.js @@ -1,6 +1,9 @@ //= require jquery //= require jquery_ujs -//= require selectize -//= require moment + +//= require cocoon //= require datetime_picker +//= require moment +//= require selectize + //= require_tree . diff --git a/app/views/fields/nested_has_many/_fields.html.erb b/app/views/fields/nested_has_many/_fields.html.erb new file mode 100644 index 0000000000..b1d4f673a6 --- /dev/null +++ b/app/views/fields/nested_has_many/_fields.html.erb @@ -0,0 +1,12 @@ +
+ + <%# TODO filter out the parent relationship, without this crude array slicing %> + <% field.associated_form.attributes[1..-1].each do |attribute| -%> +
+ <%= render_field attribute, f: f %> +
+ <% end -%> + + <%# TODO I18n %> + <%= link_to_remove_association "Remove #{field.associated_class_name.titleize}", f %> +
diff --git a/app/views/fields/nested_has_many/_form.html.erb b/app/views/fields/nested_has_many/_form.html.erb new file mode 100644 index 0000000000..abde253f2b --- /dev/null +++ b/app/views/fields/nested_has_many/_form.html.erb @@ -0,0 +1,24 @@ +
+ + <%= f.fields_for field.association_name do |nested_form| %> + <%= render( + partial: "fields/nested_has_many/fields", + locals: { + f: nested_form, + field: field, + }, + ) %> + <% end %> + + + +
diff --git a/app/views/fields/nested_has_many/_index.html.erb b/app/views/fields/nested_has_many/_index.html.erb new file mode 100644 index 0000000000..2de6f77a65 --- /dev/null +++ b/app/views/fields/nested_has_many/_index.html.erb @@ -0,0 +1,19 @@ +<%# +# HasMany Index Partial + +This partial renders a has_many relationship, +to be displayed on a resource's index page. + +By default, the relationship is rendered +as a count of how many objects are associated through the relationship. + +## Local variables: + +- `field`: + An instance of [Administrate::Field::HasMany][1]. + A wrapper around the has_many relationship pulled from the database. + +[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/HasMany +%> + +<%= pluralize(field.data.size, field.attribute.to_s.humanize.downcase) %> diff --git a/app/views/fields/nested_has_many/_show.html.erb b/app/views/fields/nested_has_many/_show.html.erb new file mode 100644 index 0000000000..05ba784a90 --- /dev/null +++ b/app/views/fields/nested_has_many/_show.html.erb @@ -0,0 +1,40 @@ +<%# +# HasMany Show Partial + +This partial renders a has_many relationship, +to be displayed on a resource's show page. + +By default, the relationship is rendered +as a table of the first few associated resources. +The columns of the table are taken +from the associated resource class's dashboard. + +## Local variables: + +- `field`: + An instance of [Administrate::Field::HasMany][1]. + Contains methods to help display a table of associated resources. + +[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/HasMany +%> + +<% if field.resources.any? %> + <%= render( + "collection", + collection_presenter: field.associated_collection, + resources: field.resources + ) %> + + <% if field.more_than_limit? %> + + <%= t( + 'administrate.fields.has_many.more', + count: field.limit, + total_count: field.data.count, + ) %> + + <% end %> + +<% else %> + <%= t("administrate.fields.has_many.none") %> +<% end %> diff --git a/lib/administrate/base_dashboard.rb b/lib/administrate/base_dashboard.rb index 83e993aa11..7c75d2ebd4 100644 --- a/lib/administrate/base_dashboard.rb +++ b/lib/administrate/base_dashboard.rb @@ -5,6 +5,7 @@ require "administrate/field/has_many" require "administrate/field/has_one" require "administrate/field/image" +require "administrate/field/nested_has_many" require "administrate/field/number" require "administrate/field/polymorphic" require "administrate/field/select" diff --git a/lib/administrate/engine.rb b/lib/administrate/engine.rb index 5b486ec935..856bc44b5e 100644 --- a/lib/administrate/engine.rb +++ b/lib/administrate/engine.rb @@ -1,3 +1,4 @@ +require "cocoon" require "datetime_picker_rails" require "jquery-rails" require "kaminari" diff --git a/lib/administrate/field/nested_has_many.rb b/lib/administrate/field/nested_has_many.rb new file mode 100644 index 0000000000..32b7e816c6 --- /dev/null +++ b/lib/administrate/field/nested_has_many.rb @@ -0,0 +1,46 @@ +require_relative "has_many" +require "administrate/page/form" + +module Administrate + module Field + class NestedHasMany < Administrate::Field::HasMany + DEFAULT_ATTRIBUTES = [:id, :_destroy].freeze + + def to_s + data + end + + protected + + def self.dashboard_for_resource(resource) + "#{resource.to_s.classify}Dashboard".constantize + end + + def self.associated_attributes(associated_resource) + DEFAULT_ATTRIBUTES + + dashboard_for_resource(associated_resource).new.permitted_attributes + end + + public + + def self.permitted_attribute(associated_resource) + { + "#{associated_resource}_attributes".to_sym => + associated_attributes(associated_resource) + } + end + + def associated_class_name + options.fetch(:class_name, attribute.to_s.singularize.camelcase) + end + + def association_name + associated_class_name.underscore.pluralize + end + + def associated_form + Administrate::Page::Form.new(associated_dashboard, association_name) + end + end + end +end