avo 4.2.5 → 4.2.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bba089d3b0fe4f9d84ca2284da37ced9876bb8380a883a29c7a542c4c7bd79c
4
- data.tar.gz: 46fe47577923cc7f1de8e6df76fefcfb4ab999360eb59a08d39d48d111687fa2
3
+ metadata.gz: 6a64a8878e0c42cef0fcbe360cdf217635be5cdcca396cf180888e24a034ab8d
4
+ data.tar.gz: f05b38be082e6fbe6cc9c5f5c85c87452108d8539dd284d320bd3bd09c9fb92d
5
5
  SHA512:
6
- metadata.gz: 8501ec94f95a72ab5ed6eb2d4750791686440b3f74ee17630f553ea169ab26d9ca475163562aa0ab4a5735d6e3ada3082f2421c0c52e7a1db3d2d35d41590d4a
7
- data.tar.gz: de04871e9e0a0f6d058197970d3424b7ce10b7cb810edfa0aab06ea598fc132ef16c041bd28816b218ac5d3b61dbb095982ae14bf56c9d4661540b12524ce522
6
+ metadata.gz: 19222c1d66ac795e3e0fa668ca5a903604dbe3ae412e2aa528bb8afe6e753cf79abfdd3f3d84f0cfaaecca6e4847ce56f692a14a7abbec498e524c5bbe63009a
7
+ data.tar.gz: 294e34314e882e5cc255f34e9060f683e384fa888795083275566546d305955d7d090e7293471e1588ea4c0f06a9afb8d4b3085c31e611b98c2ee16bad037d2c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (4.2.5)
4
+ avo (4.2.6)
5
5
  actionview (>= 6.1)
6
6
  active_link_to
7
7
  activerecord (>= 6.1)
@@ -120,7 +120,7 @@ GEM
120
120
  prism
121
121
  addressable (2.9.0)
122
122
  public_suffix (>= 2.0.2, < 8.0)
123
- amazing_print (2.0.0)
123
+ amazing_print (3.0.0)
124
124
  annotate (2.6.5)
125
125
  activerecord (>= 2.3.0)
126
126
  rake (>= 0.8.7)
@@ -139,8 +139,8 @@ GEM
139
139
  avo-record_link_field (0.0.2)
140
140
  avo-rhino_field (4.1.0)
141
141
  aws-eventstream (1.4.0)
142
- aws-partitions (1.1283.0)
143
- aws-sdk-core (3.254.1)
142
+ aws-partitions (1.1287.0)
143
+ aws-sdk-core (3.257.0)
144
144
  aws-eventstream (~> 1, >= 1.3.0)
145
145
  aws-partitions (~> 1, >= 1.992.0)
146
146
  aws-sigv4 (~> 1.9)
@@ -148,11 +148,12 @@ GEM
148
148
  bigdecimal
149
149
  jmespath (~> 1, >= 1.6.1)
150
150
  logger
151
- aws-sdk-kms (1.130.0)
152
- aws-sdk-core (~> 3, >= 3.254.0)
151
+ rexml (~> 3.4, >= 3.4.2)
152
+ aws-sdk-kms (1.132.0)
153
+ aws-sdk-core (~> 3, >= 3.256.0)
153
154
  aws-sigv4 (~> 1.5)
154
- aws-sdk-s3 (1.229.0)
155
- aws-sdk-core (~> 3, >= 3.254.1)
155
+ aws-sdk-s3 (1.232.1)
156
+ aws-sdk-core (~> 3, >= 3.256.0)
156
157
  aws-sdk-kms (~> 1)
157
158
  aws-sigv4 (~> 1.5)
158
159
  aws-sigv4 (1.12.1)
@@ -179,9 +180,9 @@ GEM
179
180
  erubi (~> 1.4)
180
181
  parser (>= 2.4)
181
182
  smart_properties
182
- bigdecimal (4.1.2)
183
+ bigdecimal (4.1.3)
183
184
  bindex (0.8.1)
184
- bootsnap (1.25.0)
185
+ bootsnap (1.26.0)
185
186
  msgpack (~> 1.5)
186
187
  builder (3.3.0)
187
188
  bump (0.10.0)
@@ -218,15 +219,16 @@ GEM
218
219
  bigdecimal
219
220
  rexml
220
221
  crass (1.0.7)
221
- css_parser (3.0.0)
222
+ css_parser (3.2.0)
222
223
  addressable
224
+ base64
223
225
  ssrf_filter (~> 1.5)
224
226
  cssbundling-rails (1.4.3)
225
227
  railties (>= 6.0.0)
