trestle 0.10.0 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +4 -0
  3. data/app/assets/bundle/trestle/admin.css +3 -3
  4. data/app/assets/bundle/trestle/admin.js +13 -13
  5. data/app/assets/bundle/trestle/photoswipe-2d522a3abaa59f8a8f73.digested.js +6 -0
  6. data/app/helpers/trestle/avatar_helper.rb +20 -14
  7. data/app/helpers/trestle/card_helper.rb +27 -9
  8. data/app/helpers/trestle/container_helper.rb +37 -6
  9. data/app/helpers/trestle/display_helper.rb +11 -0
  10. data/app/helpers/trestle/flash_helper.rb +1 -10
  11. data/app/helpers/trestle/form_helper.rb +32 -18
  12. data/app/helpers/trestle/format_helper.rb +47 -17
  13. data/app/helpers/trestle/gravatar_helper.rb +48 -0
  14. data/app/helpers/trestle/grid_helper.rb +12 -14
  15. data/app/helpers/trestle/headings_helper.rb +2 -23
  16. data/app/helpers/trestle/i18n_helper.rb +1 -0
  17. data/app/helpers/trestle/icon_helper.rb +16 -3
  18. data/app/helpers/trestle/layout_helper.rb +1 -0
  19. data/app/helpers/trestle/modal_helper.rb +21 -2
  20. data/app/helpers/trestle/navigation_helper.rb +1 -0
  21. data/app/helpers/trestle/pagination_helper.rb +1 -0
  22. data/app/helpers/trestle/params_helper.rb +32 -0
  23. data/app/helpers/trestle/sort_helper.rb +38 -7
  24. data/app/helpers/trestle/status_helper.rb +19 -3
  25. data/app/helpers/trestle/tab_helper.rb +42 -7
  26. data/app/helpers/trestle/table_helper.rb +23 -23
  27. data/app/helpers/trestle/timestamp_helper.rb +18 -25
  28. data/app/helpers/trestle/title_helper.rb +2 -0
  29. data/app/helpers/trestle/toolbars_helper.rb +2 -1
  30. data/app/helpers/trestle/turbo/frame_helper.rb +25 -14
  31. data/app/helpers/trestle/url_helper.rb +124 -54
  32. data/app/views/kaminari/trestle/_first_page.html.erb +1 -2
  33. data/app/views/kaminari/trestle/_gap.html.erb +0 -1
  34. data/app/views/kaminari/trestle/_last_page.html.erb +1 -2
  35. data/app/views/kaminari/trestle/_page.html.erb +1 -2
  36. data/app/views/kaminari/trestle/_paginator.html.erb +0 -1
  37. data/app/views/layouts/trestle/admin.html.erb +3 -3
  38. data/app/views/layouts/trestle/modal.html.erb +2 -2
  39. data/app/views/trestle/application/_layout.html.erb +22 -18
  40. data/app/views/trestle/application/_tabs.html.erb +1 -1
  41. data/app/views/trestle/flash/_alert.html.erb +3 -1
  42. data/app/views/trestle/flash/_flash.html.erb +7 -4
  43. data/app/views/trestle/resource/_scopes.html.erb +3 -3
  44. data/app/views/trestle/resource/create.turbo_stream.erb +1 -0
  45. data/app/views/trestle/resource/destroy.turbo_stream.erb +2 -0
  46. data/app/views/trestle/resource/index.html.erb +10 -12
  47. data/app/views/trestle/resource/update.turbo_stream.erb +1 -0
  48. data/app/views/trestle/shared/_sidebar.html.erb +8 -8
  49. data/app/views/trestle/table/_table.html.erb +2 -2
  50. data/config/locales/pt-BR.yml +13 -13
  51. data/frontend/css/components/_scopes.scss +6 -7
  52. data/frontend/css/core/_theme.scss +3 -3
  53. data/frontend/css/layout/_sidebar.scss +2 -0
  54. data/frontend/js/controllers/flatpickr_controller.js +2 -2
  55. data/frontend/js/controllers/lightbox_controller.js +3 -3
  56. data/frontend/js/controllers/modal_trigger_controller.js +2 -2
  57. data/frontend/js/controllers/sidebar_controller.js +13 -3
  58. data/frontend/js/controllers/tab_errors_controller.js +2 -2
  59. data/frontend/js/core/backdrop.js +30 -28
  60. data/frontend/js/core/error_modal.js +7 -9
  61. data/frontend/js/core/fetch.js +2 -0
  62. data/lib/trestle/admin.rb +9 -2
  63. data/lib/trestle/configuration.rb +3 -0
  64. data/lib/trestle/engine.rb +1 -1
  65. data/lib/trestle/evaluation_context.rb +2 -4
  66. data/lib/trestle/form/automatic.rb +1 -1
  67. data/lib/trestle/form/field.rb +1 -1
  68. data/lib/trestle/form/fields/check_box.rb +1 -1
  69. data/lib/trestle/form/fields/collection_check_boxes.rb +1 -1
  70. data/lib/trestle/form/fields/collection_radio_buttons.rb +1 -1
  71. data/lib/trestle/form/fields/date_select.rb +1 -1
  72. data/lib/trestle/form/fields/datetime_select.rb +1 -1
  73. data/lib/trestle/form/fields/form_control.rb +2 -2
  74. data/lib/trestle/form/fields/form_group.rb +4 -4
  75. data/lib/trestle/form/fields/radio_button.rb +1 -1
  76. data/lib/trestle/form/fields/static_field.rb +1 -1
  77. data/lib/trestle/form/fields/time_select.rb +1 -1
  78. data/lib/trestle/form/renderer.rb +2 -4
  79. data/lib/trestle/hook/helpers.rb +21 -0
  80. data/lib/trestle/navigation/block.rb +8 -15
  81. data/lib/trestle/navigation/group.rb +2 -2
  82. data/lib/trestle/navigation/item.rb +21 -4
  83. data/lib/trestle/registry.rb +14 -11
  84. data/lib/trestle/resource/builder.rb +9 -6
  85. data/lib/trestle/resource/toolbar.rb +4 -4
  86. data/lib/trestle/resource.rb +7 -5
  87. data/lib/trestle/scopes/block.rb +8 -12
  88. data/lib/trestle/scopes/definition.rb +6 -2
  89. data/lib/trestle/scopes/scope.rb +13 -10
  90. data/lib/trestle/tab.rb +2 -2
  91. data/lib/trestle/table/column.rb +4 -3
  92. data/lib/trestle/table/row.rb +1 -1
  93. data/lib/trestle/toolbar/builder.rb +6 -6
  94. data/lib/trestle/toolbar/context.rb +2 -4
  95. data/lib/trestle/toolbar/item.rb +10 -19
  96. data/lib/trestle/toolbar/menu.rb +9 -9
  97. data/lib/trestle/version.rb +1 -1
  98. data/lib/trestle.rb +2 -2
  99. data/package.json +7 -7
  100. data/yarn.lock +517 -564
  101. metadata +5 -5
  102. data/app/assets/bundle/trestle/photoswipe-063ce7be40e10b3e6848.digested.js +0 -6
  103. data/app/views/layouts/trestle/admin.turbo_stream.erb +0 -4
