Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,4 @@ DEPENDENCIES
webpacker (~> 5.2, >= 5.2.1)

BUNDLED WITH
2.2.3
2.2.14
35 changes: 34 additions & 1 deletion app/views/madmin/application/_javascript.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<%= stylesheet_link_tag "https://unpkg.com/slim-select@1.27.0/dist/slimselect.min.css", "data-turbo-track": "reload" %>

<script type="module">
import { Application } from 'https://cdn.skypack.dev/stimulus'
import { Application, Controller } from 'https://cdn.skypack.dev/stimulus'
const application = Application.start()

import stimulusFlatpickr from 'https://cdn.skypack.dev/stimulus-flatpickr'
Expand All @@ -21,4 +21,37 @@
ActiveStorage.start()

import * as Turbo from "https://cdn.skypack.dev/@hotwired/turbo"

(() => {
application.register('nested-form', class extends Controller {
static targets = [ "links", "template" ]

connect() {
this.wrapperClass = this.data.get("wrapperClass") || "nested-fields"
}

add_association(event) {
event.preventDefault()

var content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, new Date().getTime())
this.linksTarget.insertAdjacentHTML('beforebegin', content)
}

remove_association(event) {
event.preventDefault()

let wrapper = event.target.closest("." + this.wrapperClass)

// New records are simply removed from the page
if (wrapper.dataset.newRecord == "true") {
wrapper.remove()

// Existing records are hidden and flagged for deletion
} else {
wrapper.querySelector("input[name*='_destroy']").value = 1
wrapper.style.display = 'none'
}
}
})
})()
</script>
18 changes: 18 additions & 0 deletions app/views/madmin/fields/nested_has_many/_fields.html.erb
Original file line number Diff line number Diff line change
@@ -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] %>

<div class="mb-4 flex">
<%= render partial: nested_field.to_partial_path("form"), locals: { field: nested_field, record: f.object, form: f, resource: field.resource } %>
</div>
<% end %>

<small><%= link_to "Remove", "#", data: { action: "click->nested-form#remove_association" } %></small>

<%= f.hidden_field :_destroy %>

<% end %>
30 changes: 30 additions & 0 deletions app/views/madmin/fields/nested_has_many/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<%= form.label field.attribute_name, class: "inline-block w-32 flex-shrink-0" %>

<div class="container space-y-8" data-controller="nested-form">
<template data-target="nested-form.template">

<%= form.fields_for field.attribute_name, field.to_model.new, child_index: 'NEW_RECORD' do |nested_form| %>
<%= render(
partial: field.to_partial_path('fields'),
locals: {
f: nested_form,
field: field
}
) %>
<% end %>
</template>

<%= 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 %>
</div>
1 change: 1 addition & 0 deletions app/views/madmin/fields/nested_has_many/_index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= pluralize field.value(record).count, field.attribute_name.to_s %>
5 changes: 5 additions & 0 deletions app/views/madmin/fields/nested_has_many/_show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% field.value(record).each do |object| %>
<div>
<%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
</div>
<% end %>
1 change: 1 addition & 0 deletions lib/madmin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand Down
40 changes: 40 additions & 0 deletions lib/madmin/fields/nested_has_many.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/madmin/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 0 additions & 8 deletions test/dummy/app/madmin/resources/post_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion test/dummy/app/madmin/resources/user_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions test/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -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

Expand Down
26 changes: 26 additions & 0 deletions test/madmin/nested_form_test.rb
Original file line number Diff line number Diff line change
@@ -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