para 0.9.0 → 0.9.3.3
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 +4 -4
- data/app/assets/javascripts/para/inputs/nested_many.coffee +14 -8
- data/app/controllers/para/admin/exports_controller.rb +1 -1
- data/app/controllers/para/admin/nested_forms_controller.rb +1 -0
- data/app/views/para/admin/nested_forms/show.html.haml +10 -1
- data/app/views/para/admin/resources/_navigation.html.haml +6 -3
- data/app/views/para/admin/resources/_remote_nested_form.html.haml +1 -1
- data/app/views/para/inputs/_nested_many.html.haml +1 -0
- data/app/views/para/inputs/_nested_one.html.haml +2 -2
- data/app/views/para/inputs/nested_many/_container.html.haml +1 -1
- data/app/views/para/inputs/nested_one/_container.html.haml +1 -1
- data/lib/para/cloneable/attachments_cloner.rb +74 -20
- data/lib/para/cloneable/include_tree_builder.rb +163 -34
- data/lib/para/cloneable.rb +20 -4
- data/lib/para/components_configuration.rb +12 -4
- data/lib/para/engine.rb +1 -0
- data/lib/para/ext/deep_cloneable.rb +26 -0
- data/lib/para/ext.rb +1 -0
- data/lib/para/form_builder/nested_form.rb +8 -0
- data/lib/para/inputs/nested_many_input.rb +3 -1
- data/lib/para/version.rb +3 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f742ef6484e0ccd595c1a79e219f4cb2a03c72a7fbf9f584990c638412f19e0
|
4
|
+
data.tar.gz: 8c231925d6c2ecc4d0eb7208c5c4b530410d1a30d8a1276d849f31d3e52f5f76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e42df31fcb4d4d490f5b5127939494fbc9e07855ee1123fd720f089c2e36737b4e1f706b347d36699270ed00eeeb4a228ca81050bdc8ae35b84bbde8a7dfd7b6
|
7
|
+
data.tar.gz: 707696ad2b1f6a2c43f6944f6543ca5dd8dd5a17c3c91942d0d3c38b43e5d2c652d1b76a35409c2de0019c11658f81a6922931d3e4126a6490a77ed189bd8e8a
|
@@ -5,7 +5,7 @@ class Para.NestedManyField
|
|
5
5
|
@initializeOrderable()
|
6
6
|
@initializeCocoon()
|
7
7
|
|
8
|
-
@$field.on 'shown.bs.collapse',
|
8
|
+
@$field.on 'shown.bs.collapse', @stoppingPropagation(@collapseShown)
|
9
9
|
|
10
10
|
initializeOrderable: ->
|
11
11
|
@orderable = @$field.hasClass('orderable')
|
@@ -21,11 +21,17 @@ class Para.NestedManyField
|
|
21
21
|
$(el).find('.resource-position-field').val(i)
|
22
22
|
|
23
23
|
initializeCocoon: ->
|
24
|
-
@$fieldsList.on 'cocoon:after-insert',
|
25
|
-
@$fieldsList.on 'cocoon:before-remove',
|
26
|
-
@$fieldsList.on 'cocoon:after-remove',
|
24
|
+
@$fieldsList.on 'cocoon:after-insert', @stoppingPropagation(@afterInsertField)
|
25
|
+
@$fieldsList.on 'cocoon:before-remove', @stoppingPropagation(@beforeRemoveField)
|
26
|
+
@$fieldsList.on 'cocoon:after-remove', @stoppingPropagation(@afterRemoveField)
|
27
27
|
|
28
|
-
|
28
|
+
stoppingPropagation: (callback) =>
|
29
|
+
(e, args...) =>
|
30
|
+
e.stopPropagation()
|
31
|
+
callback(e, args...)
|
32
|
+
|
33
|
+
|
34
|
+
afterInsertField: (e, $element) =>
|
29
35
|
if ($collapsible = $element.find('[data-open-on-insert="true"]')).length
|
30
36
|
@openInsertedField($collapsible)
|
31
37
|
|
@@ -36,21 +42,21 @@ class Para.NestedManyField
|
|
36
42
|
|
37
43
|
$element.simpleForm()
|
38
44
|
|
39
|
-
beforeRemoveField: (e, $element)
|
45
|
+
beforeRemoveField: (e, $element) =>
|
40
46
|
$nextEl = $element.next()
|
41
47
|
# Remove attributes mappings field for new records since it will try to
|
42
48
|
# create an empty nested resource otherwise
|
43
49
|
$nextEl.remove() if $nextEl.is('[data-attributes-mappings]') and not $element.is('[data-persisted]')
|
44
50
|
|
45
51
|
# When a sub field is removed, update every sub field position
|
46
|
-
afterRemoveField:
|
52
|
+
afterRemoveField: =>
|
47
53
|
@handleOrderingUpdated();
|
48
54
|
|
49
55
|
openInsertedField: ($field) ->
|
50
56
|
$target = $($field.attr('href'))
|
51
57
|
$target.collapse('show')
|
52
58
|
|
53
|
-
collapseShown: (e)
|
59
|
+
collapseShown: (e) =>
|
54
60
|
$target = $(e.target)
|
55
61
|
|
56
62
|
if $target.is("[data-rendered]") or $target.data("rendered")
|
@@ -1,6 +1,15 @@
|
|
1
1
|
- nested_form = nil
|
2
2
|
|
3
|
-
|
3
|
+
-# Initialize an unredered form builder that will only be used to create the nested fields
|
4
|
+
-# by using the yielded block form builder argument.
|
5
|
+
-#
|
6
|
+
-# The form's object name is set through the provided :object_name param to ensure that
|
7
|
+
-# the form inputs are named correctly in the loading form.
|
8
|
+
-#
|
9
|
+
-# Builder options allow to customize the options passed to the form builder to customize
|
10
|
+
-# the rendered partials.
|
11
|
+
-#
|
12
|
+
- para_form_for(@object, @builder_options.merge(url: "")) do |form|
|
4
13
|
- form.object_name = @object_name
|
5
14
|
- nested_form = capture do
|
6
15
|
= render(partial: find_partial_for(@model, :remote_nested_form), locals: { form: form, model: @model, object: @object, object_name: @object_name })
|
@@ -1,10 +1,13 @@
|
|
1
1
|
.top-nav-tabs-affix-placeholder
|
2
2
|
%ul.top-nav-tabs-affix.nav.nav-tabs{ role: "tablist", "data-top-level-affix": true }
|
3
|
-
|
4
|
-
|
5
|
-
= parent_component.
|
3
|
+
- if can?(:manage, parent_component) && show_component?(parent_component)
|
4
|
+
%li{ class: ("active" if active_component == parent_component) }
|
5
|
+
= link_to parent_component.path do
|
6
|
+
= parent_component.name
|
6
7
|
|
7
8
|
- parent_component.child_components.each do |child_component|
|
9
|
+
- next unless can?(:manage, child_component) && show_component?(child_component)
|
10
|
+
|
8
11
|
%li{ class: ("active" if active_component == child_component) }
|
9
12
|
= link_to child_component.path do
|
10
13
|
= child_component.name
|
@@ -1 +1 @@
|
|
1
|
-
= render partial: find_partial_for(model,
|
1
|
+
= render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: form }
|
@@ -2,6 +2,7 @@
|
|
2
2
|
.fields-list{ id: dom_identifier }
|
3
3
|
= form.simple_fields_for attribute_name, resources, nested_attribute_name: attribute_name, orderable: orderable, track_attribute_mappings: render_partial do |nested_form|
|
4
4
|
= render partial: find_partial_for(model, 'nested_many/container', partial_dir: 'inputs'), locals: { form: nested_form, model: nested_form.object.class, subclass: subclass, nested_locals: nested_locals, inset: inset, uncollapsed: uncollapsed, render_partial: render_partial, remote_partial_params: remote_partial_params }
|
5
|
+
|
5
6
|
-# Add button
|
6
7
|
- if add_button
|
7
8
|
- if subclass
|
@@ -6,7 +6,7 @@
|
|
6
6
|
= render partial: find_partial_for(model, 'nested_one/container', partial_dir: 'inputs'), locals: { nested_form: nested_form, form: form, model: nested_form.object.class, subclass: subclass, nested_locals: nested_locals }
|
7
7
|
|
8
8
|
- else
|
9
|
-
= render partial: find_partial_for(model,
|
9
|
+
= render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: nested_form, parent: form.object }.merge(nested_locals)
|
10
10
|
|
11
11
|
- if defined?(subclass) && subclass
|
12
|
-
= render partial: 'para/inputs/nested_one/add_with_subclasses', locals: { form: form, model: model, dom_identifier: dom_identifier, nested_locals: nested_locals, attribute_name: attribute_name, subclasses: subclasses, add_button_label: add_button_label, add_button_class: add_button_class, subclass: subclass }
|
12
|
+
= render partial: 'para/inputs/nested_one/add_with_subclasses', locals: { form: form, model: model, dom_identifier: dom_identifier, nested_locals: nested_locals, attribute_name: attribute_name, subclasses: subclasses, add_button_label: add_button_label, add_button_class: add_button_class, subclass: subclass }
|
@@ -14,7 +14,7 @@
|
|
14
14
|
.panel-collapse.form-inputs.collapse{ id: form.nested_resource_dom_id, class: ('in' if uncollapsed && form.object.persisted?), data: { rendered: render_partial, render_path: @component.path(remote_partial_params), id: form.object.id, :"object-name" => form.object_name, :"model-name" => model.name } }
|
15
15
|
.panel-body{ data: { :"nested-form-container" => true } }
|
16
16
|
- if render_partial
|
17
|
-
= render partial: find_partial_for(model,
|
17
|
+
= render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: form }.merge(nested_locals)
|
18
18
|
- else
|
19
19
|
= fa_icon "spinner spin"
|
20
20
|
|
@@ -6,4 +6,4 @@
|
|
6
6
|
%i.fa.fa-angle-up
|
7
7
|
|
8
8
|
.panel-body.panel-collapse.form-inputs.collapse{ id: nested_form.nested_resource_dom_id }
|
9
|
-
= render partial: find_partial_for(model,
|
9
|
+
= render partial: find_partial_for(model, form.nested_fields_partial_name), locals: { form: nested_form, parent: nested_form.object }.merge(nested_locals)
|
@@ -1,36 +1,90 @@
|
|
1
1
|
module Para
|
2
2
|
module Cloneable
|
3
3
|
class AttachmentsCloner
|
4
|
-
attr_reader :original, :clone
|
4
|
+
attr_reader :original, :clone, :dictionary
|
5
5
|
|
6
|
-
|
6
|
+
# Handle both one and many attachment relations
|
7
|
+
ATTACHMENTS_RELATION_REGEX = /_attachments?\z/
|
8
|
+
|
9
|
+
def initialize(original, clone, dictionary)
|
7
10
|
@original = original
|
8
11
|
@clone = clone
|
12
|
+
@dictionary = dictionary
|
9
13
|
end
|
10
|
-
|
14
|
+
|
11
15
|
def clone!
|
12
16
|
return unless defined?(ActiveStorage)
|
13
|
-
|
14
|
-
attachment_reflections = original.class.reflections.select
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
|
18
|
+
attachment_reflections = original.class.reflections.select do |name, reflection|
|
19
|
+
name.to_s.match(ATTACHMENTS_RELATION_REGEX) &&
|
20
|
+
reflection.options[:class_name] == "ActiveStorage::Attachment"
|
21
|
+
end
|
18
22
|
|
19
23
|
attachment_reflections.each do |name, reflection|
|
20
|
-
|
21
|
-
next unless
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
content_type: original_attachment.blob.content_type
|
30
|
-
})
|
24
|
+
association_target = original.send(name)
|
25
|
+
next unless association_target
|
26
|
+
|
27
|
+
if reflection.collection?
|
28
|
+
association_target.each do |attachment|
|
29
|
+
clone_attachment(name, attachment)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
clone_attachment(name, association_target)
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
36
|
+
|
37
|
+
def clone_attachment(name, original_attachment)
|
38
|
+
association_name = name.gsub(ATTACHMENTS_RELATION_REGEX, "")
|
39
|
+
original_blob = original_attachment.blob
|
40
|
+
|
41
|
+
# Handle missing file in storage service by bypassing the attachment cloning
|
42
|
+
return unless ActiveStorage::Blob.service.exist?(original_blob&.key)
|
43
|
+
|
44
|
+
Para::ActiveStorageDownloader.new(original_attachment).download_blob_to_tempfile do |tempfile|
|
45
|
+
attachment_target = clone.send(association_name)
|
46
|
+
|
47
|
+
attachment_target.attach({
|
48
|
+
io: tempfile,
|
49
|
+
filename: original_blob.filename,
|
50
|
+
content_type: original_blob.content_type
|
51
|
+
})
|
52
|
+
|
53
|
+
cloned_attachment = find_cloned_attachment(attachment_target, original_blob)
|
54
|
+
|
55
|
+
# Store the cloned attachment and blob into the deep_cloneable dictionary used
|
56
|
+
# by the `deep_clone` method to ensure that, if needed during the cloning
|
57
|
+
# operation, they won't be cloned once more and are accessible for processing
|
58
|
+
store_cloned(original_attachment, cloned_attachment)
|
59
|
+
store_cloned(original_blob, cloned_attachment.blob)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Seemlessly handle one and many attachment relations return values and fetch
|
64
|
+
# the attachment that we just cloned by comparing blobs checksum, as depending
|
65
|
+
# which ActiveStorage version we're on (Rails 5.2 or 6), the `#attach` method
|
66
|
+
# doesn't always return the same, so for now we still handle the Rails 5.2 case.
|
67
|
+
def find_cloned_attachment(attachment_target, original_blob)
|
68
|
+
attachments = if attachment_target.attachments.any?
|
69
|
+
attachment_target.attachments
|
70
|
+
else
|
71
|
+
[attachment_target.attachment]
|
72
|
+
end
|
73
|
+
|
74
|
+
attachment = attachments.find do |att|
|
75
|
+
att.blob.checksum == original_blob.checksum
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# This stores the source and clone resources into the deep_clone dictionary, which
|
80
|
+
# simulates what the deep_cloneable gem does when it clones a resource
|
81
|
+
#
|
82
|
+
def store_cloned(source, clone)
|
83
|
+
store_key = source.class.name.tableize.to_sym
|
84
|
+
|
85
|
+
dictionary[store_key] ||= {}
|
86
|
+
dictionary[store_key][source] = clone
|
87
|
+
end
|
34
88
|
end
|
35
89
|
end
|
36
|
-
end
|
90
|
+
end
|
@@ -1,67 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Para
|
2
4
|
module Cloneable
|
5
|
+
# This object acts as a service to compile a nested cloneable options hash to be
|
6
|
+
# provided to the `deep_clone` method from the `deep_cloneable` gem. It iterates over
|
7
|
+
# every reflections that must be included for a given model when it's cloned, and
|
8
|
+
# creates a nested hash of :include and :except directives based on the tree that
|
9
|
+
# is created by nested `acts_as_cloneable` calls on the different models of the
|
10
|
+
# application
|
11
|
+
#
|
12
|
+
# Example :
|
13
|
+
#
|
14
|
+
# Given the following model structure :
|
15
|
+
#
|
16
|
+
# class Article < ApplicationRecord
|
17
|
+
# acts_as_cloneable :category, :comments, except: [:publication_date]
|
18
|
+
#
|
19
|
+
# belongs_to :category
|
20
|
+
# has_many :comments
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class Category < ApplicationRecord
|
24
|
+
# acts_as_cloneable :category, except: [:articles_count]
|
25
|
+
#
|
26
|
+
# has_many :articles
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class Comment < ApplicationRecord
|
30
|
+
# acts_as_cloneable :author
|
31
|
+
#
|
32
|
+
# belongs_to :article
|
33
|
+
# belongs_to :author
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# class Author < ApplicationRecord
|
37
|
+
# acts_as_cloneable except: [:email]
|
38
|
+
#
|
39
|
+
# has_many :articles
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# The behavior would be :
|
43
|
+
#
|
44
|
+
# Para::Cloneable::IncludeTreeBuilder.new(article).build
|
45
|
+
# # => {
|
46
|
+
# include: [:category, { comments: :author }],
|
47
|
+
# except: [:publication_date, {
|
48
|
+
# category: [:articles_count],
|
49
|
+
# comments: { author: [:email] }
|
50
|
+
# }]
|
51
|
+
# }
|
52
|
+
#
|
3
53
|
class IncludeTreeBuilder
|
4
|
-
attr_reader :resource,
|
54
|
+
attr_reader :resource, :cloneable_options
|
5
55
|
|
6
|
-
def initialize(resource
|
56
|
+
def initialize(resource)
|
7
57
|
@resource = resource
|
8
|
-
@cloneable_options = cloneable_options.deep_dup
|
58
|
+
@cloneable_options = resource.cloneable_options.deep_dup
|
9
59
|
end
|
10
|
-
|
60
|
+
|
11
61
|
def build
|
12
|
-
|
13
|
-
|
14
|
-
|
62
|
+
options_tree = build_cloneable_options_tree(resource)
|
63
|
+
exceptions = extract_exceptions_from(options_tree)
|
64
|
+
inclusions = clean_options_tree(options_tree)
|
65
|
+
cloneable_options.merge(include: inclusions, except: exceptions)
|
15
66
|
end
|
16
|
-
|
67
|
+
|
17
68
|
private
|
18
69
|
|
19
|
-
|
20
|
-
|
70
|
+
# The cloneable options tree iterates over the resources' relations that are
|
71
|
+
# declared as included in the cloneable_options of the provided resource, and
|
72
|
+
# recursively checks included relations for its associated resources.
|
73
|
+
#
|
74
|
+
# It returns a nested hash with the included relations and their :except array
|
75
|
+
# if it exist, which include the attributes that shouldn't be duplicated when
|
76
|
+
# the resource is cloned.
|
77
|
+
#
|
78
|
+
def build_cloneable_options_tree(resource, path = [])
|
79
|
+
cloneable_options = resource.cloneable_options
|
80
|
+
|
81
|
+
# Iterate over the resource's cloneable options' :include array and recursively
|
82
|
+
# add nested included resources to its own included resources.
|
83
|
+
options = cloneable_options[:include].each_with_object({}) do |reflection_name, hash|
|
84
|
+
# This avoids cyclic dependencies issues by stopping nested association
|
85
|
+
# inclusions before the cycle starts.
|
86
|
+
#
|
87
|
+
# For example, if a post includes its author, and the author includes its posts,
|
88
|
+
# this would make the system fail with a stack level too deep error. Here this
|
89
|
+
# guard allows the inclusion to stop at :
|
90
|
+
#
|
91
|
+
# { posts: { author: { posts: { author: {}}}}}
|
92
|
+
#
|
93
|
+
# Which ensures that, using the dictionary strategy of deep_cloneable, all
|
94
|
+
# posts' authors' posts will have their author mapped to an already cloned
|
95
|
+
# author when it comes to cloning the "author" 4th level of the include tree.
|
96
|
+
#
|
97
|
+
# This is not the most optimized solution, but works well enough as if the
|
98
|
+
# author's posts match previously cloned posts, they won't be cloned as they'll
|
99
|
+
# exist in the cloned resources dictionary.
|
100
|
+
next if path.length >= 4 &&
|
101
|
+
path[-4] == path[-2] &&
|
102
|
+
path[-2] == reflection_name &&
|
103
|
+
path[-3] == path[-1]
|
104
|
+
|
21
105
|
hash[reflection_name] = {}
|
22
106
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
add_reflection_options(
|
107
|
+
unless (reflection = resource.class.reflections[reflection_name.to_s])
|
108
|
+
next
|
109
|
+
end
|
110
|
+
|
111
|
+
reflection_options = hash[reflection_name]
|
112
|
+
association_target = resource.send(reflection_name)
|
113
|
+
|
114
|
+
if reflection.collection?
|
115
|
+
association_target.each do |nested_resource|
|
116
|
+
add_reflection_options(
|
117
|
+
reflection_options,
|
118
|
+
nested_resource,
|
119
|
+
[*path, reflection_name]
|
120
|
+
)
|
33
121
|
end
|
34
|
-
|
122
|
+
else
|
123
|
+
add_reflection_options(
|
124
|
+
reflection_options,
|
125
|
+
association_target,
|
126
|
+
[*path, reflection_name]
|
127
|
+
)
|
128
|
+
end
|
35
129
|
end
|
130
|
+
|
131
|
+
# Add the :except array from the resource to the current options hash and merge
|
132
|
+
# it if one already exist from another resource of the same class.
|
133
|
+
options[:except] ||= []
|
134
|
+
options[:except] |= Array.wrap(cloneable_options[:except])
|
135
|
+
|
136
|
+
options
|
36
137
|
end
|
37
138
|
|
38
|
-
def add_reflection_options(reflection_options, nested_resource)
|
139
|
+
def add_reflection_options(reflection_options, nested_resource, path)
|
39
140
|
options = nested_resource.class.try(:cloneable_options)
|
40
141
|
return reflection_options unless options
|
41
|
-
|
42
|
-
|
43
|
-
target_options = build_cloneable_tree(nested_resource, include_options)
|
142
|
+
|
143
|
+
target_options = build_cloneable_options_tree(nested_resource, path)
|
44
144
|
reflection_options.deep_merge!(target_options)
|
45
145
|
end
|
46
146
|
|
47
|
-
|
147
|
+
# Iterates over the generated options tree to extract all the nested :except options
|
148
|
+
# into their own separate hash, removing :except keys from the original options
|
149
|
+
# tree hash.
|
150
|
+
#
|
151
|
+
def extract_exceptions_from(tree)
|
152
|
+
exceptions = tree.delete(:except) || []
|
153
|
+
nested_exceptions = {}
|
154
|
+
|
155
|
+
tree.each do |key, value|
|
156
|
+
next unless value.is_a?(Hash) && !value.empty?
|
157
|
+
|
158
|
+
sub_exceptions = extract_exceptions_from(value)
|
159
|
+
nested_exceptions[key] = sub_exceptions unless sub_exceptions.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
exceptions += [nested_exceptions] unless nested_exceptions.empty?
|
163
|
+
exceptions
|
164
|
+
end
|
165
|
+
|
166
|
+
# Iterates over the remaining options tree hash and converts empty hash values' keys
|
167
|
+
# to be stored in an array, and returns an array of symbols and hashes that is
|
168
|
+
# compatible with what is expected as argument for the :include option of the
|
169
|
+
# `deep_clone` method.
|
170
|
+
#
|
171
|
+
# Example :
|
172
|
+
#
|
173
|
+
# clean_options_tree({ category: {}, comments: { author: {} } })
|
174
|
+
# # => [:category, { comments: [:author] }]
|
175
|
+
#
|
176
|
+
def clean_options_tree(tree)
|
48
177
|
shallow_relations = []
|
49
178
|
deep_relations = {}
|
50
|
-
|
179
|
+
|
51
180
|
tree.each do |key, value|
|
181
|
+
# If the value is an empty hash, consider it as a shallow relation and add
|
182
|
+
# it to the shallow relations array
|
52
183
|
if !value || value.empty?
|
53
184
|
shallow_relations << key
|
185
|
+
# If the value is a hash with nested keys, process its nested values and add
|
186
|
+
# the result to the deep relations hash
|
54
187
|
else
|
55
|
-
deep_relations[key] =
|
188
|
+
deep_relations[key] = clean_options_tree(value)
|
56
189
|
end
|
57
190
|
end
|
58
191
|
|
59
|
-
|
60
|
-
shallow_relations
|
61
|
-
else
|
62
|
-
shallow_relations + [deep_relations]
|
63
|
-
end
|
192
|
+
deep_relations.empty? ? shallow_relations : shallow_relations + [deep_relations]
|
64
193
|
end
|
65
194
|
end
|
66
195
|
end
|
67
|
-
end
|
196
|
+
end
|
data/lib/para/cloneable.rb
CHANGED
@@ -17,12 +17,14 @@ module Para
|
|
17
17
|
# macro.
|
18
18
|
#
|
19
19
|
def deep_clone!(options = {})
|
20
|
-
|
20
|
+
dictionary = options[:dictionary] ||= {}
|
21
|
+
|
22
|
+
processed_options = Para::Cloneable::IncludeTreeBuilder.new(self).build
|
21
23
|
options = options.reverse_merge(processed_options)
|
22
24
|
callback = build_clone_callback(options.delete(:prepare))
|
23
|
-
|
25
|
+
|
24
26
|
deep_clone(options) do |original, clone|
|
25
|
-
Para::Cloneable::AttachmentsCloner.new(original, clone).clone!
|
27
|
+
Para::Cloneable::AttachmentsCloner.new(original, clone, dictionary).clone!
|
26
28
|
callback&.call(original, clone)
|
27
29
|
end
|
28
30
|
end
|
@@ -63,9 +65,23 @@ module Para
|
|
63
65
|
# if other sibling models don't define those relations
|
64
66
|
options[:skip_missing_associations] = true
|
65
67
|
|
66
|
-
|
68
|
+
# If `acts_as_cloneable` is called multiple times, for example by a parent class
|
69
|
+
# and the by its subclass, ensure that we don't lose the previously defined
|
70
|
+
# cloneable options, to avoid having to repeat the same include options in each
|
71
|
+
# subclass, if it has to define subclass specific cloneable options.
|
72
|
+
previous_cloneable_options = cloneable_options || {}
|
73
|
+
|
74
|
+
# Prepare the new cloneable options hash with the provided arguments
|
75
|
+
new_cloneable_options = options.reverse_merge({
|
67
76
|
include: args
|
68
77
|
})
|
78
|
+
|
79
|
+
# Merges previous and new cloneable options into the cloneable_options class
|
80
|
+
# attribute, also merging the `:include` array
|
81
|
+
self.cloneable_options =
|
82
|
+
previous_cloneable_options.merge(new_cloneable_options) do |key, a, b|
|
83
|
+
a.is_a?(Array) && b.is_a?(Array) ? (a + b).uniq : b
|
84
|
+
end
|
69
85
|
end
|
70
86
|
|
71
87
|
def cloneable?
|
@@ -34,16 +34,24 @@ module Para
|
|
34
34
|
def section_for(identifier)
|
35
35
|
if (section = sections_cache[identifier])
|
36
36
|
section
|
37
|
-
|
38
|
-
sections_cache[identifier] =
|
37
|
+
else
|
38
|
+
sections_cache[identifier] = if (section_id = sections_ids_hash[identifier])
|
39
|
+
Para::ComponentSection.find(section_id)
|
40
|
+
else
|
41
|
+
Para::ComponentSection.find_by(identifier: identifier)
|
42
|
+
end
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
42
46
|
def component_for(identifier)
|
43
47
|
if (component = components_cache[identifier])
|
44
48
|
component
|
45
|
-
|
46
|
-
components_cache[identifier] =
|
49
|
+
else
|
50
|
+
components_cache[identifier] = if (component_id = components_ids_hash[identifier])
|
51
|
+
Para::Component::Base.find(component_id)
|
52
|
+
else
|
53
|
+
Para::Component::Base.find_by(identifier: identifier)
|
54
|
+
end
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
data/lib/para/engine.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "deep_cloneable/deep_clone"
|
2
|
+
|
3
|
+
module Para
|
4
|
+
module Ext
|
5
|
+
module DeepCloneExtension
|
6
|
+
# Override the default deep_cloneable method to avoid nested :except rules that target
|
7
|
+
# polymorphic relations to try to assign default values to unexisting attributes on
|
8
|
+
# models that don't define the excluded attribute
|
9
|
+
#
|
10
|
+
# For example, we can have :
|
11
|
+
#
|
12
|
+
# { except: { comments: { author: [:confirmation_token] } } }
|
13
|
+
#
|
14
|
+
# Because some comments have a an author that's a user, and the user `acts_as_cloneable`
|
15
|
+
# macro defines `{ except: [:confirmation_token] }`, but if one of the comments has
|
16
|
+
# an anonymous user in its author relation, this method would faild with a
|
17
|
+
# ActiveModel::MissingAttributeError.
|
18
|
+
#
|
19
|
+
def dup_default_attribute_value_to(kopy, attribute, origin)
|
20
|
+
return unless kopy.attributes.keys.include?(attribute.to_s)
|
21
|
+
|
22
|
+
kopy[attribute] = origin.class.column_defaults.dup[attribute.to_s]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/para/ext.rb
CHANGED
@@ -76,6 +76,14 @@ module Para
|
|
76
76
|
nested? && options[:parent_builder].object
|
77
77
|
end
|
78
78
|
|
79
|
+
# Returns the partial name to be looked up for rendering used inside the nested
|
80
|
+
# fields partials, for the nested fields container and the remote nested fields
|
81
|
+
# partial.
|
82
|
+
#
|
83
|
+
def nested_fields_partial_name
|
84
|
+
:fields
|
85
|
+
end
|
86
|
+
|
79
87
|
private
|
80
88
|
|
81
89
|
def default_resource_name
|
@@ -3,7 +3,7 @@ module Para
|
|
3
3
|
class NestedManyInput < NestedBaseInput
|
4
4
|
attr_reader :resource
|
5
5
|
|
6
|
-
def input(
|
6
|
+
def input(_wrapper_options = nil)
|
7
7
|
input_html_options[:class] << "nested-many"
|
8
8
|
|
9
9
|
orderable = options.fetch(:orderable, model.orderable?)
|
@@ -40,6 +40,8 @@ module Para
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
private
|
44
|
+
|
43
45
|
def parent_model
|
44
46
|
@parent_model ||= @builder.object.class
|
45
47
|
end
|
data/lib/para/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: para
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Valentin Ballestrino
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -766,6 +766,7 @@ files:
|
|
766
766
|
- lib/para/ext.rb
|
767
767
|
- lib/para/ext/active_job_status.rb
|
768
768
|
- lib/para/ext/active_record_nested_attributes.rb
|
769
|
+
- lib/para/ext/deep_cloneable.rb
|
769
770
|
- lib/para/ext/paperclip.rb
|
770
771
|
- lib/para/ext/request_iframe_xhr.rb
|
771
772
|
- lib/para/form_builder.rb
|