avo 3.29.1 → 3.30.0

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -8
  3. data/app/components/avo/filters_component.html.erb +1 -1
  4. data/app/components/avo/items/panel_component.html.erb +1 -1
  5. data/app/components/avo/items/switcher_component.html.erb +1 -1
  6. data/app/components/avo/items/switcher_component.rb +1 -1
  7. data/app/components/avo/items/visible_items_component.html.erb +1 -0
  8. data/app/components/avo/items/visible_items_component.rb +1 -0
  9. data/app/controllers/avo/base_controller.rb +1 -2
  10. data/app/javascript/js/controllers/fields/date_field_controller.js +12 -0
  11. data/app/javascript/js/controllers/fields/easy_mde_controller.js +9 -2
  12. data/app/javascript/js/controllers/fields/key_value_controller.js +10 -1
  13. data/app/javascript/js/controllers/fields/tags_field_controller.js +7 -0
  14. data/app/javascript/js/controllers/media_library_attach_controller.js +7 -0
  15. data/app/javascript/js/controllers/preview_controller.js +8 -1
  16. data/app/javascript/js/controllers/record_selector_controller.js +11 -7
  17. data/app/javascript/js/controllers/search_controller.js +4 -0
  18. data/app/javascript/js/controllers/table_row_controller.js +9 -6
  19. data/app/javascript/js/controllers/tippy_controller.js +8 -1
  20. data/app/javascript/js/controllers/toggle_controller.js +1 -1
  21. data/lib/avo/fields/base_field.rb +3 -3
  22. data/lib/avo/fields/belongs_to_field.rb +15 -0
  23. data/lib/avo/fields/frame_base_field.rb +1 -1
  24. data/lib/avo/test_helpers.rb +6 -0
  25. data/lib/avo/version.rb +1 -1
  26. data/public/avo-assets/avo.base.css +1 -1
  27. data/public/avo-assets/avo.base.js +95 -95
  28. data/public/avo-assets/avo.base.js.map +3 -3
  29. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f86cf659cddf676d34648a053c832654ce670b40a7bb67c191baf337d7d10bba
4
- data.tar.gz: b2735bb819605098ccf7140d9f33f2aed09b534d42e84ec568dab42266a0500f
3
+ metadata.gz: 381ab579d675357bd7e3ec288df2f56d8c80db211b55e3ca9bffeda3a74c82ce
4
+ data.tar.gz: d485e192ea98b5e5f84a5bc6f906c175dbe5eb755cd4aa7b94a275f24d9c5937
5
5
  SHA512:
6
- metadata.gz: c32bd3bdc9a375977f08e0c1bcc59a8ceb0022d9f9e6f898a038246405714cbb961d6b24dcc1f40d33f244810f8c9cb27e20eb20f0f519f4ccad2e5145f32cec
7
- data.tar.gz: f7e57d7fcf36745e12ed4d957ffbaaa41bd51f79e97d70066f287e0faa757c4ebc5f6b71c035247babff0bb4301a55700a68116d8e9a9c85a946a2e48ef03e49
6
+ metadata.gz: 8b76ac1bb168ec995a0e88e9cee6b87fa624cce346c0cd07339d895c777181862ce890b375ba679e67436947a055d918f345656e48bf6fd766433c586d1f55e2
7
+ data.tar.gz: 576a88f74323ac640e183136d9865ff4f5e7ce675373e697f2ceb32036cd43e34c36bf6f39f5bd4911ea7477047620edd52bfd0235a0e188d849d0f4f783a3a9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (3.29.1)
4
+ avo (3.30.0)
5
5
  actionview (>= 6.1)
6
6
  active_link_to
7
7
  activerecord (>= 6.1)
@@ -284,7 +284,7 @@ GEM
284
284
  railties (>= 6.1.0)
285
285
  faker (3.5.3)
286
286
  i18n (>= 1.8.11, < 2)
287
- faraday (2.14.0)
287
+ faraday (2.14.1)
288
288
  faraday-net_http (>= 2.0, < 3.5)
289
289
  json
290
290
  logger
@@ -369,7 +369,7 @@ GEM
369
369
  jmespath (1.6.2)
370
370
  jsbundling-rails (1.3.1)
371
371
  railties (>= 6.0.0)
372
- json (2.17.1)
372
+ json (2.18.1)
373
373
  kramdown (2.5.1)
