para 0.8.5 → 0.8.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. data/app/assets/javascripts/para/admin.coffee +2 -1
  3. data/app/assets/javascripts/para/admin/table.coffee +3 -5
  4. data/app/assets/javascripts/para/admin/tree.coffee +68 -16
  5. data/app/assets/javascripts/para/inputs/multi-select-input.coffee +2 -3
  6. data/app/assets/javascripts/para/inputs/nested_many.coffee +10 -6
  7. data/app/assets/stylesheets/para/admin/src/_list.sass +7 -3
  8. data/app/helpers/para/admin/nested_inputs_helper.rb +17 -0
  9. data/app/helpers/para/admin/page_helper.rb +30 -3
  10. data/app/models/application_record.rb +3 -0
  11. data/app/models/para/application_record.rb +20 -0
  12. data/app/models/para/cache/item.rb +1 -1
  13. data/app/models/para/component/base.rb +16 -2
  14. data/app/models/para/component_resource.rb +1 -1
  15. data/app/models/para/component_section.rb +1 -1
  16. data/app/models/para/library/file.rb +1 -1
  17. data/app/models/para/page/section.rb +3 -10
  18. data/app/models/para/page/section_resource.rb +1 -1
  19. data/app/views/para/admin/resources/_navigation.html.haml +10 -0
  20. data/app/views/para/admin/resources/_tree.html.haml +4 -0
  21. data/app/views/para/admin/shared/_navigation.html.haml +1 -1
  22. data/app/views/para/inputs/_nested_many.html.haml +3 -3
  23. data/app/views/para/inputs/nested_many/_add.html.haml +1 -1
  24. data/app/views/para/inputs/nested_many/_add_with_subclasses.html.haml +1 -1
  25. data/app/views/para/inputs/nested_many/_container.html.haml +1 -1
  26. data/app/views/para/inputs/nested_one/_add_with_subclasses.html.haml +1 -1
  27. data/db/migrate/20201210152223_add_parent_component_to_para_components.rb +6 -0
  28. data/lib/generators/para/install/templates/initializer.rb +9 -1
  29. data/lib/para/components_configuration.rb +64 -13
  30. data/lib/para/config.rb +3 -0
  31. data/lib/para/inputs/nested_many_input.rb +7 -2
  32. data/lib/para/job/base.rb +2 -2
  33. data/lib/para/version.rb +1 -1
  34. data/vendor/assets/javascripts/Sortable.js +4144 -0
  35. data/vendor/assets/javascripts/jquery.sortable.js +76 -0
  36. metadata +9 -5
  37. data/vendor/assets/javascripts/html5-sortable.js +0 -142
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 46dd04d9d028f48edd61932e381aba77607696e2
4
- data.tar.gz: 7b3c9b9fa4a6d1440879c2cec9561c9ddb36fe8b
2
+ SHA256:
3
+ metadata.gz: 61c7a5614276ef1baeb61bc3374e309f17b3316e87bcf4694a3fdd5fb57ac3ed
4
+ data.tar.gz: 7af31c4c81ba3c3ac2b925ee719914e3d781e67c1033d225d668033d2a27243a
5
5
  SHA512:
6
- metadata.gz: 21fc22237a68f50fdac07a4fbb8916928735c2d4db73f70aa99debc31992312727ccb0d8cb07991869222795be844148674a50bddebb49af17af0418a7a010b7
7
- data.tar.gz: 12a8f6e72b4f33d1c2a0c0ebaefb58bb0a59ad698e60a3afb4575799550fb5dee72ea6308977dbb98d3c269864930ac5464ddc77b8bd62a4cc9f5ef2a17eeb10
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 html5-sortable
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
- forcePlaceholderSize: true
16
- items: 'tr'
17
- placeholder: "<tr><td colspan=\"100%\" class=\"sortable-placeholder\"></td></tr>"
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
- $(".tree")
10
- .sortable
11
- handle: ".handle"
12
- items: ".node"
13
- connectWith: ".tree"
14
- .on('sortupdate', $.proxy(@sortUpdate, this))
15
-
16
- sortUpdate: (e, data) ->
17
- @handlePlaceholder($el) for $el in [data.endparent, data.startparent, data.item.find('.tree')]
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
- if $el.parents('.tree').length - 1 >= @maxDepth or $el.find('> .node').length
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("> .tree").each (index, el) => @handlePlaceholder $(el)
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
- resources: @buildOrderedData()
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.parent().parent().data("id")
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
- forcePlaceholderSize: true
126
- placeholder: "<tr><td colspan='#{ columnsCount }'></td></tr>"
125
+ animation: 150
127
126
 
128
- @$selectedItems.on('sortupdate', @selectedItemsSorted)
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
- forcePlaceholderSize: true
16
+ animation: 150
17
+ onUpdate: $.proxy(@handleOrderingUpdated, this)
17
18
 
18
- @$fieldsList.on('sortupdate', $.proxy(@sortUpdate, this))
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
- @sortUpdate()
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
- content_tag(:div, class: 'actions-control pull-right') do
16
- link_to(action[:url], class: 'btn btn-default btn-shadow') do
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
@@ -1,3 +1,6 @@
1
+ # Base class used for the `AdminUser` model class as parent but automatically overriden
2
+ # by application's own ApplicationRecord definition in Rails 5+
3
+ #
1
4
  class ApplicationRecord < ActiveRecord::Base
2
5
  self.abstract_class = true
3
6
  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
@@ -3,7 +3,7 @@
3
3
  #
4
4
  module Para
5
5
  module Cache
6
- class Item < ActiveRecord::Base
6
+ class Item < Para::ApplicationRecord
7
7
  def value
8
8
  Marshal.load(::Base64.decode64(self[:value])) if self[:value].present?
9
9
  end
@@ -1,6 +1,6 @@
1
1
  module Para
2
2
  module Component
3
- class Base < ActiveRecord::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
- belongs_to :component_section, class_name: 'Para::ComponentSection'
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,5 +1,5 @@
1
1
  module Para
2
- class ComponentResource < ActiveRecord::Base
2
+ class ComponentResource < Para::ApplicationRecord
3
3
  belongs_to :component, class_name: 'Para::Component::Base'
4
4
  belongs_to :resource, polymorphic: true
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module Para
2
- class ComponentSection < ActiveRecord::Base
2
+ class ComponentSection < Para::ApplicationRecord
3
3
  has_many :components, -> { ordered }, class_name: 'Para::Component::Base',
4
4
  autosave: true, foreign_key: :component_section_id,
5
5
  dependent: :destroy
@@ -1,6 +1,6 @@
1
1
  module Para
2
2
  module Library
3
- class File < ActiveRecord::Base
3
+ class File < Para::ApplicationRecord
4
4
  if defined?(ActiveStorage)
5
5
  has_one_attached :attachment
6
6
 
@@ -1,21 +1,14 @@
1
1
  module Para
2
2
  module Page
3
- class Section < ActiveRecord::Base
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
- page_relation_options = { polymorphic: true }
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
@@ -1,6 +1,6 @@
1
1
  module Para
2
2
  module Page
3
- class SectionResource < ActiveRecord::Base
3
+ class SectionResource < Para::ApplicationRecord
4
4
  self.table_name = 'para_page_section_resources'
5
5
 
6
6
  acts_as_orderable parent: :section, as: :section_resources