@@ -109,7 +109,7 @@ export default class extends ApplicationController {
109
109
  itemData.type = 'html'
110
110
  itemData.html = `<video controls><source src="${itemData.src}"></video>`
111
111
 
112
- this.setDefaultVideoDimensions(itemData)
112
+ this.#setDefaultVideoDimensions(itemData)
113
113
  }
114
114
 
115
115
  return itemData
@@ -126,7 +126,7 @@ export default class extends ApplicationController {
126
126
  itemData.type = 'html'
127
127
  itemData.html = `<iframe src="${src}" allowfullscreen></iframe>`
128
128
 
129
- this.setDefaultVideoDimensions(itemData)
129
+ this.#setDefaultVideoDimensions(itemData)
130
130
  }
131
131
  }
132
132
 
@@ -151,7 +151,7 @@ export default class extends ApplicationController {
151
151
  }
152
152
  }
153
153
 
154
- setDefaultVideoDimensions(itemData) {
154
+ #setDefaultVideoDimensions(itemData) {
155
155
  itemData.w ||= this.defaultVideoWidthValue
156
156
  itemData.h ||= this.defaultVideoHeightValue
157
157
  }
@@ -26,7 +26,7 @@ export default class extends ApplicationController {
26
26
  .then((modal) => {
27
27
  this.modal = modal
28
28
 
29
- const modalController = this._getModalController(modal)
29
+ const modalController = this.#getModalController(modal)
30
30
  modalController.modalTrigger = this
31
31
 
32
32
  this.dispatch('loaded', { detail: modal })
@@ -68,7 +68,7 @@ export default class extends ApplicationController {
68
68
  return this.element.nodeName === 'A'
69
69
  }
70
70
 
71
- _getModalController (modal) {
71
+ #getModalController (modal) {
72
72
  return this.application.getControllerForElementAndIdentifier(modal, 'modal')
73
73
  }
74
74
  }
@@ -5,6 +5,10 @@ import cookie from '../core/cookie'
5
5
  export default class extends ApplicationController {
6
6
  static targets = ["inner"]
7
7
 
8
+ static values = {
9
+ scrollMargin: { type: Number, default: 100 }
10
+ }
11
+
8
12
  connect () {
9
13
  this.scrollToActive()
10
14
  }
@@ -25,9 +29,15 @@ export default class extends ApplicationController {
25
29
  }
26
30
 
27
31
  scrollToActive () {
28
- const active = this.element.getElementsByClassName('active')[0]
29
- if (active && this.hasInnerTarget) {
30
- this.innerTarget.scrollTop = active.offsetTop - 100
32
+ if (!this.hasInnerTarget) return
33
+
34
+ const active = this.element.querySelector('.active')
35
+ if (!active) return
36
+
37
+ // Check if bottom of active element is outside of visible navigation height (plus scroll margin)
38
+ const activeOffset = active.offsetTop + active.offsetHeight + this.scrollMarginValue
39
+ if (activeOffset > this.innerTarget.clientHeight) {
40
+ this.innerTarget.scrollTop = activeOffset - this.innerTarget.clientHeight
31
41
  }
32
42
  }
33
43
  }
@@ -20,7 +20,7 @@ export default class extends ApplicationController {
20
20
  const errorCount = pane.querySelectorAll(this.errorSelectorValue).length
21
21
 
22
22
  if (errorCount > 0) {
23
- const badge = this._createErrorBadge(errorCount)
23
+ const badge = this.#createErrorBadge(errorCount)
24
24
  link.appendChild(badge)
25
25
  }
26
26
  }
@@ -34,7 +34,7 @@ export default class extends ApplicationController {
34
34
  })
35
35
  }
36
36
 
37
- _createErrorBadge (count) {
37
+ #createErrorBadge (count) {
38
38
  const badge = document.createElement('span')
39
39
 
40
40
  badge.classList.add('badge', 'badge-danger', 'badge-pill')
@@ -12,6 +12,10 @@ const CLASS_NAME_SHOW = 'show'
12
12
  const CLASS_NAME_LOADING = 'loading'
13
13
 
14
14
  export default class Backdrop {
15
+ #config
16
+ #element
17
+ #isAppended
18
+
15
19
  static getInstance () {
16
20
  if (!this.instance) {
17
21
  this.instance = new Backdrop()
@@ -21,31 +25,31 @@ export default class Backdrop {
21
25
  }
22
26
 
23
27
  constructor () {
24
- this._config = Default
25
- this._element = null
26
- this._isAppended = false
28
+ this.#config = Default
29
+ this.#element = null
30
+ this.#isAppended = false
27
31
  }
28
32
 
29
33
  show (callback) {
30
- this._append()
34
+ this.#append()
31
35
 
32
- if (this._config.isAnimated) {
33
- reflow(this._getElement())
36
+ if (this.#config.isAnimated) {
37
+ reflow(this.#getElement())
34
38
  }
35
39
 
36
- this._getElement().classList.add(CLASS_NAME_SHOW)
40
+ this.#getElement().classList.add(CLASS_NAME_SHOW)
37
41
 
38
- this._emulateAnimation(() => {
42
+ this.#emulateAnimation(() => {
39
43
  execute(callback)
40
44
  })
41
45
  }
42
46
 
43
47
  hide (callback) {
44
48
  if (Modal.existing.length === 0) {
45
- this._getElement().classList.remove(CLASS_NAME_SHOW)
49
+ this.#getElement().classList.remove(CLASS_NAME_SHOW)
46
50
  }
47
51
 
48
- this._emulateAnimation(() => {
52
+ this.#emulateAnimation(() => {
49
53
  this.dispose()
50
54
  execute(callback)
51
55
  Modal.restorePrevious()
@@ -53,48 +57,46 @@ export default class Backdrop {
53
57
  }
54
58
 
55
59
  dispose () {
56
- if (!this._isAppended) {
60
+ if (!this.#isAppended) {
57
61
  return
58
62
  }
59
63
 
60
64
  if (Modal.existing.length === 0) {
61
- this._element.remove()
62
- this._isAppended = false
65
+ this.#element.remove()
66
+ this.#isAppended = false
63
67
  }
64
68
  }
65
69
 
66
70
  loading (isLoading) {
67
- const el = this._getElement()
71
+ const el = this.#getElement()
68
72
  el.classList[isLoading ? 'add' : 'remove'](CLASS_NAME_LOADING)
69
73
  }
70
74
 
71
- // Private
72
-
73
- _getElement () {
74
- if (!this._element) {
75
+ #getElement () {
76
+ if (!this.#element) {
75
77
  const backdrop = document.createElement('div')
76
- backdrop.className = this._config.className
77
- if (this._config.isAnimated) {
78
+ backdrop.className = this.#config.className
79
+ if (this.#config.isAnimated) {
78
80
  backdrop.classList.add(CLASS_NAME_FADE)
79
81
  }
80
82
 
81
- this._element = backdrop
83
+ this.#element = backdrop
82
84
  }
83
85
 
84
- return this._element
86
+ return this.#element
85
87
  }
86
88
 
87
- _append () {
88
- if (this._isAppended) {
89
+ #append () {
90
+ if (this.#isAppended) {
89
91
  return
90
92
  }
91
93
 
92
- document.body.append(this._getElement())
94
+ document.body.append(this.#getElement())
93
95
 
94
- this._isAppended = true
96
+ this.#isAppended = true
95
97
  }
96
98
 
97
- _emulateAnimation (callback) {
98
- executeAfterTransition(callback, this._getElement(), this._config.isAnimated)
99
+ #emulateAnimation (callback) {
100
+ executeAfterTransition(callback, this.#getElement(), this.#config.isAnimated)
99
101
  }
100
102
  }
@@ -33,33 +33,31 @@ export default class ErrorModal {
33
33
  }
34
34
 
35
35
  show () {
36
- this._append(this._buildModal())
36
+ this.#append(this.#buildModal())
37
37
  }
38
38
 
39
- // Private
40
-
41
- _buildModal () {
42
- const el = this._buildWrapper()
39
+ #buildModal () {
40
+ const el = this.#buildWrapper()
43
41
  el.querySelector('.modal-title').textContent = this.title
44
42
 
45
- const iframe = this._buildIframe(this.content)
43
+ const iframe = this.#buildIframe(this.content)
46
44
  el.querySelector('.modal-body').append(iframe)
47
45
 
48
46
  return el
49
47
  }
50
48
 
51
- _buildWrapper () {
49
+ #buildWrapper () {
52
50
  return new DOMParser().parseFromString(TEMPLATE(), 'text/html').body.childNodes[0]
53
51
  }
54
52
 
55
- _buildIframe () {
53
+ #buildIframe () {
56
54
  const iframe = document.createElement('iframe')
57
55
  iframe.className = 'error-iframe'
58
56
  iframe.srcdoc = this.content
59
57
  return iframe
60
58
  }
61
59
 
62
- _append (el) {
60
+ #append (el) {
63
61
  document.getElementById('modal').append(el)
64
62
  }
65
63
  }
@@ -13,6 +13,7 @@ export function fetchWithErrorHandling (url, options = {}) {
13
13
  .catch(response => {
14
14
  const title = `${response.status} (${response.statusText})`
15
15
  response.text().then(content => ErrorModal.show({ title, content }))
16
+ throw response
16
17
  })
17
18
  }
18
19
 
@@ -29,4 +30,5 @@ export function fetchTurboStream (url, options = {}) {
29
30
  return fetchWithErrorHandling(url, options)
30
31
  .then(response => response.text())
31
32
  .then(html => renderStreamMessage(html))
33
+ .catch(() => { /* Error already handled */ })
32
34
  }
data/lib/trestle/admin.rb CHANGED
@@ -14,6 +14,7 @@ module Trestle
14
14
  super
15
15
  end
16
16
  end
17
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
17
18
 
18
19
  def respond_to_missing?(name, include_private=false)
19
20
  self.class.respond_to?(name, include_private) || super
@@ -118,8 +119,14 @@ module Trestle
118
119
  "#{name.underscore}/admin"
119
120
  end
120
121
 
121
- def path(action=root_action, options={})
122
- Engine.routes.url_for(options.merge(controller: controller_namespace, action: action, only_path: true))
122
+ def path(action=nil, options={})
123
+ defaults = {
124
+ controller: controller_namespace,
125
+ action: action || root_action,
126
+ only_path: true
127
+ }
128
+
129
+ Engine.routes.url_for(defaults.merge(options))
123
130
  end
124
131
 
125
132
  def to_param(*)
@@ -91,6 +91,9 @@ module Trestle
91
91
  # Default adapter class used by all admin resources
92
92
  option :default_adapter, Adapters.compose(Adapters::ActiveRecordAdapter, Adapters::DraperAdapter)
93
93
 
94
+ # List of Stimulus controllers to add to forms by default
95
+ option :default_form_controllers, %w(keyboard-submit form-loading form-error)
96
+
94
97
  # Register a custom form field class
95
98
  def form_field(name, field)
96
99
  Form::Builder.register(name, field)
@@ -17,7 +17,7 @@ module Trestle
17
17
  initializer "trestle.automount" do |app|
18
18
  if Trestle.config.automount
19
19
  app.routes.prepend do
20
- mount Trestle::Engine => Trestle.config.path
20
+ mount Trestle::Engine, at: Trestle.config.path
21
21
  end
22
22
  end
23
23
  end
@@ -5,20 +5,18 @@ module Trestle
5
5
  # both the Adapter/Navigation instance, as well as the controller/view from where they are invoked.
6
6
  module EvaluationContext
7
7
  protected
8
- def self.ruby2_keywords(*)
9
- end unless respond_to?(:ruby2_keywords, true)
10
-
11
8
  # Missing methods are called on the given context if available.
12
9
  #
13
10
  # We include private methods as methods such as current_user
14
11
  # are usually declared as private or protected.
15
- ruby2_keywords def method_missing(name, *args, &block)
12
+ def method_missing(name, *args, &block)
16
13
  if context_responds_to?(name)
17
14
  @context.send(name, *args, &block)
18
15
  else
19
16
  super
20
17
  end
21
18
  end
19
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
22
20
 
23
21
  def respond_to_missing?(name, include_private=false)
24
22
  context_responds_to?(name) || super
@@ -16,7 +16,7 @@ module Trestle
16
16
  if associated_instance = instance.public_send(attribute.association_name)
17
17
  admin_link_to format_value(associated_instance), associated_instance
18
18
  else
19
- content_tag(:span, I18n.t("admin.format.blank"), class: "blank")
19
+ tag.span(I18n.t("admin.format.blank"), class: "blank")
20
20
  end
21
21
  end
22
22
  else
@@ -3,7 +3,7 @@ module Trestle
3
3
  class Field
4
4
  attr_reader :builder, :template, :name, :options, :block
5
5
 
6
- delegate :admin, :content_tag, :concat, :safe_join, :icon, to: :template
6
+ delegate :admin, :tag, :content_tag, :concat, :safe_join, :icon, to: :template
7
7
 
8
8
  def initialize(builder, template, name, options={}, &block)
9
9
  @builder, @template, @name, @block = builder, template, name, block
@@ -19,7 +19,7 @@ module Trestle
19
19
  wrapper_class = options.delete(:class)
20
20
  wrapper_class = default_wrapper_class if wrapper_class.empty?
21
21
 
22
- content_tag(:div, class: wrapper_class) do
22
+ tag.div(class: wrapper_class) do
23
23
  safe_join([
24
24
  builder.raw_check_box(name, options.merge(class: input_class), checked_value, unchecked_value),
25
25
  builder.label(name, options[:label] || admin.human_attribute_name(name), class: label_class, value: (checked_value if options[:multiple]))
@@ -18,7 +18,7 @@ module Trestle
18
18
  if block
19
19
  block.call(b)
20
20
  else
21
- content_tag(:div, class: default_wrapper_class) do
21
+ tag.div(class: default_wrapper_class) do
22
22
  b.check_box(class: input_class) + b.label(class: label_class) { b.text }
23
23
  end
24
24
  end
@@ -18,7 +18,7 @@ module Trestle
18
18
  if block
19
19
  block.call(b)
20
20
  else
21
- content_tag(:div, class: default_wrapper_class) do
21
+ tag.div(class: default_wrapper_class) do
22
22
  b.radio_button(class: input_class) + b.label(class: label_class) { b.text }
23
23
  end
24
24
  end
@@ -11,7 +11,7 @@ module Trestle
11
11
  end
12
12
 
13
13
  def field
14
- content_tag(:div, class: "date-select") do
14
+ tag.div(class: "date-select") do
15
15
  builder.raw_date_select(name, options, html_options, &block)
16
16
  end
17
17
  end
@@ -11,7 +11,7 @@ module Trestle
11
11
  end
12
12
 
13
13
  def field
14
- content_tag(:div, class: "datetime-select") do
14
+ tag.div(class: "datetime-select") do
15
15
  builder.raw_datetime_select(name, options, html_options, &block)
16
16
  end
17
17
  end
@@ -12,7 +12,7 @@ module Trestle
12
12
 
13
13
  def input_group
14
14
  if @prepend || @append
15
- content_tag(:div, class: "input-group") do
15
+ tag.div(class: "input-group") do
16
16
  concat input_group_addon(@prepend) if @prepend
17
17
  concat yield
18
18
  concat input_group_addon(@append) if @append
@@ -24,7 +24,7 @@ module Trestle
24
24
 
25
25
  def input_group_addon(addon)
26
26
  if addon[:wrap]
27
- content_tag(:span, addon[:content], class: "input-group-text")
27
+ tag.span(addon[:content], class: "input-group-text")
28
28
  else
29
29
  addon[:content]
30
30
  end
@@ -12,7 +12,7 @@ module Trestle
12
12
  end
13
13
 
14
14
  def render
15
- content_tag(:div, options.except(*WRAPPER_OPTIONS)) do
15
+ tag.div(**options.except(*WRAPPER_OPTIONS)) do
16
16
  concat label if name && options[:label] != false
17
17
  concat template.capture(&block) if block
18
18
  concat help_message if options[:help]
@@ -30,13 +30,13 @@ module Trestle
30
30
  message = options[:help]
31
31
  end
32
32
 
33
- content_tag(:p, message, class: classes)
33
+ tag.p(message, class: classes)
34
34
  end
35
35
 
36
36
  def error_messages
37
- content_tag(:ul, class: "invalid-feedback") do
37
+ tag.ul(class: "invalid-feedback") do
38
38
  safe_join(errors.map { |error|
39
- content_tag(:li, safe_join([icon("fa fa-warning"), error], " "))
39
+ tag.li(safe_join([icon("fa fa-warning"), error], " "))
40
40
  }, "\n")
41
41
  end
42
42
  end
@@ -20,7 +20,7 @@ module Trestle
20
20
  wrapper_class = options.delete(:class)
21
21
  wrapper_class = default_wrapper_class if wrapper_class.empty?
22
22
 
23
- content_tag(:div, class: wrapper_class) do
23
+ tag.div(class: wrapper_class) do
24
24
  safe_join([
25
25
  builder.raw_radio_button(name, tag_value, options.merge(class: input_class)),
26
26
  builder.label(name, options[:label] || tag_value.to_s.humanize, value: tag_value, class: label_class)
@@ -18,7 +18,7 @@ module Trestle
18
18
  if block
19
19
  template.capture(&block)
20
20
  else
21
- content_tag(:p, value || default_value, class: "form-control-static")
21
+ tag.p(value || default_value, class: "form-control-static")
22
22
  end
23
23
  end
24
24
 
@@ -11,7 +11,7 @@ module Trestle
11
11
  end
12
12
 
13
13
  def field
14
- content_tag(:div, class: "time-select") do
14
+ tag.div(class: "time-select") do
15
15
  builder.raw_time_select(name, options, html_options, &block)
16
16
  end
17
17
  end
@@ -4,9 +4,6 @@ require "action_view/helpers"
4
4
  module Trestle
5
5
  class Form
6
6
  class Renderer
7
- def self.ruby2_keywords(*)
8
- end unless respond_to?(:ruby2_keywords, true)
9
-
10
7
  include ::ActionView::Context
11
8
  include ::ActionView::Helpers::CaptureHelper
12
9
 
@@ -46,7 +43,7 @@ module Trestle
46
43
  concat(result)
47
44
  end
48
45
 
49
- ruby2_keywords def method_missing(name, *args, &block)
46
+ def method_missing(name, *args, &block)
50
47
  target = @form.respond_to?(name) ? @form : @template
51
48
 
52
49
  if block_given? && !RAW_BLOCK_HELPERS.include?(name)
@@ -63,6 +60,7 @@ module Trestle
63
60
  result
64
61
  end
65
62
  end
63
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
66
64
 
67
65
  def respond_to_missing?(name, include_all=false)
68
66
  @form.respond_to?(name, include_all) ||
@@ -1,6 +1,25 @@
1
1
  module Trestle
2
2
  class Hook
3
3
  module Helpers
4
+ # Evaluates any defined hooks with the given name and returns the result.
5
+ #
6
+ # Each hook is evaluated and passed any provided arguments, and the result
7
+ # is concatenated together. If no hooks are defined, and a block is passed
8
+ # to this helper, then the block will be evaluated instead.
9
+ #
10
+ # name - Name of hook to evaluate
11
+ # args - Arguments to pass to hook blocks
12
+ # block - Optional block to evaluate as a fallback if no hooks are defined
13
+ #
14
+ # Examples
15
+ #
16
+ # <%= hook("index.toolbar.primary", toolbar) %>
17
+ #
18
+ # <%= hook("view.title") do %>
19
+ # Default Title
20
+ # <% end %>
21
+ #
22
+ # Returns a HTML-safe string.
4
23
  def hook(name, *args, &block)
5
24
  hooks = hooks(name)
6
25
 
@@ -13,6 +32,8 @@ module Trestle
13
32
  end
14
33
  end
15
34
 
35
+ # Returns true or false depending on whether there are any defined hooks
36
+ # (either on the current admin or globally) with the given name.
16
37
  def hook?(name)
17
38
  hooks(name).any?
18
39
  end
@@ -17,6 +17,8 @@ module Trestle
17
17
  class Evaluator
18
18
  include EvaluationContext
19
19
 
20
+ delegate :path, to: :@admin
21
+
20
22
  attr_reader :items
21
23
 
22
24
  def initialize(admin=nil, context=nil)
@@ -24,30 +26,21 @@ module Trestle
24
26
  @items = []
25
27
  end
26
28
 
27
- def default_path
28
- @admin ? @admin.path : nil
29
- end
30
-
31
- def item(name, path=nil, options={})
32
- if path.is_a?(Hash)
33
- options = path
34
- path = nil
35
- end
36
-
29
+ def item(name, path=nil, **options)
37
30
  if options[:group]
38
31
  group = Group.new(options[:group])
39
32
  elsif @current_group
40
33
  group = @current_group
41
34
  end
42
35
 
43
- options = options.merge(group: group) if group
44
- options = options.merge(admin: @admin) if @admin
36
+ options.merge!(group: group) if group
37
+ options.merge!(admin: @admin) if @admin
45
38
 
46
- items << Item.new(name, path || default_path, options)
39
+ items << Item.new(name, path, **options)
47
40
  end
48
41
 
49
- def group(name, options={})
50
- @current_group = Group.new(name, options)
42
+ def group(name, **options)
43
+ @current_group = Group.new(name, **options)
51
44
  yield
52
45
  ensure
53
46
  @current_group = nil
@@ -3,7 +3,7 @@ module Trestle
3
3
  class Group
4
4
  attr_reader :name, :options
5
5
 
6
- def initialize(name, options={})
6
+ def initialize(name, **options)
7
7
  @name, @options = name.to_s, options
8
8
  end
9
9
 
@@ -26,7 +26,7 @@ module Trestle
26
26
  end
27
27
 
28
28
  def merge(other)
29
- self.class.new(name, options.merge(other.options))
29
+ self.class.new(name, **options.merge(other.options))
30
30
  end
31
31
 
32
32
  def priority
@@ -1,9 +1,9 @@
1
1
  module Trestle
2
2
  class Navigation
3
3
  class Item
4
- attr_reader :name, :path, :options
4
+ attr_reader :name, :options
5
5
 
6
- def initialize(name, path=nil, options={})
6
+ def initialize(name, path=nil, **options)
7
7
  @name, @path, @options = name.to_s, path, options
8
8
  end
9
9
 
@@ -35,8 +35,25 @@ module Trestle
35
35
  options[:group] || NullGroup.new
36
36
  end
37
37
 
38
+ def path
39
+ if @path
40
+ @path
41
+ elsif admin = self.admin
42
+ admin.path(options[:action])
43
+ else
44
+ "#"
45
+ end
46
+ end
47
+
38
48
  def admin
39
- options[:admin]
49
+ case options[:admin]
50
+ when nil, false
51
+ return
52
+ when Symbol, String
53
+ Trestle.lookup(options[:admin]) or raise ActionController::UrlGenerationError, "No admin found named #{options[:admin].inspect}"
54
+ else
55
+ options[:admin]
56
+ end
40
57
  end
41
58
 
42
59
  def label
@@ -56,7 +73,7 @@ module Trestle
56
73
  end
57
74
 
58
75
  def html_options
59
- options.except(:admin, :badge, :group, :icon, :if, :label, :priority, :unless)
76
+ options.except(:action, :admin, :badge, :group, :icon, :if, :label, :priority, :unless)
60
77
  end
61
78
 
62
79
  def visible?(context)