From 3616a7117f41e7fa896677a0e24e73b20a558955 Mon Sep 17 00:00:00 2001 From: Dhurba Baral Date: Mon, 19 Apr 2021 21:42:23 +0545 Subject: [PATCH] allow creating nested form for has-many association --- Gemfile.lock | 2 +- .../madmin/application/_javascript.html.erb | 35 +++++++++++++++- .../fields/nested_has_many/_fields.html.erb | 18 +++++++++ .../fields/nested_has_many/_form.html.erb | 30 ++++++++++++++ .../fields/nested_has_many/_index.html.erb | 1 + .../fields/nested_has_many/_show.html.erb | 5 +++ lib/madmin.rb | 1 + lib/madmin/fields/nested_has_many.rb | 40 +++++++++++++++++++ lib/madmin/resource.rb | 3 +- .../app/madmin/resources/post_resource.rb | 8 ---- .../app/madmin/resources/user_resource.rb | 2 +- test/dummy/app/models/user.rb | 2 + test/madmin/nested_form_test.rb | 26 ++++++++++++ 13 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 app/views/madmin/fields/nested_has_many/_fields.html.erb create mode 100644 app/views/madmin/fields/nested_has_many/_form.html.erb create mode 100644 app/views/madmin/fields/nested_has_many/_index.html.erb create mode 100644 app/views/madmin/fields/nested_has_many/_show.html.erb create mode 100644 lib/madmin/fields/nested_has_many.rb create mode 100644 test/madmin/nested_form_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index c3934770..394ce33f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -225,4 +225,4 @@ DEPENDENCIES webpacker (~> 5.2, >= 5.2.1) BUNDLED WITH - 2.2.3 + 2.2.14 diff --git a/app/views/madmin/application/_javascript.html.erb b/app/views/madmin/application/_javascript.html.erb index 177cae66..7ec62ca6 100644 --- a/app/views/madmin/application/_javascript.html.erb +++ b/app/views/madmin/application/_javascript.html.erb @@ -3,7 +3,7 @@ <%= stylesheet_link_tag "https://unpkg.com/slim-select@1.27.0/dist/slimselect.min.css", "data-turbo-track": "reload" %> diff --git a/app/views/madmin/fields/nested_has_many/_fields.html.erb b/app/views/madmin/fields/nested_has_many/_fields.html.erb new file mode 100644 index 00000000..67318882 --- /dev/null +++ b/app/views/madmin/fields/nested_has_many/_fields.html.erb @@ -0,0 +1,18 @@ +<%= content_tag :div, class: "nested-fields bg-gray-100 rounded-t-xl p-5", data: { new_record: f.object.new_record? } do %> + <% field.nested_attributes.each do |nested_attribute| %> + <% next if nested_attribute[:field].nil? %> + <% next unless nested_attribute[:field].visible?(action_name) %> + <% next unless nested_attribute[:field].visible?(:form) %> + + <% nested_field = nested_attribute[:field] %> + +
+ <%= render partial: nested_field.to_partial_path("form"), locals: { field: nested_field, record: f.object, form: f, resource: field.resource } %> +
+ <% end %> + + <%= link_to "Remove", "#", data: { action: "click->nested-form#remove_association" } %> + + <%= f.hidden_field :_destroy %> + +<% end %> diff --git a/app/views/madmin/fields/nested_has_many/_form.html.erb b/app/views/madmin/fields/nested_has_many/_form.html.erb new file mode 100644 index 00000000..c7d36c32 --- /dev/null +++ b/app/views/madmin/fields/nested_has_many/_form.html.erb @@ -0,0 +1,30 @@ +<%= form.label field.attribute_name, class: "inline-block w-32 flex-shrink-0" %> + +
+ + + <%= form.fields_for field.attribute_name do |nested_form| %> + <%= render( + partial: field.to_partial_path('fields'), + locals: { + f: nested_form, + field: field + } + ) %> + <% end %> + + <%= content_tag :div, class: '', data: { target:"nested-form.links" } do %> + <%= link_to "+ Add new", "#", data: { action: "click->nested-form#add_association" } %> + <% end %> +
diff --git a/app/views/madmin/fields/nested_has_many/_index.html.erb b/app/views/madmin/fields/nested_has_many/_index.html.erb new file mode 100644 index 00000000..3b911d95 --- /dev/null +++ b/app/views/madmin/fields/nested_has_many/_index.html.erb @@ -0,0 +1 @@ +<%= pluralize field.value(record).count, field.attribute_name.to_s %> diff --git a/app/views/madmin/fields/nested_has_many/_show.html.erb b/app/views/madmin/fields/nested_has_many/_show.html.erb new file mode 100644 index 00000000..05f1e3c9 --- /dev/null +++ b/app/views/madmin/fields/nested_has_many/_show.html.erb @@ -0,0 +1,5 @@ +<% field.value(record).each do |object| %> +
+ <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %> +
+<% end %> diff --git a/lib/madmin.rb b/lib/madmin.rb index bf6fcaff..a0154518 100644 --- a/lib/madmin.rb +++ b/lib/madmin.rb @@ -26,6 +26,7 @@ module Fields autoload :RichText, "madmin/fields/rich_text" autoload :Attachment, "madmin/fields/attachment" autoload :Attachments, "madmin/fields/attachments" + autoload :NestedHasMany, "madmin/fields/nested_has_many" end mattr_accessor :resources, default: [] diff --git a/lib/madmin/fields/nested_has_many.rb b/lib/madmin/fields/nested_has_many.rb new file mode 100644 index 00000000..d084bc0d --- /dev/null +++ b/lib/madmin/fields/nested_has_many.rb @@ -0,0 +1,40 @@ +module Madmin + module Fields + class NestedHasMany < Field + DEFAULT_ATTRIBUTES = %w[_destroy id].freeze + def nested_attributes + resource.attributes.reject { |i| skipped_fields.include?(i[:name]) } + end + + def resource + "#{to_model.name}Resource".constantize + end + + def to_param + {"#{attribute_name}_attributes": permitted_fields} + end + + def to_partial_path(name) + unless %w[index show form fields].include? name + raise ArgumentError, "`partial` must be 'index', 'show', 'form' or 'fields'" + end + + "/madmin/fields/#{self.class.field_type}/#{name}" + end + + def to_model + attribute_name.to_s.singularize.classify.constantize + end + + private + + def permitted_fields + (resource.permitted_params - skipped_fields + DEFAULT_ATTRIBUTES).uniq + end + + def skipped_fields + options[:skip] || [] + end + end + end +end diff --git a/lib/madmin/resource.rb b/lib/madmin/resource.rb index 036bda12..0e229df1 100644 --- a/lib/madmin/resource.rb +++ b/lib/madmin/resource.rb @@ -140,7 +140,8 @@ def field_for_type(name, type) polymorphic: Fields::Polymorphic, has_many: Fields::HasMany, has_one: Fields::HasOne, - rich_text: Fields::RichText + rich_text: Fields::RichText, + nested_has_many: Fields::NestedHasMany }.fetch(type) rescue raise ArgumentError, <<~MESSAGE diff --git a/test/dummy/app/madmin/resources/post_resource.rb b/test/dummy/app/madmin/resources/post_resource.rb index 0d20e5d5..d62ca43d 100644 --- a/test/dummy/app/madmin/resources/post_resource.rb +++ b/test/dummy/app/madmin/resources/post_resource.rb @@ -2,18 +2,10 @@ class PostResource < Madmin::Resource # Attributes attribute :id, form: false attribute :title - attribute :comments_count, form: false - attribute :metadata - attribute :created_at, form: false - attribute :updated_at, form: false attribute :body, index: false attribute :image, index: false attribute :attachments, index: false attribute :enum - # Associations - attribute :user - attribute :comments - scope :recent end diff --git a/test/dummy/app/madmin/resources/user_resource.rb b/test/dummy/app/madmin/resources/user_resource.rb index 79c3925f..edda1adb 100644 --- a/test/dummy/app/madmin/resources/user_resource.rb +++ b/test/dummy/app/madmin/resources/user_resource.rb @@ -10,7 +10,7 @@ class UserResource < Madmin::Resource attribute :avatar, index: false # Associations - attribute :posts + attribute :posts, :nested_has_many, skip: %I[enum attachments] attribute :comments attribute :habtms diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb index 301bfd1b..67c12f98 100644 --- a/test/dummy/app/models/user.rb +++ b/test/dummy/app/models/user.rb @@ -1,5 +1,7 @@ class User < ApplicationRecord has_many :posts + accepts_nested_attributes_for :posts, allow_destroy: true + has_many :comments has_and_belongs_to_many :habtms, join_table: :user_habtms diff --git a/test/madmin/nested_form_test.rb b/test/madmin/nested_form_test.rb new file mode 100644 index 00000000..7f0d127a --- /dev/null +++ b/test/madmin/nested_form_test.rb @@ -0,0 +1,26 @@ +require "test_helper" + +class NestedHasManyTest < ActiveSupport::TestCase + test "checks for the right field class" do + field = UserResource.attributes.find { |i| i[:name] == :posts }[:field] + field_comment = UserResource.attributes.find { |i| i[:name] == :comments }[:field] + # Make sure :posts is a :nested_has_many type + assert field.instance_of?(Madmin::Fields::NestedHasMany) + refute field_comment.instance_of?(Madmin::Fields::NestedHasMany) + assert_equal field.resource, PostResource + end + + test "skips fields which is skipped in configuration" do + field = UserResource.attributes.find { |i| i[:name] == :posts }[:field] + + # Make sure :enum is skipped in the UserResource + refute field.to_param.values.flatten.include?(:enum) + assert field.to_param.values.flatten.include?(:body) + end + + test "whitelists unskipped and required params" do + field = UserResource.attributes.find { |i| i[:name] == :posts }[:field] + + assert field.to_param == {posts_attributes: [:id, :title, :body, :image, {attachments: []}, "_destroy", "id"]} + end +end