activeadmin_polymorphic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/Gemfile +40 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +20 -0
- data/README.md +86 -0
- data/Rakefile +33 -0
- data/activeadmin_polymorphic.gemspec +19 -0
- data/app/assets/javascripts/activeadmin_polymorphic.js.coffee +172 -0
- data/app/assets/stylesheets/activeadmin_polymorphic.css.sass +25 -0
- data/lib/activeadmin_polymorphic/engine.rb +5 -0
- data/lib/activeadmin_polymorphic/form_builder.rb +124 -0
- data/lib/activeadmin_polymorphic/version.rb +3 -0
- data/lib/activeadmin_polymorphic.rb +5 -0
- data/spec/rails_helper.rb +154 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/deferred_garbage_collection.rb +19 -0
- data/spec/support/detect_rails_version.rb +26 -0
- data/spec/support/integration_example_group.rb +31 -0
- data/spec/support/jslint.yml +80 -0
- data/spec/support/rails_template.rb +104 -0
- data/spec/support/rails_template_with_data.rb +59 -0
- data/spec/support/templates/cucumber.rb +24 -0
- data/spec/support/templates/cucumber_with_reloading.rb +5 -0
- data/spec/support/templates/en.yml +8 -0
- data/spec/support/templates/policies/active_admin/comment_policy.rb +9 -0
- data/spec/support/templates/policies/active_admin/page_policy.rb +18 -0
- data/spec/support/templates/policies/application_policy.rb +45 -0
- data/spec/support/templates/policies/category_policy.rb +7 -0
- data/spec/support/templates/policies/post_policy.rb +15 -0
- data/spec/support/templates/policies/store_policy.rb +11 -0
- data/spec/support/templates/policies/user_policy.rb +11 -0
- data/spec/support/templates/post_decorator.rb +11 -0
- data/spec/unit/form_builder_spec.rb +121 -0
- data/tasks/test.rake +83 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 806875f4dc043b24f94549838a81c5ad6d180e4b
|
4
|
+
data.tar.gz: 786835f0298f3ce1e08870a9bede6f9ab036d9e6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8a5da9426cf627abefc48134e70fc9bea72ebd90de1f8c1e28094e6dc693037609120a1ed514a26a1d5b417ed64a6d558623f8a47ffcab26b0585eee6b60b2e5
|
7
|
+
data.tar.gz: d8f296a37a267d68ccccf8d9cfea1b8449f7ebe66b3a889e1791ad29126093191a68bde27f27c8834cb093d4aa64a03bd8eff151f36b3c0e2a65bede0e26e666
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
## Mac
|
3
|
+
.DS_Store
|
4
|
+
|
5
|
+
## Windows
|
6
|
+
.Thumbs.db
|
7
|
+
|
8
|
+
## TextMate
|
9
|
+
*.tm_project
|
10
|
+
*.tmproj
|
11
|
+
tmtags
|
12
|
+
|
13
|
+
## Emacs
|
14
|
+
*~
|
15
|
+
\#*
|
16
|
+
.\#*
|
17
|
+
|
18
|
+
## Vim
|
19
|
+
*.swp
|
20
|
+
# IDEA / RUBYMINE
|
21
|
+
.idea
|
22
|
+
|
23
|
+
## RVM
|
24
|
+
.rvmrc
|
25
|
+
.ruby-version
|
26
|
+
.ruby-gemset
|
27
|
+
|
28
|
+
## Project (general)
|
29
|
+
tags
|
30
|
+
coverage
|
31
|
+
rdoc
|
32
|
+
doc
|
33
|
+
.yardoc
|
34
|
+
pkg
|
35
|
+
|
36
|
+
## Project (specific)
|
37
|
+
bin/
|
38
|
+
.bundle
|
39
|
+
spec/rails
|
40
|
+
*.sqlite3-journal
|
41
|
+
Gemfile.lock
|
42
|
+
Gemfile-*.lock
|
43
|
+
capybara*
|
44
|
+
viewcumber
|
45
|
+
test-rails*
|
46
|
+
public
|
47
|
+
.rspec
|
48
|
+
.rails-version
|
49
|
+
.rbenv-version
|
50
|
+
.localeapp/*
|
data/Gemfile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'activeadmin', github: 'activeadmin'
|
6
|
+
gem 'inherited_resources'
|
7
|
+
|
8
|
+
group :development do
|
9
|
+
# Debugging
|
10
|
+
gem 'better_errors' # Web UI to debug exceptions. Go to /__better_errors to access the latest one
|
11
|
+
gem 'binding_of_caller' # Retrieve the binding of a method's caller in MRI Ruby >= 1.9.2
|
12
|
+
|
13
|
+
# Performance
|
14
|
+
gem 'rack-mini-profiler' # Inline app profiler. See ?pp=help for options.
|
15
|
+
gem 'flamegraph' # Flamegraph visualiztion: ?pp=flamegraph
|
16
|
+
|
17
|
+
# Documentation
|
18
|
+
gem 'yard' # Documentation generator
|
19
|
+
gem 'redcarpet' # Markdown implementation (for yard)
|
20
|
+
end
|
21
|
+
|
22
|
+
group :test do
|
23
|
+
gem 'capybara'
|
24
|
+
gem 'simplecov', require: false # Test coverage generator. Go to /coverage/ after running tests
|
25
|
+
gem 'coveralls', require: false # Test coverage website. Go to https://coveralls.io
|
26
|
+
gem 'cucumber-rails', require: false
|
27
|
+
gem 'database_cleaner'
|
28
|
+
gem 'guard-rspec'
|
29
|
+
gem 'jasmine'
|
30
|
+
gem 'jslint_on_rails'
|
31
|
+
gem 'launchy'
|
32
|
+
gem 'rails-i18n' # Provides default i18n for many languages
|
33
|
+
gem 'rspec'
|
34
|
+
gem 'rspec-mocks'
|
35
|
+
gem 'rspec-rails'
|
36
|
+
gem 'i18n-spec'
|
37
|
+
gem 'shoulda-matchers'
|
38
|
+
gem 'sqlite3'
|
39
|
+
gem 'poltergeist'
|
40
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# More info at https://github.com/guard/guard#readme
|
2
|
+
|
3
|
+
guard 'rspec', all_on_start: false, cmd: "bundle exec rspec" do
|
4
|
+
watch(%r{^spec/.+_spec\.rb$})
|
5
|
+
watch(%r{^lib/active_admin_polymorphic/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
6
|
+
watch('spec/spec_helper.rb') { "spec/" }
|
7
|
+
watch('spec/rails_helper.rb') { "spec/" }
|
8
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Sergeev Peter
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# ActiveadminPolymorphic
|
2
|
+
|
3
|
+
Activeadmin Polymorphic gem is made to bring has_many polymorphic nested forms into your ActiveAdmin. ActiveAdmin users formtastic to build awesome forms, but formatstic itself doesn't support polymorphic relations. `activeadmin_polymorphic` gem is trying to solve that problem.
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
# Features
|
8
|
+
|
9
|
+
* polymorphic forms
|
10
|
+
* validation
|
11
|
+
* sortable behaviour
|
12
|
+
* file uploads
|
13
|
+
|
14
|
+
# Installation
|
15
|
+
|
16
|
+
Add this to your Gemfile:
|
17
|
+
|
18
|
+
``` ruby
|
19
|
+
gem "activeadmin_polymorphic"
|
20
|
+
```
|
21
|
+
|
22
|
+
and run `bundle install`.
|
23
|
+
|
24
|
+
Include assets in js and css manifests
|
25
|
+
|
26
|
+
```
|
27
|
+
#= require activeadmin_polymorphic
|
28
|
+
@import "activeadmin_polymorphic";
|
29
|
+
```
|
30
|
+
|
31
|
+
# Usage
|
32
|
+
|
33
|
+
To use gem, your model should have related model, which works as a proxy to polymorphic relations.
|
34
|
+
|
35
|
+

|
36
|
+
|
37
|
+
Gem extrands activeadmin's form builder, so to enable `has_many_polymorphic` method you need to override form builder using `builder` option:
|
38
|
+
|
39
|
+
```
|
40
|
+
...
|
41
|
+
SECTIONABLES = [Image, Text]
|
42
|
+
|
43
|
+
form builder: ActiveadminPolymorphic::FormBuilder do |f|
|
44
|
+
f.polymorphic_has_many :sections, :sectionable, types: SECTIONABLES
|
45
|
+
end
|
46
|
+
...
|
47
|
+
```
|
48
|
+
|
49
|
+
There are few options available:
|
50
|
+
* first option is a name of polymorphic has_many association
|
51
|
+
* second option referes to polymorphied version of association name (sectionable_id and sectaionable_type for example)
|
52
|
+
* `types` - list of related models you want to use
|
53
|
+
* `allow_destroy` - weather or not to allow to destroy related objects
|
54
|
+
* `sortable` - enables drag'n'drop for nested forms, accepts sortable column name, for example `sortable: :priority`
|
55
|
+
|
56
|
+
Subforms for polymorphic relations are forms which you define in your ActiveAdmin. Gem fetches and submits them using some ajax magic.
|
57
|
+
|
58
|
+
# Under the hood
|
59
|
+
|
60
|
+
This gem is a set of dirty hacks and tricks. Calling `polymorphic_has_many` makes it to do the following things:
|
61
|
+
|
62
|
+
* for new records it generates dropdown with polymorphic types
|
63
|
+
* for exising records it generates two hidden fields with id and type
|
64
|
+
* then the real javascript starts, it extracts whole forms from polymorphic models new or edit pages, strips form actions, and inserts that forms right into parent form
|
65
|
+
* when you try to submit forms, javascripts submit subforms first; if subforms are invalid, it reloads them with erros and interupts main form submission process
|
66
|
+
* after all sub forms successfully saved, it strips them (because forms nested into other forms are simantically invalid, right?) and submits parent form
|
67
|
+
|
68
|
+
# File uploads in subforms
|
69
|
+
|
70
|
+
Gem relies on [rails ajax](https://github.com/rails/jquery-rails) form submissions, which doesn't allow to submit files directly. Workaround for it is asynchronous file submission using for example [remotipart](https://github.com/JangoSteve/remotipart) for CarrierWave or [refile](https://github.com/elabs/refile) with [refile-input](https://github.com/hyperoslo/refile-input). Note: before subforms submissions javascript strips all file inputs from forms.
|
71
|
+
|
72
|
+
# Testing
|
73
|
+
|
74
|
+
Tests stucture is mostly copied from original ActiveAdmin.
|
75
|
+
|
76
|
+
Install development dependencies with `bundle install`. To setup test suit run `rake test`. Run tests with `bundle exec guard`. There aren't many of them, let's say there are quite a few.
|
77
|
+
|
78
|
+
# In plan
|
79
|
+
|
80
|
+
* allow to reuse existing polymorphic objects
|
81
|
+
* check who it works with models under sertain namespace
|
82
|
+
* improve tests
|
83
|
+
|
84
|
+
# License
|
85
|
+
|
86
|
+
[MIT](LICENSE.txt)
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake'
|
3
|
+
Bundler.setup
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
def cmd(command)
|
7
|
+
puts command
|
8
|
+
fail unless system command
|
9
|
+
end
|
10
|
+
|
11
|
+
require File.expand_path('../spec/support/detect_rails_version', __FILE__)
|
12
|
+
|
13
|
+
# Import all our rake tasks
|
14
|
+
FileList['tasks/**/*.rake'].each { |task| import task }
|
15
|
+
|
16
|
+
task default: :test
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'jasmine'
|
20
|
+
load 'jasmine/tasks/jasmine.rake'
|
21
|
+
rescue LoadError
|
22
|
+
task :jasmine do
|
23
|
+
abort 'Jasmine is not available. In order to run jasmine, you must: (sudo) gem install jasmine'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
task :console do
|
28
|
+
require 'irb'
|
29
|
+
require 'irb/completion'
|
30
|
+
|
31
|
+
ARGV.clear
|
32
|
+
IRB.start
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
# Maintain your gem's version:
|
4
|
+
require "activeadmin_polymorphic/version"
|
5
|
+
|
6
|
+
# Describe your gem and declare its dependencies:
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.license = "MIT"
|
9
|
+
s.name = "activeadmin_polymorphic"
|
10
|
+
s.version = ActiveadminPolymorphic::VERSION
|
11
|
+
s.authors = ["Petr Sergeev", "Sindre Moen"]
|
12
|
+
s.email = ["peter@hyper.no", "sindre@hyper.no"]
|
13
|
+
s.description = 'This gem extends formtastic\'s form builder to support polymoprhic has many relations in your forms'
|
14
|
+
s.summary = 'HasMany polymoprhic support for active admin.'
|
15
|
+
s.homepage = "https://github.com/hyperoslo/activeadmin-polymorphic"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n").sort
|
18
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
19
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
$ ->
|
2
|
+
if $('.polymorphic_has_many_container').length
|
3
|
+
form = $('#main_content').find('form:first')
|
4
|
+
$(form).on 'submit', (e) ->
|
5
|
+
submissions_counter = 0
|
6
|
+
parentForm = @
|
7
|
+
expect = $(@).find('form').length
|
8
|
+
if submissions_counter < expect
|
9
|
+
e.preventDefault()
|
10
|
+
|
11
|
+
$(@).find('form').each ->
|
12
|
+
remoteSubmit @, ->
|
13
|
+
submissions_counter++
|
14
|
+
if submissions_counter == expect
|
15
|
+
$(form).find('form').remove()
|
16
|
+
stripEmptyRelations()
|
17
|
+
$(parentForm).submit()
|
18
|
+
|
19
|
+
$(document).on "upload:start", "form", (event) ->
|
20
|
+
form = $('#main_content').find('form:first')
|
21
|
+
form.find("input[type=submit]").attr "disabled", true
|
22
|
+
|
23
|
+
$(document).on "upload:complete", "form", (event) ->
|
24
|
+
form = $('#main_content').find('form:first')
|
25
|
+
|
26
|
+
unless form.find("input.uploading").length
|
27
|
+
form.find("input[type=submit]").removeAttr "disabled"
|
28
|
+
|
29
|
+
$('.polymorphic_has_many_fields').each (index, rapper) ->
|
30
|
+
rapper = $ rapper
|
31
|
+
|
32
|
+
hiddenField = rapper.find 'input[type=hidden][data-path]'
|
33
|
+
formPath = hiddenField.data 'path'
|
34
|
+
|
35
|
+
extractAndInsertForm formPath, rapper
|
36
|
+
|
37
|
+
|
38
|
+
$(document).on 'click', 'a.button.polymorphic_has_many_remove', (e)->
|
39
|
+
e.preventDefault()
|
40
|
+
parent = $(@).closest '.polymorphic_has_many_container'
|
41
|
+
to_remove = $(@).closest 'fieldset'
|
42
|
+
recompute_positions parent
|
43
|
+
|
44
|
+
parent.trigger 'polymorphic_has_many_remove:before', [to_remove, parent]
|
45
|
+
to_remove.remove()
|
46
|
+
parent.trigger 'polymorphic_has_many_remove:after', [to_remove, parent]
|
47
|
+
|
48
|
+
$(document).on 'click', 'a.button.polymorphic_has_many_add', (e)->
|
49
|
+
e.preventDefault()
|
50
|
+
parent = $(@).closest '.polymorphic_has_many_container'
|
51
|
+
parent.trigger before_add = $.Event('polymorphic_has_many_add:before'), [parent]
|
52
|
+
|
53
|
+
unless before_add.isDefaultPrevented()
|
54
|
+
index = parent.data('polymorphic_has_many_index') || parent.children('fieldset').length - 1
|
55
|
+
parent.data has_many_index: ++index
|
56
|
+
|
57
|
+
regex = new RegExp $(@).data('placeholder'), 'g'
|
58
|
+
html = $(@).data('html').replace regex, index
|
59
|
+
|
60
|
+
fieldset = $(html).insertBefore(@)
|
61
|
+
recompute_positions parent
|
62
|
+
parent.trigger 'polymorphic_has_many_add:after', [fieldset, parent]
|
63
|
+
|
64
|
+
init_polymorphic_sortable()
|
65
|
+
|
66
|
+
|
67
|
+
$('.polymorphic_has_many_container').on 'change', '.polymorphic_type_select', (event) ->
|
68
|
+
fieldset = $(this).closest 'fieldset'
|
69
|
+
|
70
|
+
selectedOption = $(this).find 'option:selected'
|
71
|
+
formPath = selectedOption.data 'path'
|
72
|
+
|
73
|
+
label = $(this).prev 'label'
|
74
|
+
label.remove()
|
75
|
+
|
76
|
+
hiddenField = $('<input type="hidden" />')
|
77
|
+
hiddenField.attr 'name', $(this).attr('name')
|
78
|
+
hiddenField.val $(this).val()
|
79
|
+
|
80
|
+
$(this).replaceWith hiddenField
|
81
|
+
|
82
|
+
newListItem = $ '<li>'
|
83
|
+
|
84
|
+
extractAndInsertForm formPath, fieldset
|
85
|
+
|
86
|
+
init_polymorphic_sortable = ->
|
87
|
+
elems = $('.polymorphic_has_many_container[data-sortable]:not(.ui-sortable)')
|
88
|
+
|
89
|
+
elems.sortable
|
90
|
+
axis: 'y'
|
91
|
+
items: '> fieldset',
|
92
|
+
handle: '> ol > .handle',
|
93
|
+
stop: recompute_positions
|
94
|
+
elems.each recompute_positions
|
95
|
+
|
96
|
+
# Removes relations if id or type is not specified
|
97
|
+
# For example when user clicked add relation button, but didn't selected type
|
98
|
+
stripEmptyRelations = ->
|
99
|
+
$('.polymorphic_has_many_fields input:hidden').each ->
|
100
|
+
if $(@).val() == ""
|
101
|
+
$(@).parents('.polymorphic_has_many_fields').remove()
|
102
|
+
|
103
|
+
recompute_positions = (parent)->
|
104
|
+
parent = if parent instanceof jQuery then parent else $(@)
|
105
|
+
input_name = parent.data 'sortable'
|
106
|
+
position = parseInt(parent.data('sortable-start') || 0, 10)
|
107
|
+
|
108
|
+
parent.children('fieldset').each ->
|
109
|
+
# We ignore nested inputs, so when defining your has_many, be sure to keep
|
110
|
+
# your sortable input at the root of the has_many block.
|
111
|
+
destroy_input = $(@).find "> ol > .input > :input[name$='[_destroy]']"
|
112
|
+
sortable_input = $(@).find "> ol > .input > :input[name$='[#{input_name}]']"
|
113
|
+
|
114
|
+
if sortable_input.length
|
115
|
+
sortable_input.val if destroy_input.is ':checked' then '' else position++
|
116
|
+
|
117
|
+
window.extractAndInsertForm= (url, target)->
|
118
|
+
target = $ target
|
119
|
+
|
120
|
+
$.get url, (data)->
|
121
|
+
elements = $(data)
|
122
|
+
form = $('#main_content form', elements).first()
|
123
|
+
$(form).find('.actions').remove()
|
124
|
+
$(form).on 'submit', -> return false
|
125
|
+
|
126
|
+
target.prepend form
|
127
|
+
|
128
|
+
window.loadErrors = (target) ->
|
129
|
+
$(target).off('ajax:success') # unbind successfull action for json form
|
130
|
+
$(target).trigger('submit.rails').on 'ajax:success', (event, data, result) ->
|
131
|
+
# duplicates method above. refactor using callbacks
|
132
|
+
elements = $(data)
|
133
|
+
form = $('#main_content form', elements).first()
|
134
|
+
$(form).find('.actions').remove()
|
135
|
+
$(form).on 'submit', -> return false
|
136
|
+
|
137
|
+
$(target).replaceWith(form)
|
138
|
+
|
139
|
+
|
140
|
+
window.remoteSubmit = (target, callback)->
|
141
|
+
$(target).data('remote', true)
|
142
|
+
$(target).removeAttr('novalidate')
|
143
|
+
action = $(target).attr('action')
|
144
|
+
$(target).find("input[type=file]").remove()
|
145
|
+
# we gonna burn in hell for that
|
146
|
+
# perhaps we can use ajax:before callback
|
147
|
+
# to set type json
|
148
|
+
action_with_json = action + '.json'
|
149
|
+
$(target).attr('action', action_with_json)
|
150
|
+
|
151
|
+
# unbind callbacks action for form if it was submitted before
|
152
|
+
$(target).off('ajax:success').off('ajax:aborted:file').off('ajax:error')
|
153
|
+
|
154
|
+
$(target).trigger('submit.rails')
|
155
|
+
.on 'ajax:aborted:file', (inputs) ->
|
156
|
+
false
|
157
|
+
.on 'ajax:error', (event, response, status)->
|
158
|
+
$(target).attr('action', action)
|
159
|
+
if response.status == 422
|
160
|
+
loadErrors(target)
|
161
|
+
.on 'ajax:success', (event, object, status, response) ->
|
162
|
+
$(target).attr('action', action)
|
163
|
+
if response.status == 201 # created
|
164
|
+
$(target).next().find('input:first').val(object.id)
|
165
|
+
# replace new form with edit form
|
166
|
+
# to update form method to PATCH and form action
|
167
|
+
url = "#{action}/#{object.id}/edit"
|
168
|
+
extractAndInsertForm(url, $(target).parent('fieldset'))
|
169
|
+
$(target).remove()
|
170
|
+
|
171
|
+
callback()
|
172
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
form .polymorphic_has_many_container fieldset
|
2
|
+
position: relative
|
3
|
+
|
4
|
+
form .polymorphic_has_many_container fieldset:not(:last-of-type)
|
5
|
+
border-bottom: 1px solid lightgray
|
6
|
+
margin-bottom: 20px
|
7
|
+
|
8
|
+
.input.refile img.image-preview
|
9
|
+
margin: 0.5em 0 0 20%
|
10
|
+
display: block
|
11
|
+
|
12
|
+
form .polymorphic_has_many_container .handle
|
13
|
+
position: absolute
|
14
|
+
top: calc(50% - 60px)
|
15
|
+
right: 2px
|
16
|
+
padding: 0
|
17
|
+
cursor: move
|
18
|
+
|
19
|
+
&.ui-sortable-handle
|
20
|
+
top: calc(50% - 42px)
|
21
|
+
|
22
|
+
span.icon
|
23
|
+
width: 3em
|
24
|
+
height: 3em
|
25
|
+
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module ActiveadminPolymorphic
|
2
|
+
class FormBuilder < ::ActiveAdmin::FormBuilder
|
3
|
+
def polymorphic_has_many(assoc, poly_name, options = {}, &block)
|
4
|
+
custom_settings = :new_record, :allow_destroy, :heading, :sortable, :sortable_start, :types, :path_prefix
|
5
|
+
builder_options = {new_record: true, path_prefix: :admin}.merge! options.slice *custom_settings
|
6
|
+
|
7
|
+
options = {for: assoc }.merge! options.except *custom_settings
|
8
|
+
options[:class] = [options[:class], "polymorphic_has_many_fields"].compact.join(' ')
|
9
|
+
sortable_column = builder_options[:sortable]
|
10
|
+
sortable_start = builder_options.fetch(:sortable_start, 0)
|
11
|
+
|
12
|
+
html = "".html_safe
|
13
|
+
html << template.capture do
|
14
|
+
contents = "".html_safe
|
15
|
+
|
16
|
+
block = polymorphic_form(poly_name, builder_options)
|
17
|
+
|
18
|
+
template.assign('polymorphic_has_many_block' => true)
|
19
|
+
contents = without_wrapper { inputs(options, &block) }
|
20
|
+
|
21
|
+
if builder_options[:new_record]
|
22
|
+
contents << js_for_polymorphic_has_many(
|
23
|
+
assoc, poly_name, template, builder_options, options[:class]
|
24
|
+
)
|
25
|
+
else
|
26
|
+
contents
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
tag = @already_in_an_inputs_block ? :li : :div
|
31
|
+
html = template.content_tag(tag, html, class: "polymorphic_has_many_container #{assoc}", 'data-sortable' => sortable_column, 'data-sortable-start' => sortable_start)
|
32
|
+
template.concat(html) if template.output_buffer
|
33
|
+
html
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def polymorphic_has_many_actions(has_many_form, builder_options, contents)
|
39
|
+
if has_many_form.object.new_record?
|
40
|
+
contents << template.content_tag(:li) do
|
41
|
+
template.link_to I18n.t('active_admin.has_many_remove'),
|
42
|
+
"#", class: 'button polymorphic_has_many_remove'
|
43
|
+
end
|
44
|
+
elsif builder_options[:allow_destroy]
|
45
|
+
contents << has_many_form.input(:_destroy, as: :boolean,
|
46
|
+
wrapper_html: {class: 'polymorphic_has_many_delete'},
|
47
|
+
label: I18n.t('active_admin.has_many_delete'))
|
48
|
+
end
|
49
|
+
|
50
|
+
if builder_options[:sortable]
|
51
|
+
contents << has_many_form.input(builder_options[:sortable], as: :hidden)
|
52
|
+
|
53
|
+
contents << template.content_tag(:li, class: 'handle') do
|
54
|
+
::ActiveAdmin::Iconic.icon :move_vertical
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
contents
|
59
|
+
end
|
60
|
+
|
61
|
+
def js_for_polymorphic_has_many(assoc, poly_name, template, builder_options, class_string)
|
62
|
+
new_record = builder_options[:new_record]
|
63
|
+
assoc_reflection = object.class.reflect_on_association assoc
|
64
|
+
assoc_name = assoc_reflection.klass.model_name
|
65
|
+
placeholder = "NEW_#{assoc_name.to_s.underscore.upcase.gsub(/\//, '_')}_RECORD"
|
66
|
+
|
67
|
+
text = new_record.is_a?(String) ? new_record : I18n.t('active_admin.has_many_new', model: assoc_name.human)
|
68
|
+
form_block = polymorphic_form(poly_name, builder_options, true)
|
69
|
+
|
70
|
+
opts = {
|
71
|
+
for: [assoc, assoc_reflection.klass.new],
|
72
|
+
class: class_string,
|
73
|
+
for_options: { child_index: placeholder }
|
74
|
+
}
|
75
|
+
|
76
|
+
html = "".html_safe
|
77
|
+
html << template.capture do
|
78
|
+
inputs_for_nested_attributes opts, &form_block
|
79
|
+
end
|
80
|
+
|
81
|
+
template.link_to text, '#', class: "button polymorphic_has_many_add", data: {
|
82
|
+
html: CGI.escapeHTML(html).html_safe, placeholder: placeholder
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def polymorphic_options(builder_options)
|
87
|
+
# add internationalization
|
88
|
+
builder_options[:types].each_with_object([]) do |model, options|
|
89
|
+
options << [
|
90
|
+
model.model_name.human, model,
|
91
|
+
{"data-path" => form_new_path(model, builder_options) }
|
92
|
+
]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def polymorphic_form(poly_name, builder_options, for_js = false)
|
97
|
+
proc do |f|
|
98
|
+
html = "".html_safe
|
99
|
+
html << f.input("#{poly_name}_id", as: :hidden)
|
100
|
+
|
101
|
+
if f.object.send(poly_name).nil?
|
102
|
+
html << f.input("#{poly_name}_type", input_html: { class: 'polymorphic_type_select' }, as: :select, collection: polymorphic_options(builder_options))
|
103
|
+
else
|
104
|
+
html << f.input(
|
105
|
+
"#{poly_name}_type", as: :hidden,
|
106
|
+
input_html: {"data-path" => form_edit_path(f.object.send(poly_name), builder_options) }
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
html << polymorphic_has_many_actions(f, builder_options, "".html_safe)
|
111
|
+
|
112
|
+
html
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def form_new_path(object, builder_options)
|
117
|
+
"/#{builder_options[:path_prefix]}/#{ActiveModel::Naming.plural(object)}/new"
|
118
|
+
end
|
119
|
+
|
120
|
+
def form_edit_path(object, builder_options)
|
121
|
+
"/#{builder_options[:path_prefix]}/#{ActiveModel::Naming.plural(object)}/#{object.id}/edit"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|