para 0.8.5 → 0.8.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/app/assets/javascripts/para/admin.coffee +2 -1
- data/app/assets/javascripts/para/admin/table.coffee +3 -5
- data/app/assets/javascripts/para/admin/tree.coffee +68 -16
- data/app/assets/javascripts/para/inputs/multi-select-input.coffee +2 -3
- data/app/assets/javascripts/para/inputs/nested_many.coffee +10 -6
- data/app/assets/stylesheets/para/admin/src/_list.sass +7 -3
- data/app/helpers/para/admin/nested_inputs_helper.rb +17 -0
- data/app/helpers/para/admin/page_helper.rb +30 -3
- data/app/models/application_record.rb +3 -0
- data/app/models/para/application_record.rb +20 -0
- data/app/models/para/cache/item.rb +1 -1
- data/app/models/para/component/base.rb +16 -2
- data/app/models/para/component_resource.rb +1 -1
- data/app/models/para/component_section.rb +1 -1
- data/app/models/para/library/file.rb +1 -1
- data/app/models/para/page/section.rb +3 -10
- data/app/models/para/page/section_resource.rb +1 -1
- data/app/views/para/admin/resources/_navigation.html.haml +10 -0
- data/app/views/para/admin/resources/_tree.html.haml +4 -0
- data/app/views/para/admin/shared/_navigation.html.haml +1 -1
- data/app/views/para/inputs/_nested_many.html.haml +3 -3
- data/app/views/para/inputs/nested_many/_add.html.haml +1 -1
- data/app/views/para/inputs/nested_many/_add_with_subclasses.html.haml +1 -1
- data/app/views/para/inputs/nested_many/_container.html.haml +1 -1
- data/app/views/para/inputs/nested_one/_add_with_subclasses.html.haml +1 -1
- data/db/migrate/20201210152223_add_parent_component_to_para_components.rb +6 -0
- data/lib/generators/para/install/templates/initializer.rb +9 -1
- data/lib/para/components_configuration.rb +64 -13
- data/lib/para/config.rb +3 -0
- data/lib/para/inputs/nested_many_input.rb +7 -2
- data/lib/para/job/base.rb +2 -2
- data/lib/para/version.rb +1 -1
- data/vendor/assets/javascripts/Sortable.js +4144 -0
- data/vendor/assets/javascripts/jquery.sortable.js +76 -0
- metadata +9 -5
- data/vendor/assets/javascripts/html5-sortable.js +0 -142
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 61c7a5614276ef1baeb61bc3374e309f17b3316e87bcf4694a3fdd5fb57ac3ed
|
4
|
+
data.tar.gz: 7af31c4c81ba3c3ac2b925ee719914e3d781e67c1033d225d668033d2a27243a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c0c2d1301ee1cf60438125c8067062809d05ca5ed5a61d8884f60ff72986bf10f1682a40c2f1fa3c00df81188f8530e950d8eda484d8cd4a179ea41e0843110
|
7
|
+
data.tar.gz: 518d36587de3461721498f6a76660a7c85ffb96eed5f3a8724cd7056855a21e83e509351b444538b6fe101792239ab908540603f9027fbe7960c908f286fb264
|
@@ -6,7 +6,8 @@
|
|
6
6
|
#= require jasny-bootstrap
|
7
7
|
#= require simple_form_extension
|
8
8
|
#= require jquery.scrollto
|
9
|
-
#= require
|
9
|
+
#= require Sortable
|
10
|
+
#= require jquery.sortable
|
10
11
|
#= require cocoon
|
11
12
|
#= require jquery.remote-modal-form
|
12
13
|
#= require jquery.iframe-transport
|
@@ -12,11 +12,9 @@ class Para.ResourceTable
|
|
12
12
|
|
13
13
|
@$tbody.sortable
|
14
14
|
handle: '.order-anchor'
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@$tbody.on('sortupdate', $.proxy(@sortUpdate, this))
|
15
|
+
draggable: 'tr'
|
16
|
+
ghostClass: 'sortable-placeholder'
|
17
|
+
onUpdate: $.proxy(@sortUpdate, this)
|
20
18
|
|
21
19
|
sortUpdate: ->
|
22
20
|
@$tbody.find('tr').each (i, el) ->
|
@@ -6,22 +6,75 @@ class Para.ResourceTree
|
|
6
6
|
@orderUrl = @$el.data('url')
|
7
7
|
@maxDepth = parseInt @$el.data('max-depth')
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
# Each is needed here as the sortable jQuery plugin doesn't loop over each found node
|
10
|
+
# but initializes the tree on the first found element.
|
11
|
+
$(".tree").each(@initializeSubTree)
|
12
|
+
|
13
|
+
initializeSubTree: (_i, el) =>
|
14
|
+
$(el).sortable(
|
15
|
+
group: "tree"
|
16
|
+
handle: ".handle"
|
17
|
+
draggable: ".node"
|
18
|
+
fallbackOnBody: true
|
19
|
+
swapThreshold: 0.65
|
20
|
+
animation: 150
|
21
|
+
onSort: @handleOrderUpdated
|
22
|
+
onMove: @isMovementValid
|
23
|
+
)
|
24
|
+
|
25
|
+
# Note : This method is called often (many times per second while we're dragging) and
|
26
|
+
# takes quite some processing.
|
27
|
+
isMovementValid: (e) =>
|
28
|
+
$movedNode = $(e.dragged)
|
29
|
+
$target = $(e.related)
|
30
|
+
|
31
|
+
# Calculate the deepness of the moved and target nodes
|
32
|
+
movedNodeDeepness = $movedNode.parents(".node").length - 1
|
33
|
+
# If the target is a node, the moved node root deepness is gonna be the same as the
|
34
|
+
# the target one, else the tree's parent node is counted also
|
35
|
+
targetDeepness = $target.parents(".node").length - 1
|
36
|
+
|
37
|
+
# Find the deepest node in the subtree of the moved node
|
38
|
+
$movedNodeSubtrees = $movedNode.find(".tree")
|
39
|
+
movedNodeTreeDeepness = 0
|
40
|
+
|
41
|
+
# The movedNodeTreeDeepness is the maximum deepness of a child node of the current
|
42
|
+
# moved node, relative to the moved node
|
43
|
+
$movedNodeSubtrees.each (i, el) =>
|
44
|
+
subtreeDeepness = $(el).parents(".node").length - 1
|
45
|
+
subtreeRelativeDeepness = subtreeDeepness - movedNodeDeepness
|
46
|
+
movedNodeTreeDeepness = Math.max(movedNodeTreeDeepness, subtreeRelativeDeepness)
|
47
|
+
|
48
|
+
# Calculate the final subtree deepness once we move the whole moved node subtree to
|
49
|
+
# its target position
|
50
|
+
finalSubtreeDeepnessAfterMove = movedNodeTreeDeepness + targetDeepness
|
51
|
+
|
52
|
+
# We finally validate the move only if the final subtree deepness is lower than the
|
53
|
+
# maximum allowed depth
|
54
|
+
finalSubtreeDeepnessAfterMove <= @maxDepth
|
55
|
+
|
56
|
+
handleOrderUpdated: (e) =>
|
57
|
+
|
58
|
+
# Get all involved tree leaves that may include a subtree
|
59
|
+
treeLeaves = [$(e.target), $(e.from), $(e.item).find('.tree')]
|
60
|
+
|
61
|
+
# Update their placeholder display wether they can be a drop target or not
|
62
|
+
@handlePlaceholder($el) for $el in treeLeaves
|
63
|
+
# Save the tree structure on the server
|
18
64
|
@updateOrder()
|
19
65
|
|
66
|
+
# This method checks wether a given tree leaf can be a drop target, depending
|
67
|
+
# on wether it's located at the maximum allowed depth for the tree or not, and adds or
|
68
|
+
# remove a the visual placeholder to indicate its droppable state.
|
69
|
+
#
|
20
70
|
handlePlaceholder: ($el) ->
|
21
71
|
$placeholder = $el.find("> .placeholder")
|
22
|
-
|
72
|
+
parentsCount = $el.parents('.node').length - 1
|
73
|
+
hasChildren = $el.find('> .node').length
|
74
|
+
|
75
|
+
if parentsCount >= @maxDepth or hasChildren
|
23
76
|
$placeholder.hide()
|
24
|
-
$el.children("
|
77
|
+
$el.children(".tree").each (index, el) => @handlePlaceholder($(el))
|
25
78
|
else
|
26
79
|
$placeholder.show()
|
27
80
|
|
@@ -29,9 +82,8 @@ class Para.ResourceTree
|
|
29
82
|
Para.ajax(
|
30
83
|
url: @orderUrl
|
31
84
|
method: 'patch'
|
32
|
-
data:
|
33
|
-
|
34
|
-
success: $.proxy(@orderUpdated, this)
|
85
|
+
data: { resources: @buildOrderedData() }
|
86
|
+
success: @orderUpdated
|
35
87
|
)
|
36
88
|
|
37
89
|
buildOrderedData: ->
|
@@ -41,11 +93,11 @@ class Para.ResourceTree
|
|
41
93
|
data[index] = {
|
42
94
|
id: $this.data("id"),
|
43
95
|
position: index,
|
44
|
-
parent_id: $this.
|
96
|
+
parent_id: $this.parents(".node:first").data("id")
|
45
97
|
}
|
46
98
|
data
|
47
99
|
|
48
|
-
orderUpdated:
|
100
|
+
orderUpdated: =>
|
49
101
|
# TODO: Add flash message to display ordering success
|
50
102
|
|
51
103
|
$(document).on 'page:change turbolinks:load', ->
|
@@ -122,10 +122,9 @@ class Para.MultiSelectInput extends Vertebra.View
|
|
122
122
|
|
123
123
|
@$selectedItems.sortable
|
124
124
|
handle: '.order-anchor'
|
125
|
-
|
126
|
-
placeholder: "<tr><td colspan='#{ columnsCount }'></td></tr>"
|
125
|
+
animation: 150
|
127
126
|
|
128
|
-
@$selectedItems.on('
|
127
|
+
@$selectedItems.on('sort', @selectedItemsSorted)
|
129
128
|
|
130
129
|
selectedItemsSorted: =>
|
131
130
|
indices = {}
|
@@ -13,17 +13,17 @@ class Para.NestedManyField
|
|
13
13
|
|
14
14
|
@$fieldsList.sortable
|
15
15
|
handle: '.order-anchor'
|
16
|
-
|
16
|
+
animation: 150
|
17
|
+
onUpdate: $.proxy(@handleOrderingUpdated, this)
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
sortUpdate: ->
|
21
|
-
@$fieldsList.find('.form-fields').each (i, el) ->
|
19
|
+
handleOrderingUpdated: ->
|
20
|
+
@$fieldsList.find('.form-fields:visible').each (i, el) ->
|
22
21
|
$(el).find('.resource-position-field').val(i)
|
23
22
|
|
24
23
|
initializeCocoon: ->
|
25
24
|
@$fieldsList.on 'cocoon:after-insert', $.proxy(@afterInsertField, this)
|
26
25
|
@$fieldsList.on 'cocoon:before-remove', $.proxy(@beforeRemoveField, this)
|
26
|
+
@$fieldsList.on 'cocoon:after-remove', $.proxy(@afterRemoveField, this)
|
27
27
|
|
28
28
|
afterInsertField: (e, $element) ->
|
29
29
|
if ($collapsible = $element.find('[data-open-on-insert="true"]')).length
|
@@ -32,7 +32,7 @@ class Para.NestedManyField
|
|
32
32
|
if @orderable
|
33
33
|
@$fieldsList.sortable('destroy')
|
34
34
|
@initializeOrderable()
|
35
|
-
@
|
35
|
+
@handleOrderingUpdated()
|
36
36
|
|
37
37
|
$element.simpleForm()
|
38
38
|
|
@@ -42,6 +42,10 @@ class Para.NestedManyField
|
|
42
42
|
# create an empty nested resource otherwise
|
43
43
|
$nextEl.remove() if $nextEl.is('[data-attributes-mappings]') and not $element.is('[data-persisted]')
|
44
44
|
|
45
|
+
# When a sub field is removed, update every sub field position
|
46
|
+
afterRemoveField: ->
|
47
|
+
@handleOrderingUpdated();
|
48
|
+
|
45
49
|
openInsertedField: ($field) ->
|
46
50
|
$target = $($field.attr('href'))
|
47
51
|
$target.collapse('show')
|
@@ -9,7 +9,11 @@
|
|
9
9
|
.actions-control
|
10
10
|
list-style: none
|
11
11
|
padding: 0
|
12
|
-
margin: 0
|
12
|
+
margin: 0 15px 0 0
|
13
|
+
|
14
|
+
&:last-child
|
15
|
+
margin-right: 0
|
16
|
+
|
13
17
|
li
|
14
18
|
+inline-block
|
15
19
|
padding: 0 5px
|
@@ -42,7 +46,7 @@
|
|
42
46
|
|
43
47
|
.page-list-heading
|
44
48
|
border-bottom: 1px solid $gray-light
|
45
|
-
|
49
|
+
|
46
50
|
.page-list-body:last-child
|
47
51
|
padding-bottom: 0
|
48
52
|
|
@@ -76,7 +80,7 @@
|
|
76
80
|
color: $gray
|
77
81
|
border-color: transparent
|
78
82
|
&:hover
|
79
|
-
color: $gray-dark
|
83
|
+
color: $gray-dark
|
80
84
|
|
81
85
|
|
82
86
|
.page-entries-info
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Para
|
2
|
+
module Admin
|
3
|
+
module NestedInputsHelper
|
4
|
+
# Helper that allows filling a parent association for a given resource, based on the
|
5
|
+
# inverse_of option of the parent resource association.
|
6
|
+
#
|
7
|
+
def with_inverse_association_for(resource, attribute_name, parent_resource)
|
8
|
+
resource.tap do
|
9
|
+
association_name = parent_resource.association(attribute_name).options[:inverse_of]
|
10
|
+
return resource unless association_name
|
11
|
+
|
12
|
+
resource.association(association_name).replace(parent_resource)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -2,18 +2,25 @@ module Para
|
|
2
2
|
module Admin
|
3
3
|
module PageHelper
|
4
4
|
def page_top_bar(options = {})
|
5
|
-
content_tag(:div, class: 'page-title row') do
|
5
|
+
top_bar = content_tag(:div, class: 'page-title row') do
|
6
6
|
content_tag(:h1, options[:title]) +
|
7
7
|
|
8
8
|
if (actions = actions_for(options[:type]))
|
9
9
|
actions.map(&method(:build_action)).join('').html_safe
|
10
10
|
end
|
11
11
|
end
|
12
|
+
|
13
|
+
# Return both top bar and component navigation to be displayed at the top of the
|
14
|
+
# page.
|
15
|
+
top_bar + component_navigation
|
12
16
|
end
|
13
17
|
|
14
18
|
def build_action(action)
|
15
|
-
|
16
|
-
|
19
|
+
link_options = action.fetch(:link_options, {})
|
20
|
+
link_options[:class] ||= "btn btn-default btn-shadow"
|
21
|
+
|
22
|
+
content_tag(:div, class: 'actions-control pull-right') do
|
23
|
+
link_to(action[:url], link_options) do
|
17
24
|
(
|
18
25
|
(fa_icon(action[:icon], class: 'fa-fw') if action[:icon]) +
|
19
26
|
action[:label]
|
@@ -27,6 +34,26 @@ module Para
|
|
27
34
|
instance_eval(&action)
|
28
35
|
end.compact
|
29
36
|
end
|
37
|
+
|
38
|
+
def component_navigation
|
39
|
+
parent_component = (
|
40
|
+
@component.parent_component ||
|
41
|
+
@component.child_components.any? && @component
|
42
|
+
)
|
43
|
+
|
44
|
+
return unless parent_component
|
45
|
+
|
46
|
+
# If the component has a `model_type` option, therefore, an associated model,
|
47
|
+
# we try to render the partial from the relative path of the model, else we
|
48
|
+
# use the component class as the base target path
|
49
|
+
partial_target = parent_component.try(:model_type) || parent_component
|
50
|
+
|
51
|
+
render partial: find_partial_for(partial_target, :navigation),
|
52
|
+
locals: {
|
53
|
+
parent_component: parent_component,
|
54
|
+
active_component: @component
|
55
|
+
}
|
56
|
+
end
|
30
57
|
end
|
31
58
|
end
|
32
59
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Para
|
2
|
+
# Base class for all para-specific models.
|
3
|
+
#
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Adds the `optional: true` option to the belongs_to calls inside the provided block,
|
10
|
+
# but only for Rails 5.1+
|
11
|
+
#
|
12
|
+
def self.with_belongs_to_optional_option_if_needed(&block)
|
13
|
+
if ActiveRecord::Associations::Builder::BelongsTo.valid_options({}).include?(:optional)
|
14
|
+
with_options(optional: true, &block)
|
15
|
+
else
|
16
|
+
block.call
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Para
|
2
2
|
module Component
|
3
|
-
class Base <
|
3
|
+
class Base < Para::ApplicationRecord
|
4
4
|
self.table_name = 'para_components'
|
5
5
|
|
6
6
|
class_attribute :component_name
|
@@ -16,7 +16,14 @@ module Para
|
|
16
16
|
|
17
17
|
configurable_on :controller
|
18
18
|
|
19
|
-
|
19
|
+
with_belongs_to_optional_option_if_needed do
|
20
|
+
belongs_to :component_section, class_name: 'Para::ComponentSection'
|
21
|
+
belongs_to :parent_component, class_name: 'Para::Component::Base'
|
22
|
+
end
|
23
|
+
|
24
|
+
has_many :child_components, -> { ordered },
|
25
|
+
class_name: 'Para::Component::Base',
|
26
|
+
foreign_key: 'parent_component_id'
|
20
27
|
|
21
28
|
validates :identifier, :type, presence: true
|
22
29
|
|
@@ -31,6 +38,13 @@ module Para
|
|
31
38
|
)
|
32
39
|
end
|
33
40
|
|
41
|
+
def main_navigation_name
|
42
|
+
::I18n.t(
|
43
|
+
"components.main_navigation.#{ identifier }",
|
44
|
+
default: name
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
34
48
|
def exportable?
|
35
49
|
false
|
36
50
|
end
|
@@ -1,21 +1,14 @@
|
|
1
1
|
module Para
|
2
2
|
module Page
|
3
|
-
class Section <
|
3
|
+
class Section < Para::ApplicationRecord
|
4
4
|
self.table_name = 'para_page_sections'
|
5
5
|
|
6
6
|
acts_as_orderable parent: :page, as: :sections
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
# Make Rails 5+ belongs_to relation optional for the parent page, to allow
|
11
|
-
# using sections in other contexts that directly included into pages
|
12
|
-
#
|
13
|
-
if ActiveRecord::Associations::Builder::BelongsTo.valid_options({}).include?(:optional)
|
14
|
-
page_relation_options[:optional] = true
|
8
|
+
with_belongs_to_optional_option_if_needed do
|
9
|
+
belongs_to :page, polymorphic: true
|
15
10
|
end
|
16
11
|
|
17
|
-
belongs_to :page, page_relation_options
|
18
|
-
|
19
12
|
def css_class
|
20
13
|
@css_class ||= self.class.name.demodulize.underscore.gsub(/_/, '-')
|
21
14
|
end
|