para 0.9.0 → 0.9.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|