226
228
  csv (3.3.6)
227
- cuprite (0.17)
229
+ cuprite (0.18)
228
230
  capybara (~> 3.0)
229
- ferrum (~> 0.17.0)
231
+ ferrum (~> 0.18.0)
230
232
  danger (9.6.1)
231
233
  base64 (~> 0.2)
232
234
  claide (~> 1.0)
@@ -326,21 +328,20 @@ GEM
326
328
  railties (>= 6.1.0)
327
329
  faker (3.8.0)
328
330
  i18n (>= 1.8.11, < 2)
329
- faraday (2.14.3)
331
+ faraday (2.14.4)
330
332
  faraday-net_http (>= 2.0, < 3.5)
331
333
  json
332
334
  logger
333
- faraday-http-cache (2.7.0)
335
+ faraday-http-cache (2.8.0)
334
336
  faraday (>= 0.8)
335
337
  faraday-multipart (1.2.0)
336
338
  multipart-post (~> 2.0)
337
339
  faraday-net_http (3.4.4)
338
340
  net-http (~> 0.5)
339
- ferrum (0.17.2)
341
+ ferrum (0.18.0)
340
342
  addressable (~> 2.5)
341
343
  base64 (~> 0.2)
342
344
  concurrent-ruby (~> 1.1)
343
- webrick (~> 1.7)
344
345
  websocket-driver (~> 0.7)
345
346
  ffi (1.17.4)
346
347
  ffi (1.17.4-arm64-darwin)
@@ -362,7 +363,7 @@ GEM
362
363
  gem-release (2.2.4)
363
364
  get_process_mem (0.2.7)
364
365
  ffi (~> 1.0)
365
- git (5.2.0)
366
+ git (5.6.0)
366
367
  activesupport (>= 5.0)
367
368
  addressable (~> 2.8)
368
369
  process_executer (~> 4.1)
@@ -409,7 +410,7 @@ GEM
409
410
  inline_svg (1.10.0)
410
411
  activesupport (>= 3.0)
411
412
  nokogiri (>= 1.6)
412
- io-console (0.9.2)
413
+ io-console (0.9.4)
413
414
  irb (1.18.0)
414
415
  pp (>= 0.6.0)
415
416
  prism (>= 1.3.0)
@@ -431,8 +432,8 @@ GEM
431
432
  addressable (~> 2.8)
432
433
  childprocess (~> 5.0)
433
434
  logger (~> 1.6)
434
- lexxy (0.9.31)
435
- rails (>= 8.0.2)
435
+ lexxy (0.9.32)
436
+ railties (>= 8.0.2)
436
437
  lint_roller (1.1.0)
437
438
  listen (3.10.0)
438
439
  logger
@@ -485,17 +486,17 @@ GEM
485
486
  monetize (~> 1.9)
486
487
  money (~> 6.13)
487
488
  railties (>= 3.0)
488
- msgpack (1.8.4)
489
+ msgpack (1.8.5)
489
490
  multipart-post (2.4.1)
490
491
  nap (1.1.0)
491
492
  net-http (0.9.1)
492
493
  uri (>= 0.11.1)
493
- net-imap (0.6.6)
494
+ net-imap (0.6.7)
494
495
  date
495
496
  net-protocol
496
497
  net-pop (0.1.2)
497
498
  net-protocol
498
- net-protocol (0.3.0)
499
+ net-protocol (0.4.0)
499
500
  timeout
500
501
  net-smtp (0.5.1)
501
502
  net-protocol
@@ -517,8 +518,8 @@ GEM
517
518
  json
518
519
  uri
519
520
  yaml
520
- parallel (2.1.0)
521
- parallel_tests (5.7.0)
521
+ parallel (2.2.0)
522
+ parallel_tests (5.8.0)
522
523
  parallel
523
524
  parser (3.3.12.0)
524
525
  ast (~> 2.4.1)
@@ -707,19 +708,19 @@ GEM
707
708
  simplecov (>= 0.22.0)
708
709
  tty-which (~> 0.5.0)
709
710
  virtus (~> 2.0)
710
- rubyzip (3.6.0)
711
+ rubyzip (3.7.0)
711
712
  sawyer (0.9.3)
712
713
  addressable (>= 2.3.5)
713
714
  faraday (>= 0.17.3, < 3)
714
715
  securerandom (0.4.1)
715
- selenium-webdriver (4.48.0)
716
+ selenium-webdriver (4.49.0)
716
717
  base64 (~> 0.2)
717
718
  logger (~> 1.4)