374
374
  rexml (>= 3.3.9)
375
375
  kramdown-parser-gfm (1.1.0)
@@ -416,7 +416,7 @@ GEM
416
416
  railties (>= 3.0)
417
417
  msgpack (1.8.0)
418
418
  multipart-post (2.4.1)
419
- net-http (0.8.0)
419
+ net-http (0.9.1)
420
420
  uri (>= 0.11.1)
421
421
  net-imap (0.5.12)
422
422
  date
@@ -428,11 +428,11 @@ GEM
428
428
  net-smtp (0.5.1)
429
429
  net-protocol
430
430
  nio4r (2.7.5)
431
- nokogiri (1.18.10-aarch64-linux-gnu)
431
+ nokogiri (1.19.1-aarch64-linux-gnu)
432
432
  racc (~> 1.4)
433
- nokogiri (1.18.10-arm64-darwin)
433
+ nokogiri (1.19.1-arm64-darwin)
434
434
  racc (~> 1.4)
435
- nokogiri (1.18.10-x86_64-linux-gnu)
435
+ nokogiri (1.19.1-x86_64-linux-gnu)
436
436
  racc (~> 1.4)
437
437
  observer (0.1.2)
438
438
  orm_adapter (0.5.0)
@@ -463,7 +463,7 @@ GEM
463
463
  puma (6.6.1)
464
464
  nio4r (~> 2.0)
465
465
  racc (1.8.1)
466
- rack (3.2.4)
466
+ rack (3.2.5)
467
467
  rack-session (2.1.1)
468
468
  base64 (>= 0.1.0)
469
469
  rack (>= 3.0.0)
@@ -1,4 +1,4 @@
1
- <div data-controller="toggle" data-component-name="<%= self.class.to_s.underscore %>">
1
+ <div data-controller="toggle" data-toggle-exemption-containers-value='[".flatpickr-calendar"]' data-component-name="<%= self.class.to_s.underscore %>">
2
2
  <div class="relative w-full flex justify-between">
3
3
  <%= a_button class: 'focus:outline-none',
4
4
  color: :primary,
@@ -8,7 +8,7 @@
8
8
  <% end %>
9
9
  <% c.with_body do %>
10
10
  <% content_tag :div, class: "divide-y overflow-auto" do %>
11
- <%= render Avo::Items::VisibleItemsComponent.new resource: @resource, item: @item, view: @view, form: @form %>
11
+ <%= render Avo::Items::VisibleItemsComponent.new resource: @resource, item: @item, view: @view, form: @form, reflection: @reflection %>
12
12
  <% end %>
13
13
  <% end %>
14
14
  <% if sidebars.any? { |sidebar| sidebar.visible_items.any? } %>
@@ -11,7 +11,7 @@
11
11
  <% elsif item.is_row? %>
12
12
  <%= render Avo::RowComponent.new(divider: item.divider) do |c| %>
13
13
  <% c.with_body do %>
14
- <%= render Avo::Items::VisibleItemsComponent.new resource: @resource, item: @item, view: @view, form: @form %>
14
+ <%= render Avo::Items::VisibleItemsComponent.new resource: @resource, item: @item, view: @view, form: @form, reflection: @reflection %>
15
15
  <% end %>
16
16
  <% end %>
17
17
  <% elsif item.is_collaboration? %>
@@ -42,7 +42,7 @@ class Avo::Items::SwitcherComponent < Avo::BaseComponent
42
42
  def render?
43
43
  # Stops rendering if the field should be hidden in reflections
44
44
  if item.is_field?
45
- return false if in_reflection? && item.hidden_in_reflection?
45
+ return false if in_reflection? && item.hidden_in_reflection?(@reflection)
46
46
  end
47
47
 
48
48
  true
@@ -1,6 +1,7 @@
1
1
  <% @item.visible_items.each_with_index do |field, index| %>
