trestle 0.10.0 → 0.10.1

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.
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)