718
719
  rexml (~> 3.2, >= 3.2.5)
719
720
  rubyzip (>= 1.2.2, < 4.0)
720
721
  websocket (~> 1.0)
721
- sexp_processor (4.17.5)
722
- simplecov (1.1.1)
722
+ sexp_processor (4.17.6)
723
+ simplecov (1.3.0)
723
724
  simplecov-cobertura (4.0.0)
724
725
  rexml
725
726
  simplecov (~> 1.0)
@@ -754,7 +755,7 @@ GEM
754
755
  spring (4.7.0)
755
756
  spring-commands-rspec (1.0.4)
756
757
  spring (>= 0.9.1)
757
- ssrf_filter (1.5.0)
758
+ ssrf_filter (1.6.0)
758
759
  standard (1.56.0)
759
760
  language_server-protocol (~> 3.17.0.2)
760
761
  lint_roller (~> 1.0)
@@ -789,9 +790,9 @@ GEM
789
790
  tzinfo (2.0.6)
790
791
  concurrent-ruby (~> 1.0)
791
792
  unaccent (0.4.0)
792
- unicode-display_width (3.2.0)
793
- unicode-emoji (~> 4.1)
794
- unicode-emoji (4.2.0)
793
+ unicode-display_width (3.3.0)
794
+ unicode-emoji (~> 4.3)
795
+ unicode-emoji (4.3.0)
795
796
  uri (1.1.1)
796
797
  useragent (0.16.11)
797
798
  view_component (4.15.0)
@@ -812,7 +813,6 @@ GEM
812
813
  addressable (>= 2.8.0)
813
814
  crack (>= 0.3.2)
814
815
  hashdiff (>= 0.4.0, < 2.0.0)
815
- webrick (1.9.2)
816
816
  websocket (1.2.11)
817
817
  websocket-driver (0.8.2)
818
818
  base64
@@ -4019,6 +4019,25 @@
4019
4019
  .tippy-box[data-theme~='preview'] .tippy-arrow {
4020
4020
  display: none;
4021
4021
  }
4022
+ .tooltip-anchor {
4023
+ max-width: 100%;
4024
+ min-width: 0px;
4025
+ }
4026
+ .tooltip-anchor--inline {
4027
+ display: inline-block;
4028
+ }
4029
+ .tooltip-anchor--block {
4030
+ display: block;
4031
+ width: 100%;
4032
+ }
4033
+ .label-tooltip__icon {
4034
+ margin-inline-start: var(--spacing);
4035
+ display: inline-block;
4036
+ width: 1em;
4037
+ height: 1em;
4038
+ vertical-align: -0.125em;
4039
+ color: var(--color-content-secondary);
4040
+ }
4022
4041
  tags.tagify {
4023
4042
  --tags-border-color: var(--color-tertiary);
4024
4043
  --tags-hover-border-color: var(--color-tertiary);
@@ -36,3 +36,24 @@
36
36
  @apply hidden;
37
37
  }
38
38
 
39
+ /* Anchors for the field `tooltip:` option. Inline hugs the value so the bubble
40
+ sits over a badge or a date rather than over the row; block keeps the width
41
+ of values that fill the row (editors, maps, progress bars). */
42
+ .tooltip-anchor {
43
+ @apply min-w-0 max-w-full;
44
+ }
45
+
46
+ .tooltip-anchor--inline {
47
+ @apply inline-block;
48
+ }
49
+
50
+ .tooltip-anchor--block {
51
+ @apply block w-full;
52
+ }
53
+
54
+ /* The cue that marks a label as hoverable, for the `label_tooltip:` option.
55
+ Sized off the label's own text so it fits the Show label and the smaller
56
+ Index header alike. */
57
+ .label-tooltip__icon {
58
+ @apply inline-block size-[1em] ms-1 align-[-0.125em] text-content-secondary;
59
+ }
@@ -7,17 +7,20 @@
7
7
  class: label_classes,
8
8
  style: label_style,
9
9
  data: label_data do %>
10
- <span class="self-center">
10
+ <%= content_tag :span, class: label_text_classes, **tooltip_attributes(label_tooltip) do %>
11
11
  <% if @form.present? %>
12
12
  <%= @form.label label_for, label %>
13
13
  <% else %>
14
14
  <%= @field.name %>
15
15
  <% end %>
16
16
  <% if on_edit? && @field.is_required? %> <span class="text-danger-content ms-1">*</span> <% end %>
17
+ <% if label_tooltip.present? %>
18
+ <%= helpers.svg "tabler/outline/info-circle", class: "label-tooltip__icon" %>
19
+ <% end %>
17
20
  <% if label_help.present? %>
18
21
  <div class="field-wrapper__label-help"><%== label_help %></div>
19
22
  <% end %>
20
- </span>
23
+ <% end %>
21
24
  <% end %>
22
25
  <%= content_tag :div,
23
26
  class: content_classes,
@@ -26,23 +29,25 @@
26
29
  <%= tag.div class: "field-wrapper__content-wrapper" do %>
27
30
  <% if on_show? %>
28
31
  <div class="flex flex-row items-center group gap-x-1 group/clipboard w-full">
29
- <% if render_dash? %>
30
- —
31
- <% elsif collapsable? %>
32
- <%= render Avo::Fields::Common::CollapsableContentComponent.new do %>
32
+ <%= value_tooltip_anchor(block: block_tooltip_anchor?) do %>
33
+ <% if render_dash? %>
34
+ —
35
+ <% elsif collapsable? %>
36
+ <%= render Avo::Fields::Common::CollapsableContentComponent.new do %>
37
+ <%= content %>
38
+ <% end %>
39
+ <% else %>
33
40
  <%= content %>
34
41
  <% end %>
35
- <% else %>
36
- <%= content %>
37
42
  <% end %>
38
43
  <% if @field.copyable %>
39
44
  <%= render Avo::ClipboardComponent.new(value: @field.value) %>
40
45
  <% end %>
41
46
  </div>
42
47
  <% elsif on_edit? %>
43
- <div class="field-wrapper__input">
48
+ <%= content_tag :div, class: "field-wrapper__input", **tooltip_attributes(tooltip) do %>
44
49
  <%= content %>
45
- </div>
50
+ <% end %>
46
51
  <% if @field.record_errors.include?(@field.id) %>
47
52
  <div class="field-wrapper__error"><%= @field.record_errors.full_messages_for(@field.id).to_sentence %></div>
48
53
  <% end %>
@@ -2,6 +2,7 @@
2
2
 
3
3
  class Avo::FieldWrapperComponent < Avo::BaseComponent
4
4
  include Avo::Concerns::HasResourceStimulusControllers
5
+ include Avo::Concerns::HasFieldTooltips
5
6
 
6
7
  prop :collapsable, default: false
7
8
  prop :dash_if_blank, default: true
@@ -90,6 +91,16 @@ class Avo::FieldWrapperComponent < Avo::BaseComponent
90
91
  Avo::ExecutionContext.new(target: @label_help || @field.label_help, record: record, resource: @resource, view: @view).handle
91
92
  end
92
93
 
94
+ def label_text_classes
95
+ class_names("self-center", "label-tooltip": label_tooltip.present?)
96
+ end
97
+
98
+ # Collapsable content fills the row like a block-shaped field does, so its
99
+ # anchor keeps that width too.
100
+ def block_tooltip_anchor?
101
+ collapsable? || @field.tooltip_anchor == :block
102
+ end
103
+
93
104
  def record
94
105
  @resource.present? ? @resource.record : nil
95
106
  end
@@ -14,14 +14,14 @@
14
14
  },
15
15
  style: @field.get_html(:style, view: @view, element: :wrapper) do %>
16
16
  <% if render_dash? %>
17
- —
17
+ <%= value_tooltip_anchor { "—" } %>
18
18
  <% else %>
19
19
  <% if @center_content %>
20
20
  <div class="flex items-center justify-center">
21
- <%= content %>
21
+ <%= value_tooltip_anchor(block: @field.tooltip_anchor == :block) { content } %>
22
22
  </div>
23
23
  <% else %>
24
- <%= content %>
24
+ <%= value_tooltip_anchor(block: @field.tooltip_anchor == :block) { content } %>
25
25
  <% end %>
26
26
  <% if @field.copyable %>
27
27
  <%= render Avo::ClipboardComponent.new(value: @field.value) %>
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::Index::FieldWrapperComponent < Avo::BaseComponent
4
+ include Avo::Concerns::HasFieldTooltips
5
+
4
6
  # Vertical padding for each density step. `nil` density falls back to
5
7
  # `Avo.configuration.density`.
6
8
  DENSITY_PADDING = {
@@ -446,9 +446,9 @@ module Avo
446
446
  page_key = "#{pagination_key}.page"
447
447
 
448
448
  @index_params[:page] = if Avo.configuration.session_persistence_enabled?
449
- session[page_key] = params[:page] || session[page_key] || 1
449
+ session[page_key] = positive_integer_or(params[:page] || session[page_key], 1)
450
450
  else
451
- params[:page] || 1
451
+ positive_integer_or(params[:page], 1)
452
452
  end
453
453
  end
454
454
 
@@ -457,9 +457,9 @@ module Avo
457
457
  per_page_key = "#{pagination_key}.per_page"
458
458
 
459
459
  @index_params[:per_page] = if Avo.configuration.session_persistence_enabled?
460
- session[per_page_key] = params[:per_page] || session[per_page_key] || Avo.configuration.via_per_page
460
+ session[per_page_key] = positive_integer_or(params[:per_page] || session[per_page_key], Avo.configuration.via_per_page)
461
461
  else
462
- params[:per_page] || Avo.configuration.via_per_page
462
+ positive_integer_or(params[:per_page], Avo.configuration.via_per_page)
463
463
  end
464
464
  end
465
465
  end
@@ -706,12 +706,17 @@ module Avo
706
706
  end
707
707
 
708
708
  def set_pagination_params
709
- @index_params[:page] = params[:page] || 1
709
+ @index_params[:page] = positive_integer_or(params[:page], 1)
710
+ @index_params[:per_page] = positive_integer_or(params[:per_page].presence || cookies[:per_page], Avo.configuration.per_page)
710
711
 
711
712
  # If the request includes the 'per_page' parameter, save its value to the cookies
712
- cookies[:per_page] = params[:per_page] if params[:per_page].present?
713
+ cookies[:per_page] = @index_params[:per_page] if params[:per_page].present?
714
+ end
713
715
 
714
- @index_params[:per_page] = cookies[:per_page] || Avo.configuration.per_page
716
+ # Pagy raises on anything that is not a positive integer, so fall back when the value is not one.
717
+ def positive_integer_or(value, fallback)
718
+ value = value.to_s.to_i
719
+ value.positive? ? value : fallback
715
720
  end
716
721
 
717
722
  # If we don't get a query object predefined from a child controller like associations, just spin one up.
@@ -53,6 +53,7 @@
53
53
  sort_direction = first_option
54
54
  end
55
55
  text_classes = "text-content-secondary tracking-tight leading-tight text-xs font-semibold"
56
+ label_tooltip = field.label_tooltip
56
57
  table_header_class = field.table_header_class
57
58
  end_aligned = table_header_class.to_s.split.include?("text-end") %>
58
59
 
@@ -86,7 +87,7 @@
86
87
  "flex-row-reverse": end_aligned
87
88
  ),
88
89
  'data-turbo-frame': params[:turbo_frame] do %>
89
- <%= field.table_header_label %>
90
+ <%= render partial: "avo/partials/table_header_label", locals: {field: field, label_tooltip: label_tooltip} %>
90
91
 
91
92
  <%= render partial: 'avo/partials/sortable_component', locals: {
92
93
  field: field,
@@ -95,7 +96,7 @@
95
96
  <% end %>
96
97
  <% else %>
97
98
  <%= content_tag :div, class: text_classes, data: {sortable: false} do %>
98
- <%= field.table_header_label %>
99
+ <%= render partial: "avo/partials/table_header_label", locals: {field: field, label_tooltip: label_tooltip} %>
99
100
  <% end %>
100
101
  <% end %>
101
102
 
@@ -0,0 +1,10 @@
1
+ <%# The label alone unless the field declares `label_tooltip:`; then the text and
2
+ its cue share one anchor, so hovering either shows the bubble. %>
3
+ <% if label_tooltip.present? %>
4
+ <%= content_tag :span, class: "label-tooltip", title: label_tooltip, data: {tippy: :tooltip} do %>
5
+ <%= field.table_header_label %>
6
+ <%= svg "tabler/outline/info-circle", class: "label-tooltip__icon" %>
7
+ <% end %>
8
+ <% else %>
9
+ <%= field.table_header_label %>
10
+ <% end %>
@@ -0,0 +1,38 @@
1
+ module Avo
2
+ module Concerns
3
+ # Hover tooltips on a field's label and value, declared with the field's
4
+ # `tooltip:` and `label_tooltip:` options. tippy takes the text from
5
+ # `title` and selects on `data-tippy`, so both attributes travel together.
6
+ module HasFieldTooltips
7
+ extend ActiveSupport::Concern
8
+
9
+ def tooltip
10
+ @resolved_tooltip ||= @field.tooltip
11
+ end
12
+
13
+ def label_tooltip
14
+ @resolved_label_tooltip ||= @field.label_tooltip
15
+ end
16
+
17
+ def tooltip_attributes(text)
18
+ return {} if text.blank?
19
+
20
+ {title: text, data: {tippy: :tooltip}}
21
+ end
22
+
23
+ # Wraps the rendered value in the tooltip anchor. Inline hugs the value so
24
+ # the bubble sits over a badge or a date rather than mid-row; block keeps
25
+ # the width of values that fill the row (editors, maps, bars). Rendered
26
+ # only when there is a tooltip: an empty wrapper would still change how
27
+ # the value sizes.
28
+ def value_tooltip_anchor(block: false, &content_block)
29
+ return capture(&content_block) if tooltip.blank?
30
+
31
+ content_tag :div,
32
+ class: class_names("tooltip-anchor", block ? "tooltip-anchor--block" : "tooltip-anchor--inline"),
33
+ **tooltip_attributes(tooltip),
34
+ &content_block
35
+ end
36
+ end
37
+ end
38
+ end
@@ -41,6 +41,10 @@ module Avo
41
41
  def geometry
42
42
  @geometry.to_s.classify
43
43
  end
44
+
45
+ def tooltip_anchor
46
+ :block
47
+ end
44
48
  end
45
49
  end
46
50
  end
@@ -20,6 +20,9 @@ module Avo
20
20
  include Avo::Fields::Concerns::DomId
21
21
 
22
22
  include ActionView::Helpers::UrlHelper
23
+ # Rails 8.2 extracted link_to, button_to and current_page? into NavigationHelper
24
+ # (rails/rails#58735). Without it, link_to on a resource is a NoMethodError there.
25
+ include ActionView::Helpers::NavigationHelper if defined?(ActionView::Helpers::NavigationHelper)
23
26
 
24
27
  delegate :app, to: ::Avo::Current
25
28
  delegate :view_context, to: :app
@@ -92,6 +95,8 @@ module Avo
92
95
  @autocomplete = args[:autocomplete]
93
96
  @help = args[:help]
94
97
  @label_help = args[:label_help]
98
+ @tooltip = args[:tooltip]
99
+ @label_tooltip = args[:label_tooltip]
95
100
  @default = args[:default]
96
101
  @visible = args[:visible]
97
102
  @html = args[:html]
@@ -227,6 +232,26 @@ module Avo
227
232
  translated_option(:help)
228
233
  end
229
234
 
235
+ # Hover text for the value: anchored to the value itself on Index and
236
+ # Show, to the input on forms. Resolved here rather than in the wrapper
237
+ # because the Index header renders the label outside any wrapper.
238
+ def tooltip
239
+ execute_context(@tooltip.nil? ? translated_option(:tooltip) : @tooltip)
240
+ end
241
+
242
+ # Hover text for the label, on every view and on the Index header.
243
+ def label_tooltip
244
+ execute_context(@label_tooltip.nil? ? translated_option(:label_tooltip) : @label_tooltip)
245
+ end
246
+
247
+ # How the value tooltip anchors to the rendered value. `:inline` hugs the
248
+ # value so the bubble sits over a badge or a date rather than mid-row;
249
+ # fields whose value fills the row (editors, maps, progress bars) answer
250
+ # `:block` so the anchor keeps their width instead of collapsing it.
251
+ def tooltip_anchor
252
+ :inline
253
+ end
254
+
230
255
  def placeholder
231
256
  target = if !@placeholder.nil?
232
257
  @placeholder
@@ -29,6 +29,10 @@ module Avo
29
29
  @line_wrapping = args[:line_wrapping].present? ? args[:line_wrapping] : true
30
30
  @always_show = args[:always_show] || false
31
31
  end
32
+
33
+ def tooltip_anchor
34
+ :block
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -20,6 +20,10 @@ module Avo
20
20
  height: @height
21
21
  }
22
22
  end
23
+
24
+ def tooltip_anchor
25
+ :block
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -33,6 +33,10 @@ module Avo
33
33
 
34
34
  record
35
35
  end
36
+
37
+ def tooltip_anchor
38
+ :block
39
+ end
36
40
  end
37
41
  end
38
42
  end
@@ -113,6 +113,10 @@ module Avo
113
113
  record.send(:"#{database_id}=", value[index])
114
114
  end
115
115
  end
116
+
117
+ def tooltip_anchor
118
+ :block
119
+ end
116
120
  end
117
121
  end
118
122
  end
@@ -14,6 +14,10 @@ module Avo
14
14
  @display_value = args[:display_value] || false
15
15
  @value_suffix = args[:value_suffix] || nil
16
16
  end
17
+
18
+ def tooltip_anchor
19
+ :block
20
+ end
17
21
  end