2
2
  <%= render Avo::Items::SwitcherComponent.new(
3
3
  resource: @resource,
4
+ reflection: @reflection,
4
5
  item: field,
5
6
  index: index,
6
7
  view: @view,
@@ -5,5 +5,6 @@ class Avo::Items::VisibleItemsComponent < Avo::BaseComponent
5
5
  prop :item
6
6
  prop :view
7
7
  prop :form
8
+ prop :reflection, default: nil
8
9
  prop :field_component_extra_args, default: {}.freeze
9
10
  end
@@ -7,7 +7,6 @@ module Avo
7
7
 
8
8
  before_action :set_resource_name
9
9
  before_action :set_resource
10
- before_action :set_applied_filters, only: :index
11
10
  before_action :set_record, only: [:show, :edit, :destroy, :update, :preview]
12
11
  before_action :set_record_to_fill, only: [:new, :edit, :create, :update]
13
12
  before_action :detect_fields
@@ -26,6 +25,7 @@ module Avo
26
25
  end
27
26
  add_breadcrumb @resource.plural_name.humanize
28
27
 
28
+ set_applied_filters
29
29
  set_index_params
30
30
  set_filters
31
31
  set_actions
@@ -92,7 +92,6 @@ module Avo
92
92
  add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
93
93
  end
94
94
 
95
-
96
95
  add_breadcrumb @resource.record_title
97
96
  add_breadcrumb I18n.t("avo.details").upcase_first
98
97
 
@@ -138,6 +138,7 @@ export default class extends Controller {
138
138
  },
139
139
  altInput: true,
140
140
  onChange: this.onChange.bind(this),
141
+ onClose: this.onClose.bind(this),
141
142
  noCalendar: false,
142
143
  ...this.pickerOptionsValue,
143
144
  }
@@ -223,6 +224,17 @@ export default class extends Controller {
223
224
  this.updateRealInput(value)
224
225
  }
225
226
 
227
+ onClose(selectedDates, dateStr, instance) {
228
+ if (instance.config.allowInput && instance.altInput.value) {
229
+ const value = instance.altInput.value
230
+ if (value) {
231
+ instance.setDate(value, true, instance.config.altFormat)
232
+ } else {
233
+ this.updateRealInput('')
234
+ }
235
+ }
236
+ }
237
+
226
238
  // Value should be a string
227
239
  updateRealInput(value) {
228
240
  this.inputTarget.value = value
@@ -28,9 +28,16 @@ export default class extends Controller {
28
28
  options.status = false
29
29
  }
30
30
 
31
- const easyMde = new EasyMDE(options)
31
+ this.easyMde = new EasyMDE(options)
32
32
  if (this.view === 'show') {
33
- easyMde.codemirror.options.readOnly = true
33
+ this.easyMde.codemirror.options.readOnly = true
34
+ }
35
+ }
36
+
37
+ disconnect() {
38
+ if (this.easyMde) {
39
+ this.easyMde.toTextArea()
40
+ this.easyMde = null
34
41
  }
35
42
  }
36
43
  }
@@ -151,6 +151,15 @@ export default class extends Controller {
151
151
  return result
152
152
  }
153
153
 
