avo 4.2.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a64a8878e0c42cef0fcbe360cdf217635be5cdcca396cf180888e24a034ab8d
4
- data.tar.gz: f05b38be082e6fbe6cc9c5f5c85c87452108d8539dd284d320bd3bd09c9fb92d
3
+ metadata.gz: 06a99742dcd8d96855fb10577085da6acf2fb2eabebc8ba66d02b653e404f962
4
+ data.tar.gz: 7d63ef33110cdf91bb3188ecf82abac3ecf9f56fd3c1edc53605737da96b2557
5
5
  SHA512:
6
- metadata.gz: 19222c1d66ac795e3e0fa668ca5a903604dbe3ae412e2aa528bb8afe6e753cf79abfdd3f3d84f0cfaaecca6e4847ce56f692a14a7abbec498e524c5bbe63009a
7
- data.tar.gz: 294e34314e882e5cc255f34e9060f683e384fa888795083275566546d305955d7d090e7293471e1588ea4c0f06a9afb8d4b3085c31e611b98c2ee16bad037d2c
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.6)
4
+ avo (4.2.7)
5
5
  actionview (>= 6.1)
6
6
  active_link_to
7
7
  activerecord (>= 6.1)
@@ -139,7 +139,7 @@ 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.1287.0)
142
+ aws-partitions (1.1289.0)
143
143
  aws-sdk-core (3.257.0)
144
144
  aws-eventstream (~> 1, >= 1.3.0)
145
145
  aws-partitions (~> 1, >= 1.992.0)
@@ -432,7 +432,7 @@ GEM
432
432
  addressable (~> 2.8)
433
433
  childprocess (~> 5.0)
434
434
  logger (~> 1.6)
435
- lexxy (0.9.32)
435
+ lexxy (0.9.33)
436
436
  railties (>= 8.0.2)
437
437
  lint_roller (1.1.0)
438
438
  listen (3.10.0)
@@ -514,7 +514,7 @@ GEM
514
514
  openssl (4.0.2)
515
515
  orm_adapter (0.5.0)
516
516
  ostruct (0.6.3)
517
- pagy (43.6.2)
517
+ pagy (43.6.3)
518
518
  json
519
519
  uri
520
520
  yaml
@@ -596,7 +596,7 @@ GEM
596
596
  zeitwerk (~> 2.6)
597
597
  rainbow (3.1.1)
598
598
  rake (13.4.2)
599
- ransack (4.4.1)
599
+ ransack (5.0.2)
600
600
  activerecord (>= 7.2)
601
601
  activesupport (>= 7.2)
602
602
  i18n
@@ -624,7 +624,7 @@ GEM
624
624
  parser (~> 3.3.0)
625
625
  rainbow (>= 2.0, < 4.0)
626
626
  rexml (~> 3.1)
627
- regexp_parser (2.12.0)
627
+ regexp_parser (2.13.0)
628
628
  reline (0.7.0)
629
629
  io-console (~> 0.5)
630
630
  responders (3.2.1)
@@ -17,7 +17,7 @@
17
17
  aria-modal="true"
18
18
  class="modal__card-container"
19
19
  >
20
- <div class="modal__card" data-<%= ctrl %>-target="card">
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" <%= "data-content-focus tabindex=-1".html_safe if content_focusable? %>>
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
  {
@@ -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-mark", class: "size-6" %>
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" %>
@@ -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.map do |id, value|
190
- [value.database_id.to_sym, value]
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|
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 { self.appearance_settings = {} }
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
@@ -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
@@ -516,21 +516,52 @@ module Avo
516
516
  fetch_record_title.to_s
517
517
  end
518
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
+
519
534
  def fetch_record_title
520
535
  return name if @record.nil?
521
536
 
522
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
523
- return @record.try(:name) || @record.try(:title) || @record.try(:label) || @record.to_param if title.nil?
538
+ return reachable_title_fallback || @record.to_param if title.nil?
524
539
 
525
540
  # If the title is a symbol, get the value from the record else execute the block/string
526
541
  case title
527
542
  when Symbol
528
- @record.send title
543
+ title_attribute_reachable?(title) ? @record.send(title) : @record.to_param
529
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.
530
547
  Avo::ExecutionContext.new(target: title, resource: self, record: @record).handle
531
548
  end
532
549
  end
533
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
+
534
565
  def record_icon
535
566
  fetch_record_icon.to_s
536
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
- field_definitions = resources.map(&:get_field_definitions)
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
 
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.2.6" unless const_defined?(:VERSION)
2
+ VERSION = "4.2.7" unless const_defined?(:VERSION)
3
3
  end
@@ -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.6
4
+ version: 4.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin