avo 4.2.5 → 4.2.7
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 +4 -4
- data/Gemfile.lock +38 -38
- data/app/assets/builds/avo/application.css +19 -0
- data/app/assets/stylesheets/css/components/tooltip.css +21 -0
- data/app/components/avo/field_wrapper_component.html.erb +15 -10
- data/app/components/avo/field_wrapper_component.rb +11 -0
- data/app/components/avo/index/field_wrapper_component.html.erb +3 -3
- data/app/components/avo/index/field_wrapper_component.rb +2 -0
- data/app/components/avo/modal_component.html.erb +1 -1
- data/app/components/avo/u_i/panel_component.html.erb +1 -1
- data/app/controllers/avo/associations_controller.rb +7 -4
- data/app/controllers/avo/base_controller.rb +8 -3
- data/app/controllers/avo/search_controller.rb +4 -0
- data/app/views/avo/partials/_confirm_dialog.html.erb +2 -2
- data/app/views/avo/partials/_table_header.html.erb +3 -2
- data/app/views/avo/partials/_table_header_label.html.erb +10 -0
- data/lib/avo/base_action.rb +5 -2
- data/lib/avo/concerns/has_field_tooltips.rb +38 -0
- data/lib/avo/current.rb +19 -1
- data/lib/avo/fields/area_field.rb +4 -0
- data/lib/avo/fields/base_field.rb +25 -0
- data/lib/avo/fields/code_field.rb +4 -0
- data/lib/avo/fields/easy_mde_field.rb +4 -0
- data/lib/avo/fields/files_field.rb +4 -0
- data/lib/avo/fields/location_field.rb +10 -0
- data/lib/avo/fields/progress_bar_field.rb +4 -0
- data/lib/avo/resources/array_resource.rb +23 -15
- data/lib/avo/resources/base.rb +36 -2
- data/lib/avo/services/telemetry_service.rb +15 -1
- data/lib/avo/skills/avo-associations/SKILL.md +1 -1
- data/lib/avo/skills/avo-fields/SKILL.md +1 -0
- data/lib/avo/skills/docs-index.json +2 -2
- data/lib/avo/version.rb +1 -1
- data/lib/avo/view_types/view_type_manager.rb +6 -4
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06a99742dcd8d96855fb10577085da6acf2fb2eabebc8ba66d02b653e404f962
|
|
4
|
+
data.tar.gz: 7d63ef33110cdf91bb3188ecf82abac3ecf9f56fd3c1edc53605737da96b2557
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4dec169c3e7a227e2fd6baffd9a8766c91fe0fba55a04fb3bcf9cd1691c12479a5579450e98cf27a90c1cfab99970d9b5498a9b26e3a745c443878b4f1504006
|
|
7
|
+
data.tar.gz: 8903fb5e47c672f02254cf44e90fb3df88775e0d5b382131b7ca70c3567d5d6b35815b09e6a129b827111ef447d6cc6dc1dba99655b4bf628d59b9eb114dd69d
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
avo (4.2.
|
|
4
|
+
avo (4.2.7)
|
|
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 (
|
|
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.
|
|
143
|
-
aws-sdk-core (3.
|
|
142
|
+
aws-partitions (1.1289.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
|
-
|
|
152
|
-
|
|
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.
|
|
155
|
-
aws-sdk-core (~> 3, >= 3.
|
|
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.
|
|
183
|
+
bigdecimal (4.1.3)
|
|
183
184
|
bindex (0.8.1)
|
|
184
|
-
bootsnap (1.
|
|
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.
|
|
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.
|
|
229
|
+
cuprite (0.18)
|
|
228
230
|
capybara (~> 3.0)
|
|
229
|
-
ferrum (~> 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
435
|
-
|
|
435
|
+
lexxy (0.9.33)
|
|
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.
|
|
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.
|
|
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.
|
|
499
|
+
net-protocol (0.4.0)
|
|
499
500
|
timeout
|
|
500
501
|
net-smtp (0.5.1)
|
|
501
502
|
net-protocol
|
|
@@ -513,12 +514,12 @@ GEM
|
|
|
513
514
|
openssl (4.0.2)
|
|
514
515
|
orm_adapter (0.5.0)
|
|
515
516
|
ostruct (0.6.3)
|
|
516
|
-
pagy (43.6.
|
|
517
|
+
pagy (43.6.3)
|
|
517
518
|
json
|
|
518
519
|
uri
|
|
519
520
|
yaml
|
|
520
|
-
parallel (2.
|
|
521
|
-
parallel_tests (5.
|
|
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)
|
|
@@ -595,7 +596,7 @@ GEM
|
|
|
595
596
|
zeitwerk (~> 2.6)
|
|
596
597
|
rainbow (3.1.1)
|
|
597
598
|
rake (13.4.2)
|
|
598
|
-
ransack (
|
|
599
|
+
ransack (5.0.2)
|
|
599
600
|
activerecord (>= 7.2)
|
|
600
601
|
activesupport (>= 7.2)
|
|
601
602
|
i18n
|
|
@@ -623,7 +624,7 @@ GEM
|
|
|
623
624
|
parser (~> 3.3.0)
|
|
624
625
|
rainbow (>= 2.0, < 4.0)
|
|
625
626
|
rexml (~> 3.1)
|
|
626
|
-
regexp_parser (2.
|
|
627
|
+
regexp_parser (2.13.0)
|
|
627
628
|
reline (0.7.0)
|
|
628
629
|
io-console (~> 0.5)
|
|
629
630
|
responders (3.2.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.
|
|
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.
|
|
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.
|
|
722
|
-
simplecov (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.
|
|
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.
|
|
793
|
-
unicode-emoji (~> 4.
|
|
794
|
-
unicode-emoji (4.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
48
|
+
<%= content_tag :div, class: "field-wrapper__input", **tooltip_attributes(tooltip) do %>
|
|
44
49
|
<%= content %>
|
|
45
|
-
|
|
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) %>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
aria-modal="true"
|
|
18
18
|
class="modal__card-container"
|
|
19
19
|
>
|
|
20
|
-
<div class="modal__card" data
|
|
20
|
+
<div class="modal__card" <%= tag.attributes(data: {"#{ctrl}-target": "card"}) %>>
|
|
21
21
|
<div class="card">
|
|
22
22
|
<div class="card__wrapper">
|
|
23
23
|
<% if has_header? %>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<% end %>
|
|
37
37
|
|
|
38
38
|
<div class="panel__content">
|
|
39
|
-
<div class="panel__body" <%= "
|
|
39
|
+
<div class="panel__body" <%= tag.attributes(content_focusable? ? {data: {"content-focus": ""}, tabindex: -1} : {}) %>>
|
|
40
40
|
<% if card? %>
|
|
41
41
|
<div class="panel__card"><%= card %></div>
|
|
42
42
|
<% else %>
|
|
@@ -412,6 +412,9 @@ module Avo
|
|
|
412
412
|
def checkbox_list_option(record)
|
|
413
413
|
resource = @attachment_resource.new(record:, view: Avo::ViewInquirer.new(:new), params:)
|
|
414
414
|
search_item = (@attachment_resource.fetch_search(:item, record:) || {}).with_indifferent_access
|
|
415
|
+
# Same split as the global search controller: `record_title` is the fallback, and a
|
|
416
|
+
# declared `self.search[:item]` supplies title, description and image itself. Those
|
|
417
|
+
# values are app-authored and outside what a title-attribute rule governs.
|
|
415
418
|
title = search_item[:title] || resource.record_title
|
|
416
419
|
|
|
417
420
|
{
|
|
@@ -446,9 +449,9 @@ module Avo
|
|
|
446
449
|
page_key = "#{pagination_key}.page"
|
|
447
450
|
|
|
448
451
|
@index_params[:page] = if Avo.configuration.session_persistence_enabled?
|
|
449
|
-
session[page_key] = params[:page] || session[page_key]
|
|
452
|
+
session[page_key] = positive_integer_or(params[:page] || session[page_key], 1)
|
|
450
453
|
else
|
|
451
|
-
params[:page]
|
|
454
|
+
positive_integer_or(params[:page], 1)
|
|
452
455
|
end
|
|
453
456
|
end
|
|
454
457
|
|
|
@@ -457,9 +460,9 @@ module Avo
|
|
|
457
460
|
per_page_key = "#{pagination_key}.per_page"
|
|
458
461
|
|
|
459
462
|
@index_params[:per_page] = if Avo.configuration.session_persistence_enabled?
|
|
460
|
-
session[per_page_key] = params[:per_page] || session[per_page_key]
|
|
463
|
+
session[per_page_key] = positive_integer_or(params[:per_page] || session[per_page_key], Avo.configuration.via_per_page)
|
|
461
464
|
else
|
|
462
|
-
params[:per_page]
|
|
465
|
+
positive_integer_or(params[:per_page], Avo.configuration.via_per_page)
|
|
463
466
|
end
|
|
464
467
|
end
|
|
465
468
|
end
|
|
@@ -706,12 +706,17 @@ module Avo
|
|
|
706
706
|
end
|
|
707
707
|
|
|
708
708
|
def set_pagination_params
|
|
709
|
-
@index_params[:page] = params[:page]
|
|
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] =
|
|
713
|
+
cookies[:per_page] = @index_params[:per_page] if params[:per_page].present?
|
|
714
|
+
end
|
|
713
715
|
|
|
714
|
-
|
|
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.
|
|
@@ -160,6 +160,10 @@ module Avo
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
def fetch_result_information(record, resource, item)
|
|
163
|
+
# `record_title` is the fallback here, not the source: a resource declaring
|
|
164
|
+
# `self.search[:item]` supplies the title itself, and that block is app-authored.
|
|
165
|
+
# Anything that governs which attributes a title may expose therefore reaches this
|
|
166
|
+
# surface through `record_title` only — the item half is the app's own.
|
|
163
167
|
title = item&.dig(:title) || resource.record_title
|
|
164
168
|
highlighted_title = highlight(title&.to_s, CGI.escapeHTML(params[:q] || ""))
|
|
165
169
|
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
<form method="dialog">
|
|
3
3
|
<div class="card relative">
|
|
4
4
|
<button value="cancel" class="absolute top-3 inset-e-2.5 text-content-secondary bg-transparent hover:bg-tertiary hover:text-content rounded-lg text-sm size-8 ms-auto inline-flex justify-center items-center" data-modal-hide="popup-modal" tabindex="3">
|
|
5
|
-
<%= svg "x
|
|
5
|
+
<%= svg "x", class: "size-6" %>
|
|
6
6
|
<span class="sr-only"><%= t "avo.close_modal" %></span>
|
|
7
7
|
</button>
|
|
8
8
|
<div class="card__wrapper">
|
|
9
9
|
<div class="card__body text-center items-center justify-between min-h-56">
|
|
10
10
|
<div class="flex flex-col items-center justify-center p-4 md:p-5">
|
|
11
11
|
<svg class="mt-4 mx-auto mb-4 text-content-secondary size-12" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
|
12
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
12
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
13
13
|
</svg>
|
|
14
14
|
<p class="text-md font-normal text-content">
|
|
15
15
|
<%= t "avo.are_you_sure" %>
|
|
@@ -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
|
-
<%=
|
|
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
|
-
<%=
|
|
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 %>
|
data/lib/avo/base_action.rb
CHANGED
|
@@ -186,8 +186,11 @@ module Avo
|
|
|
186
186
|
|
|
187
187
|
# For some fields, like belongs_to, the id and database_id differ (user vs user_id).
|
|
188
188
|
# That's why we need to fetch the database_id for when we process the action.
|
|
189
|
-
action_fields_by_database_id = action_fields.
|
|
190
|
-
|
|
189
|
+
action_fields_by_database_id = action_fields.values.flat_map do |field|
|
|
190
|
+
# A polymorphic belongs_to submits both `<name>_type` and `<name>_id`, but its database_id only covers the type.
|
|
191
|
+
database_ids = field.try(:is_polymorphic?) ? field.to_permitted_param : [field.database_id]
|
|
192
|
+
|
|
193
|
+
database_ids.map { |database_id| [database_id.to_sym, field] }
|
|
191
194
|
end.to_h
|
|
192
195
|
|
|
193
196
|
args[:fields].to_unsafe_h.map do |name, value|
|
|
@@ -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
|
data/lib/avo/current.rb
CHANGED
|
@@ -27,13 +27,31 @@ class Avo::Current < ActiveSupport::CurrentAttributes
|
|
|
27
27
|
|
|
28
28
|
attribute :appearance_settings
|
|
29
29
|
|
|
30
|
+
# Which Avo surface this request arrived through: `:ui` for the admin screens, `:api`,
|
|
31
|
+
# `:ai`, `:mcp`. It names the client, not the screen — `view` already means index/show/
|
|
32
|
+
# edit/new, which is a screen within one surface. Consumers that resolve an answer per
|
|
33
|
+
# user can differ per surface without a second declaration.
|
|
34
|
+
attribute :interface
|
|
35
|
+
|
|
36
|
+
# Set by the callers that map over every resource with no user in scope — this gem's
|
|
37
|
+
# telemetry, and avo-licensing's debug service. Consumers that restrict what a user may
|
|
38
|
+
# reach read this to opt out explicitly, so the exemption can never be inferred from a
|
|
39
|
+
# nil user (which is a legitimate state on the API surface).
|
|
40
|
+
attribute :enumerating_all_resources
|
|
41
|
+
|
|
30
42
|
# Rails 7.1 CurrentAttributes#attribute is only `def attribute(*names)` — no `default:` keyword.
|
|
31
43
|
# `attribute :x, default: {}` is passed as a second positional `{ default: {} }`, so `names.map(&:to_sym)` raises.
|
|
32
|
-
resets
|
|
44
|
+
resets do
|
|
45
|
+
self.appearance_settings = {}
|
|
46
|
+
self.interface = :ui
|
|
47
|
+
self.enumerating_all_resources = false
|
|
48
|
+
end
|
|
33
49
|
|
|
34
50
|
def initialize
|
|
35
51
|
super
|
|
36
52
|
self.appearance_settings = {}
|
|
53
|
+
self.interface = :ui
|
|
54
|
+
self.enumerating_all_resources = false
|
|
37
55
|
end
|
|
38
56
|
|
|
39
57
|
# Protect from error #<RuntimeError: Missing rack.input> when request is ActionDispatch::Request.empty
|
|
@@ -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
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
module Avo
|
|
4
4
|
module Fields
|
|
5
5
|
class LocationField < BaseField
|
|
6
|
+
# The two columns a location is stored across, when it is stored across two. Read
|
|
7
|
+
# by anything that has to know which columns back a field — `to_permitted_param`
|
|
8
|
+
# cannot answer that here, since it yields the field id and a hash rather than the
|
|
9
|
+
# column names.
|
|
10
|
+
attr_reader :stored_as
|
|
11
|
+
|
|
6
12
|
def initialize(id, **args, &block)
|
|
7
13
|
hide_on :index
|
|
8
14
|
super
|
|
@@ -113,6 +119,10 @@ module Avo
|
|
|
113
119
|
record.send(:"#{database_id}=", value[index])
|
|
114
120
|
end
|
|
115
121
|
end
|
|
122
|
+
|
|
123
|
+
def tooltip_anchor
|
|
124
|
+
:block
|
|
125
|
+
end
|
|
116
126
|
end
|
|
117
127
|
end
|
|
118
128
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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|
|
data/lib/avo/resources/base.rb
CHANGED
|
@@ -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
|
|
@@ -513,21 +516,52 @@ module Avo
|
|
|
513
516
|
fetch_record_title.to_s
|
|
514
517
|
end
|
|
515
518
|
|
|
519
|
+
# Whether this resource may expose `attribute` inside a record title. Core always
|
|
520
|
+
# answers true; a plugin that restricts what a user may reach overrides this and
|
|
521
|
+
# answers from its own rules. That is what keeps a restricted attribute out of
|
|
522
|
+
# global search results, breadcrumbs, record links and the association picker
|
|
523
|
+
# without teaching each of those surfaces separately — they all render a title.
|
|
524
|
+
#
|
|
525
|
+
# Deliberately a question about one attribute rather than a field lookup: a bare
|
|
526
|
+
# `Resource.new(record:)` is built once per option in a belongs_to lookup list, and
|
|
527
|
+
# its items holder is empty until `detect_fields` runs, so resolving a title through
|
|
528
|
+
# the field pipeline would both cost a detection per option and answer nothing on
|
|
529
|
+
# the resources that never detect.
|
|
530
|
+
def title_attribute_reachable?(attribute)
|
|
531
|
+
true
|
|
532
|
+
end
|
|
533
|
+
|
|
516
534
|
def fetch_record_title
|
|
517
535
|
return name if @record.nil?
|
|
518
536
|
|
|
519
537
|
# Get the title from the record if title is not set, try to get the name, title or label, or fallback to the to_param
|
|
520
|
-
return
|
|
538
|
+
return reachable_title_fallback || @record.to_param if title.nil?
|
|
521
539
|
|
|
522
540
|
# If the title is a symbol, get the value from the record else execute the block/string
|
|
523
541
|
case title
|
|
524
542
|
when Symbol
|
|
525
|
-
@record.send
|
|
543
|
+
title_attribute_reachable?(title) ? @record.send(title) : @record.to_param
|
|
526
544
|
when Proc
|
|
545
|
+
# App-authored code. The app's own code is outside this boundary, so the
|
|
546
|
+
# reachability question is not asked of it.
|
|
527
547
|
Avo::ExecutionContext.new(target: title, resource: self, record: @record).handle
|
|
528
548
|
end
|
|
529
549
|
end
|
|
530
550
|
|
|
551
|
+
# Preserves the original `||` chain exactly — an unreachable candidate is skipped
|
|
552
|
+
# the same way a blank one is.
|
|
553
|
+
def reachable_title_fallback
|
|
554
|
+
reachable_title_attribute(:name) ||
|
|
555
|
+
reachable_title_attribute(:title) ||
|
|
556
|
+
reachable_title_attribute(:label)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def reachable_title_attribute(attribute)
|
|
560
|
+
return unless title_attribute_reachable?(attribute)
|
|
561
|
+
|
|
562
|
+
@record.try(attribute)
|
|
563
|
+
end
|
|
564
|
+
|
|
531
565
|
def record_icon
|
|
532
566
|
fetch_record_icon.to_s
|
|
533
567
|
end
|
|
@@ -24,7 +24,10 @@ class Avo::Services::TelemetryService
|
|
|
24
24
|
def avo_metadata
|
|
25
25
|
resources = Avo.resource_manager.all
|
|
26
26
|
dashboards = Avo::Current.app.dashboard_manager.all
|
|
27
|
-
|
|
27
|
+
# Counting fields across every resource with no user in scope. Declared explicitly so a
|
|
28
|
+
# consumer that restricts fields per user opts out here rather than inferring it from a
|
|
29
|
+
# nil user — nil is a legitimate user on the API surface, where restriction must still apply.
|
|
30
|
+
field_definitions = enumerating_all_resources { resources.map(&:get_field_definitions) }
|
|
28
31
|
fields_count = field_definitions.map(&:count).sum
|
|
29
32
|
fields_per_resource = sprintf("%0.01f", fields_count / (resources.count + 0.0))
|
|
30
33
|
|
|
@@ -69,6 +72,17 @@ class Avo::Services::TelemetryService
|
|
|
69
72
|
"error: #{error.message}"
|
|
70
73
|
end
|
|
71
74
|
|
|
75
|
+
# Marks a block as enumerating every resource without a user, restoring whatever the
|
|
76
|
+
# surrounding request had set rather than assuming it was unset.
|
|
77
|
+
def enumerating_all_resources
|
|
78
|
+
previous = Avo::Current.enumerating_all_resources
|
|
79
|
+
Avo::Current.enumerating_all_resources = true
|
|
80
|
+
|
|
81
|
+
yield
|
|
82
|
+
ensure
|
|
83
|
+
Avo::Current.enumerating_all_resources = previous
|
|
84
|
+
end
|
|
85
|
+
|
|
72
86
|
def other_metadata(type = :actions)
|
|
73
87
|
resources = Avo.resource_manager.all
|
|
74
88
|
|
|
@@ -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
|
|
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": "
|
|
5
|
-
"date": "2026-09-
|
|
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
|
@@ -40,24 +40,26 @@ module Avo
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
+
# Names, not constants: plugins register view types from Avo.boot, and loading a component there loads
|
|
44
|
+
# Action View while the app initializes, which Rails 8.2 flags (rails/rails#57224).
|
|
43
45
|
def register_defaults
|
|
44
46
|
register :table,
|
|
45
|
-
component: Avo::ViewTypes::TableComponent,
|
|
47
|
+
component: "Avo::ViewTypes::TableComponent",
|
|
46
48
|
icon: "tabler/outline/layout-list",
|
|
47
49
|
active_icon: "tabler/filled/layout-list"
|
|
48
50
|
|
|
49
51
|
register :grid,
|
|
50
|
-
component: Avo::ViewTypes::GridComponent,
|
|
52
|
+
component: "Avo::ViewTypes::GridComponent",
|
|
51
53
|
icon: "tabler/outline/layout-grid",
|
|
52
54
|
active_icon: "tabler/filled/layout-grid"
|
|
53
55
|
|
|
54
56
|
register :map,
|
|
55
|
-
component: Avo::ViewTypes::MapComponent,
|
|
57
|
+
component: "Avo::ViewTypes::MapComponent",
|
|
56
58
|
icon: "tabler/outline/compass",
|
|
57
59
|
active_icon: "tabler/filled/compass"
|
|
58
60
|
|
|
59
61
|
register :list,
|
|
60
|
-
component: Avo::ViewTypes::TableComponent,
|
|
62
|
+
component: "Avo::ViewTypes::TableComponent",
|
|
61
63
|
icon: "tabler/outline/layout-list",
|
|
62
64
|
active_icon: "tabler/filled/layout-list"
|
|
63
65
|
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.
|
|
4
|
+
version: 4.2.7
|
|
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
|