154
+ escapeAttribute(str) {
155
+ if (str === null || str === undefined) return ''
156
+ return String(str)
157
+ .replace(/&/g, '&amp;')
158
+ .replace(/"/g, '&quot;')
159
+ .replace(/</g, '&lt;')
160
+ .replace(/>/g, '&gt;')
161
+ }
162
+
154
163
  inputField(id = 'key', index, key, value) {
155
164
  const inputValue = id === 'key' ? key : value
156
165
 
@@ -160,7 +169,7 @@ export default class extends Controller {
160
169
  placeholder="${this.options[`${id}_label`]}"
161
170
  data-index="${index}"
162
171
  ${this[`${id}InputDisabled`] ? "disabled='disabled'" : ''}
163
- value="${typeof inputValue === 'undefined' || inputValue === null ? '' : inputValue}"
172
+ value="${this.escapeAttribute(inputValue)}"
164
173
  />`
165
174
  }
166
175
 
@@ -79,6 +79,13 @@ export default class extends Controller {
79
79
  }
80
80
  }
81
81
 
82
+ disconnect() {
83
+ if (this.tagify) {
84
+ this.tagify.destroy()
85
+ this.tagify = null
86
+ }
87
+ }
88
+
82
89
  initTagify() {
83
90
  this.tagify = new Tagify(this.inputTarget, this.tagifyOptions)
84
91
  const that = this
@@ -77,6 +77,13 @@ export default class extends Controller {
77
77
  this.setupFileInput()
78
78
  }
79
79
 
80
+ disconnect() {
81
+ if (this.fileInput) {
82
+ this.fileInput.remove()
83
+ this.fileInput = null
84
+ }
85
+ }
86
+
80
87
  setupFileInput() {
81
88
  // Create a hidden file input element
82
89
  this.fileInput = document.createElement('input')
@@ -9,7 +9,7 @@ export default class extends Controller {
9
9
  connect() {
10
10
  const vm = this;
11
11
 
12
- tippy(vm.context.element, {
12
+ this.tippyInstance = tippy(vm.context.element, {
13
13
  content: "loading...",
14
14
  allowHTML: true,
15
15
  theme: 'light',
@@ -21,4 +21,11 @@ export default class extends Controller {
21
21
  },
22
22
  })
23
23
  }
24
+
25
+ disconnect() {
26
+ if (this.tippyInstance) {
27
+ this.tippyInstance.destroy()
28
+ this.tippyInstance = null
29
+ }
30
+ }
24
31
  }
@@ -82,10 +82,14 @@ export default class extends Controller {
82
82
  }
83
83
 
84
84
  #addEventListeners() {
85
+ // Create bound handler references so the same functions are used for add and remove
86
+ this.boundMouseenterHandler = this.#selectorMouseenterHandler.bind(this)
87
+ this.boundMouseleaveHandler = this.#selectorMouseleaveHandler.bind(this)
88
+
85
89
  // Attach event listeners to item selector cells
86
90
  Array.from(this.itemSelectorCells).forEach((itemSelectorCell) => {
87
- itemSelectorCell.addEventListener('mouseenter', this.#selectorMouseenterHandler.bind(this))
88
- itemSelectorCell.addEventListener('mouseleave', this.#selectorMouseleaveHandler.bind(this))
91
+ itemSelectorCell.addEventListener('mouseenter', this.boundMouseenterHandler)
92
+ itemSelectorCell.addEventListener('mouseleave', this.boundMouseleaveHandler)
89
93
  })
90
94
 
91
95
  // Attach event listeners to keyboard events
@@ -94,13 +98,13 @@ export default class extends Controller {
94
98
  }
95
99
 
96
100
  #removeEventListeners() {
97
- // Remove event listeners
101
+ // Remove event listeners using the same bound references from #addEventListeners
98
102
  Array.from(this.itemSelectorCells).forEach((itemSelectorCell) => {
99
- itemSelectorCell.removeEventListener('mouseenter', this.#selectorMouseenterHandler.bind(this))
100
- itemSelectorCell.removeEventListener('mouseleave', this.#selectorMouseleaveHandler.bind(this))
103
+ itemSelectorCell.removeEventListener('mouseenter', this.boundMouseenterHandler)
104
+ itemSelectorCell.removeEventListener('mouseleave', this.boundMouseleaveHandler)
101
105
  })
102
- document.removeEventListener('keydown', this.#keydownHandler.bind(this))
103
- document.removeEventListener('keyup', this.#keyupHandler.bind(this))
106
+ document.removeEventListener('keydown', this.#keydownHandler)
107
+ document.removeEventListener('keyup', this.#keyupHandler)
104
108
  }
105
109
 
106
110
  #selectorMouseenterHandler(event) {
@@ -127,6 +127,10 @@ export default class extends Controller {
127
127
  }
128
128
 
129
129
  disconnect() {
130
+ if (this.isGlobalSearch) {
131
+ Mousetrap.unbind(['command+k', 'ctrl+k'])
132
+ }
133
+
130
134
  // Don't leave open autocompletes around when disconnected. Otherwise it will still
131
135
  // be visible when navigating back to this page.
132
136
  if (this.destroyMethod) {
@@ -3,7 +3,15 @@ import { Controller } from '@hotwired/stimulus'
3
3
  export default class extends Controller {
4
4
  connect() {
5
5
  this.isSelecting = false
6
- this.#bindSelectionEvents()
6
+ this.boundHandleMouseDown = this.#handleMouseDown.bind(this)
7
+ this.boundHandleMouseMove = this.#handleMouseMove.bind(this)
8
+ this.element.addEventListener('mousedown', this.boundHandleMouseDown)
9
+ this.element.addEventListener('mousemove', this.boundHandleMouseMove)
10
+ }
11
+
12
+ disconnect() {
13
+ this.element.removeEventListener('mousedown', this.boundHandleMouseDown)
14
+ this.element.removeEventListener('mousemove', this.boundHandleMouseMove)
7
15
  }
8
16
 
9
17
  visitRecord(event) {
@@ -42,11 +50,6 @@ export default class extends Controller {
42
50
  }
43
51
  }
44
52
 
45
- #bindSelectionEvents() {
46
- this.element.addEventListener('mousedown', this.#handleMouseDown.bind(this))
47
- this.element.addEventListener('mousemove', this.#handleMouseMove.bind(this))
48
- }
49
-
50
53
  #handleMouseDown() {
51
54
  this.isSelecting = false
52
55
  }
@@ -5,10 +5,17 @@ export default class extends Controller {
5
5
  static targets = ['source', 'content']
6
6
 
7
7
  connect() {
8
- tippy(this.sourceTarget, {
8
+ this.tippyInstance = tippy(this.sourceTarget, {
9
9
  content: this.contentTarget.innerHTML,
10
10
  allowHTML: true,
11
11
  theme: 'light',
12
12
  })
13
13
  }
14
+
15
+ disconnect() {
16
+ if (this.tippyInstance) {
17
+ this.tippyInstance.destroy()
18
+ this.tippyInstance = null
19
+ }
20
+ }
14
21
  }
@@ -11,7 +11,7 @@ export default class extends Controller {
11
11
  }
12
12
 
13
13
  get exemptionContainerTargets() {
14
- return this.exemptionContainersValue.map((selector) => document.querySelector(selector)).filter(Boolean)
14
+ return this.exemptionContainersValue.flatMap((selector) => [...document.querySelectorAll(selector)])
15
15
  }
16
16
 
17
17
  connect() {
@@ -272,12 +272,12 @@ module Avo
272
272
  true
273
273
  end
274
274
 
275
- def visible_in_reflection?
275
+ def visible_in_reflection?(reflection = nil)
276
276
  true
277
277
  end
278
278
 
279
- def hidden_in_reflection?
280
- !visible_in_reflection?
279
+ def hidden_in_reflection?(reflection = nil)
280
+ !visible_in_reflection?(reflection)
281
281
  end
282
282
 
283
283
  def options_for_filter
@@ -308,6 +308,21 @@ module Avo
308
308
  "#{id}_type"
309
309
  end
310
310
 
311
+ # When displayed inside a has_one/has_many reflection, hide this field if it
312
+ # points back to the parent record (i.e., it is the inverse of the reflection).
313
+ # This mirrors the filtering logic in Avo::Concerns::HasItems#get_fields.
314
+ def visible_in_reflection?(reflection = nil)
315
+ return true if reflection.nil?
316
+ return true unless respond_to?(:foreign_key)
317
+ return true unless reflection.inverse_of.present?
318
+ return true unless reflection.inverse_of.respond_to?(:foreign_key)
319
+
320
+ inverse_fk = reflection.inverse_of.foreign_key
321
+ self_fk = is_polymorphic? ? self.reflection&.foreign_key : foreign_key
322
+
323
+ inverse_fk != self_fk
324
+ end
325
+
311
326
  private
312
327
 
313
328
  def get_model_class(record)
@@ -64,7 +64,7 @@ module Avo
64
64
  true
65
65
  end
66
66
 
67
- def visible_in_reflection?
67
+ def visible_in_reflection?(reflection = nil)
68
68
  false
69
69
  end
70
70
 
@@ -204,6 +204,12 @@ module Avo
204
204
  find(".flatpickr-second").set(value)
205
205
  end
206
206
 
207
+ def set_picker_text_input(value)
208
+ element = find("input.form-control[type='text']")
209
+ # Set value without firing input/change events
210
+ page.execute_script("arguments[0].value = arguments[1]", element.native, value)
211
+ end
212
+
207
213
  def open_picker
208
214
  text_input.click
209
215
  end
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "3.29.1" unless const_defined?(:VERSION)
2
+ VERSION = "3.30.0" unless const_defined?(:VERSION)
3
3
  end
@@ -1779,7 +1779,7 @@ span.flatpickr-weekday {
1779
1779
  }
1780
1780
  }
1781
1781
 
1782
- /*! @algolia/autocomplete-theme-classic 1.19.5 | MIT License | © Algolia, Inc. and contributors | https://github.com/algolia/autocomplete */
1782
+ /*! @algolia/autocomplete-theme-classic 1.19.6 | MIT License | © Algolia, Inc. and contributors | https://github.com/algolia/autocomplete */
1783
1783
 
1784
1784
  /* ----------------*/
1785
1785