18
22
  end
19
23
  end
@@ -14,17 +14,23 @@ module Avo
14
14
  }
15
15
 
16
16
  class << self
17
+ # Each array resource keeps its own model class. It starts as a placeholder class and
18
+ # fetch_records replaces it with the class of the records it builds or finds.
17
19
  def model_class
18
- @@model_class ||= Avo.const_set(
20
+ return @model_class if @model_class
21
+
22
+ resource_class = self
23
+
24
+ Avo.send(:remove_const, class_name) if Avo.const_defined?(class_name, false)
25
+
26
+ @model_class = Avo.const_set(
19
27
  class_name,
20
28
  Class.new do
21
29
  include ActiveModel::Model
22
30
 
23
- class << self
24
- def primary_key = nil
25
-
26
- def all = "Avo::Resources::#{class_name}".constantize.new.fetch_records
27
- end
31
+ define_singleton_method(:primary_key) { nil }
32
+ define_singleton_method(:class_name) { resource_class.class_name }
33
+ define_singleton_method(:all) { resource_class.new.fetch_records }
28
34
  end
29
35
  )
30
36
  end
@@ -68,21 +74,22 @@ module Avo
68
74
  @fetched_records ||= if array_of_records.empty?
69
75
  array_of_records
70
76
  elsif is_array_of_active_records?(array_of_records)
71
- @@model_class = array_of_records.first.class
72
- @@model_class.where(id: array_of_records.map(&:id))
77
+ self.class.model_class = array_of_records.first.class
78
+ model_class.where(id: array_of_records.map(&:id))
73
79
  elsif is_active_record_relation?(array_of_records)
74
- @@model_class = array_of_records.try(:model)
80
+ self.class.model_class = array_of_records.try(:model)
75
81
  array_of_records
76
82
  elsif is_array_of_store_model?(array_of_records)
77
- @@model_class = array_of_records.first.class
83
+ self.class.model_class = array_of_records.first.class
78
84
  return(array_of_records)
79
85
  else
80
86
  # Dynamically create a class with accessors for all unique keys from the records
81
87
  keys = array_of_records.flat_map(&:keys).uniq
88
+ resource_class = self.class
82
89
 
83
90
  Avo.send(:remove_const, class_name) if Avo.const_defined?(class_name, false)
84
91
 
85
- Avo.const_set(
92
+ custom_class = Avo.const_set(
86
93
  class_name,
87
94
  Class.new do
88
95
  include ActiveModel::Model
@@ -94,13 +101,14 @@ module Avo
94
101
  id
95
102
  end
96
103
 
97
- class << self
98
- def class_name = class_name
99
- end
104
+ define_singleton_method(:primary_key) { nil }
105
+ define_singleton_method(:class_name) { resource_class.class_name }
106
+ define_singleton_method(:all) { resource_class.new.fetch_records }
100
107
  end
101
108
  )
102
109
 
103
- custom_class = "Avo::#{class_name}".constantize
110
+ # Keep model_class in sync so the records are instances of it
111
+ resource_class.model_class = custom_class
104
112
 
105
113
  # Map the records to instances of the dynamically created class
106
114
  array_of_records.map do |item|
@@ -4,6 +4,9 @@ module Avo
4
4
  extend ActiveSupport::DescendantsTracker
5
5
 
6
6
  include ActionView::Helpers::UrlHelper
7
+ # Rails 8.2 extracted link_to, button_to and current_page? into NavigationHelper
8
+ # (rails/rails#58735). Without it, link_to on a resource is a NoMethodError there.
9
+ include ActionView::Helpers::NavigationHelper if defined?(ActionView::Helpers::NavigationHelper)
7
10
  include Avo::Concerns::HasFieldDiscovery
8
11
  include Avo::Concerns::HasItems
9
12
  include Avo::Concerns::CanReplaceItems
@@ -160,7 +160,7 @@ Turning a long `belongs_to` / `has_many` picker into a type-to-search field is t
160
160
  ## Gotchas
161
161
 
162
162
  - **Set `inverse_of` on the model association.** Avo relies on it to resolve the reciprocal; missing it causes wrong/empty attach lists and save bugs. Set it on both sides.
163
- - **The Rails association must exist first.** Adding `field :x, as: :has_many` does nothing if the model has no `has_many :x` — the association frame raises `Avo::MissingAssociationError`, naming the model, the field, and the declaration to add (`has_many :orders`).
163
+ - **The Rails association must exist first.** Adding `field :x, as: :has_many` does nothing if the model has no `has_many :x` — the association panel raises `Avo::MissingAssociationError`, naming the model, the field, and the declaration to add (`has_many :orders`). The cause is either side: the model genuinely lacks the association, or the field name doesn't match the one the model declares (`field :comments` against a `has_many :post_comments`). The **`array`** field is the one exception — it renders a panel deliberately not backed by an association, so it never raises this.
164
164
  - **`has_*` fields are hidden on Edit by default.** Add `show_on: :edit` to surface them on the form. For editing the related record *in* the form (not just displaying it), use `nested` — which needs the `avo-nested` gem.
165
165
  - **Attach/detach/create/destroy buttons come from the *target* resource's Pundit policy**, and the method names are **plural, matching the association name**: `attach_users?`, `detach_users?`, `create_users?`, `destroy_users?`, `view_users?`, `show_users?` — *not* the singular `detach_user?`. This is the #1 "why isn't my button showing" cause. Cross-link `avo-authorization`.
166
166
  - **`can_create: true` is still vetoed by the policy.** If the target resource's `create?` returns `false`, no create link appears regardless of `can_create`.
@@ -146,6 +146,7 @@ Every field accepts these common options (full list + types at the field-options
146
146
  - **`disabled:`** vs **`readonly:`** — both render the input disabled on forms. `disabled:` **also ignores the value on save** (safe against DOM tampering). `readonly:` is UI-only — a user can re-enable it and submit; don't rely on it for protection.
147
147
  - **`default:`** — pre-fills the New form (and action modals): `default: -> { Time.current }`.
148
148
  - **`help:`** / **`label_help:`** — help text (HTML allowed) below the input (forms only) or below the label (every view).
149
+ - **`tooltip:`** / **`label_tooltip:`** — hover bubble (HTML allowed) on the value (badge/text on Index and Show, the input on forms) or on the label (every view, plus the Index header, with an info icon as the cue). String or block; the block gets `record`, `resource`, `view`, `field`, and returning `nil` renders no tooltip. `record` is `nil` for `label_tooltip` on the Index header. Prefer over `help:`/`label_help:` when the context matters only on demand: `tooltip: -> { "Revoked at #{record.revoked_at}" if record.revoked_at? }`.
149
150
  - **`placeholder:`** — placeholder for empty text-like inputs.
150
151
  - **`sortable:`** — makes the Index column sortable. `true` for real columns; a `-> { query.order(...) }` lambda (receives `query` and `direction`) for computed fields.
151
152
  - **Number `format:`** — formats `:number` fields on Index and Show with `:delimited`, `:percentage`, or `:human`; forms keep the raw number. A formatted number is end-aligned in tables, while a bare number stays start-aligned for identifier/year-shaped values. Rails i18n controls delimiters and separators. For money use the `:money` field, and for anything else `format_display_using:`.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repo": "avo-hq/docs.avohq.io",
3
3
  "docs_path": "docs/4.0",
4
- "commit": "aba11fb247009e10d25a705d4e437449f4a80c5c",
5
- "date": "2026-09-17",
4
+ "commit": "7f2d52546ecf8bd84044cd011d58f5ffcdeae016",
5
+ "date": "2026-09-22",
6
6
  "note": "The docs commit these skills were last indexed from. Run ruby lib/avo/skills/bin/docs_drift.rb to see what changed since, and --update after a refresh."
7
7
  }
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.2.5" unless const_defined?(:VERSION)
2
+ VERSION = "4.2.6" unless const_defined?(:VERSION)
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avo
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.5
4
+ version: 4.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin
@@ -915,6 +915,7 @@ files:
915
915
  - app/views/avo/partials/_switcher_dropdown.html.erb
916
916
  - app/views/avo/partials/_switcher_inline.html.erb
917
917
  - app/views/avo/partials/_table_header.html.erb
918
+ - app/views/avo/partials/_table_header_label.html.erb
918
919
  - app/views/avo/partials/_view_toggle_button.html.erb
919
920
  - app/views/avo/partials/distribution_chart.html.erb
920
921
  - app/views/avo/partials/distribution_chart_full.html.erb
@@ -969,6 +970,7 @@ files:
969
970
  - lib/avo/concerns/has_description.rb
970
971
  - lib/avo/concerns/has_discreet_information.rb
971
972
  - lib/avo/concerns/has_field_discovery.rb
973
+ - lib/avo/concerns/has_field_tooltips.rb
972
974
  - lib/avo/concerns/has_helpers.rb
973
975
  - lib/avo/concerns/has_item_type.rb
974
976
  - lib/avo/concerns/has_items.rb