avo 4.0.13 → 4.0.15

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.
@@ -16,7 +16,7 @@
16
16
  outline: none;
17
17
  }
18
18
 
19
- a[href]:empty:not([aria-label]) {
19
+ a[href]:empty:not([aria-label]):not([aria-labelledby]) {
20
20
  display: none;
21
21
  }
22
22
 
@@ -8,7 +8,7 @@ class Avo::Fields::IndexComponent < Avo::BaseComponent
8
8
  attr_reader :parent_resource
9
9
  attr_reader :view
10
10
 
11
- def initialize(field: nil, resource: nil, reflection: nil, index: 0, parent_record: nil, parent_resource: nil, **kwargs)
11
+ def initialize(field: nil, resource: nil, reflection: nil, index: 0, parent_record: nil, parent_resource: nil, density: nil, field_wrapper: {}, **kwargs)
12
12
  @field = field
13
13
  @resource = resource
14
14
  @index = index
@@ -17,6 +17,8 @@ class Avo::Fields::IndexComponent < Avo::BaseComponent
17
17
  @parent_resource = parent_resource
18
18
  @view = Avo::ViewInquirer.new("index")
19
19
  @reflection = reflection
20
+ @density = density
21
+ @field_wrapper_options = field_wrapper
20
22
  end
21
23
 
22
24
  def resource_view_path
@@ -35,7 +37,9 @@ class Avo::Fields::IndexComponent < Avo::BaseComponent
35
37
  def field_wrapper_args
36
38
  {
37
39
  field: @field,
38
- resource: @resource
40
+ resource: @resource,
41
+ density: @density,
42
+ **@field_wrapper_options
39
43
  }
40
44
  end
41
45
  end
@@ -1,10 +1,10 @@
1
- <%= content_tag :td,
1
+ <%= content_tag @html_tag,
2
2
  class: class_names(
3
3
  "group/clipboard px-2 leading-none whitespace-nowrap h-full text-content text-sm",
4
4
  @classes,
5
5
  @field.get_html(:classes, view: @view, element: :wrapper),
6
6
  {
7
- "py-4": !@flush
7
+ density_padding_class => !@flush
8
8
  }
9
9
  ),
10
10
  data: {
@@ -1,11 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::Index::FieldWrapperComponent < Avo::BaseComponent
4
+ # Vertical padding for each density step. `nil` density falls back to
5
+ # `Avo.configuration.density`.
6
+ DENSITY_PADDING = {
7
+ tight: "py-2",
8
+ normal: "py-4",
9
+ relaxed: "py-6"
10
+ }.freeze
11
+
4
12
  prop :field
5
13
  prop :resource
6
14
  prop :dash_if_blank, default: true
7
15
  prop :center_content, default: false
8
16
  prop :flush, default: false
17
+ prop :density, default: nil
18
+ # The wrapper element. Defaults to a table cell; non-table hosts (e.g. the
19
+ # dashboards list card) render the same field components inside a :div.
20
+ prop :html_tag, default: :td
9
21
  prop :args, kind: :**, default: {}.freeze
10
22
 
11
23
  def after_initialize
@@ -31,4 +43,8 @@ class Avo::Index::FieldWrapperComponent < Avo::BaseComponent
31
43
  def render_dash?
32
44
  @field.value.blank? && @dash_if_blank
33
45
  end
46
+
47
+ def density_padding_class
48
+ DENSITY_PADDING[(@density || Avo.configuration.density)&.to_sym] || DENSITY_PADDING[:normal]
49
+ end
34
50
  end
@@ -1 +1 @@
1
- <%= content_tag :span, @count, class: class_names("count", @classes), data: @data %>
1
+ <%= content_tag :span, @count, class: class_names("count", @classes), data: @data, "aria-label": @label %>
@@ -8,4 +8,7 @@ class Avo::UI::CountComponent < Avo::BaseComponent
8
8
  prop :count
9
9
  prop :classes, default: nil
10
10
  prop :data, default: -> { {} }
11
+ # Optional accessible name (e.g. "1,234 records") when the visible +count+ is
12
+ # abbreviated or purely numeric. Rendered as aria-label; omitted when nil.
13
+ prop :label, default: nil
11
14
  end
@@ -22,5 +22,6 @@
22
22
  ) do %>
23
23
  <%= svg @icon, class: "tabs__item-icon", "aria-hidden": "true" if @icon.present? %>
24
24
  <%= tag.span @label, class: "tabs__item-label" if @label.present? %>
25
+ <%= render_badge %>
25
26
  <% end %>
26
27
  <% end %>
@@ -19,6 +19,17 @@ module Avo
19
19
  prop :data, default: {}.freeze
20
20
  prop :classes
21
21
  prop :title
22
+ prop :badge, default: nil
23
+
24
+ private
25
+
26
+ # The badge may be a component instance (eager count pill) or a
27
+ # pre-rendered safe string (deferred counter frame); render accordingly.
28
+ def render_badge
29
+ return unless @badge.present?
30
+
31
+ @badge.respond_to?(:render_in) ? render(@badge) : @badge
32
+ end
22
33
  end
23
34
  end
24
35
  end
@@ -17,8 +17,8 @@ export default class extends Controller {
17
17
  anchor = null
18
18
 
19
19
  mouseEntered(event) {
20
- const row = event.target.closest('tr')
21
- const url = row.dataset.visitPath
20
+ const row = event.target.closest('[data-visit-path]')
21
+ const url = row?.dataset?.visitPath
22
22
 
23
23
  if (url) {
24
24
  this.anchor = this.createAnchor(url)
@@ -64,14 +64,14 @@ export default class extends Controller {
64
64
  return // Don't navigate if a link or button is clicked
65
65
  }
66
66
 
67
- const row = event.target.closest('tr')
68
- const url = row.dataset.visitPath
67
+ const row = event.target.closest('[data-visit-path]')
68
+ const url = row?.dataset?.visitPath
69
69
 
70
70
  if (!row || !url) {
71
71
  return
72
72
  }
73
73
 
74
- if (event.metaKey || event.ctrlKey) {
74
+ if (event.metaKey || event.ctrlKey || row.dataset.visitTarget === '_blank') {
75
75
  this.#visitInNewTab(url)
76
76
  } else {
77
77
  this.#visitInSameTab(url)
@@ -51,6 +51,7 @@ module Avo
51
51
  attr_accessor :resource_parent_controller
52
52
  attr_accessor :default_url_options
53
53
  attr_accessor :click_row_to_view_record
54
+ attr_accessor :density
54
55
  attr_accessor :alert_dismiss_time
55
56
  attr_accessor :is_admin_method
56
57
  attr_accessor :is_developer_method
@@ -182,6 +183,7 @@ module Avo
182
183
  @default_url_options = []
183
184
  @pagination = {}
184
185
  @click_row_to_view_record = true
186
+ @density = :normal
185
187
  @alert_dismiss_time = 5000
186
188
  @is_admin_method = :is_admin?
187
189
  @is_developer_method = :is_developer?
@@ -1,18 +1,47 @@
1
1
  module Avo
2
2
  class PluginManager
3
3
  attr_reader :plugins
4
- attr_accessor :engines
4
+ attr_reader :engines
5
5
 
6
6
  alias_method :all, :plugins
7
7
 
8
8
  def initialize
9
9
  @plugins = []
10
10
  @engines = []
11
+ @building_plugins = []
12
+ @building_engines = []
11
13
  end
12
14
 
13
- def reset
14
- @plugins = []
15
- @engines = []
15
+ # Starts a re-registration cycle without touching the currently published
16
+ # @plugins/@engines, so concurrent readers (e.g. mount_avo's route-drawing
17
+ # loop) keep seeing the complete pre-reload list until #commit_reload
18
+ # publishes the new one. Called by Avo.boot inside its Mutex, so only one
19
+ # reload is ever building at a time.
20
+ def begin_reload
21
+ @building_plugins = []
22
+ @building_engines = []
23
+ end
24
+
25
+ # Publishes the registrations collected since #begin_reload. Each of the
26
+ # two reassignments below is individually an atomic pointer swap, so a
27
+ # reader of .engines alone (or .plugins alone) always sees the complete
28
+ # old list or the complete new one, never a partially rebuilt one. The
29
+ # two fields are not published jointly -- a reader combining both in one
30
+ # operation could observe them from different reload generations. No
31
+ # current reader does that (mount_avo reads only .engines; installed?
32
+ # reads only .plugins).
33
+ def commit_reload
34
+ @plugins = @building_plugins
35
+ @engines = @building_engines
36
+ # Reset to fresh arrays rather than nil: register/mount_engine must
37
+ # stay callable even outside a begin_reload/commit_reload window --
38
+ # e.g. avo-permissions calls Avo.plugin_manager.register at Rails
39
+ # initializer time, before Avo.boot ever runs. Such a call is simply
40
+ # discarded by the next #begin_reload rather than raising, matching
41
+ # the pre-atomic-publish behavior where register/mount_engine never
42
+ # crashed regardless of timing.
43
+ @building_plugins = []
44
+ @building_engines = []
16
45
  end
17
46
 
18
47
  def register(name, priority: 10)
@@ -21,7 +50,7 @@ module Avo
21
50
  # (e.g. `:rhino` for `avo-rhino_field`), so the name alone isn't enough.
22
51
  registered_from = caller_locations(1, 1)&.first&.path
23
52
 
24
- @plugins << Plugin.new(name:, priority: priority, registered_from: registered_from)
53
+ @building_plugins << Plugin.new(name:, priority: priority, registered_from: registered_from)
25
54
  end
26
55
 
27
56
  def register_view_type(name, component:, icon:, active_icon:, translation_key: nil)
@@ -80,7 +109,11 @@ module Avo
80
109
  end
81
110
 
82
111
  def mount_engine(klass, **options)
83
- @engines << {klass:, options:}
112
+ # Dedup by class so a plugin hook that (by bug) calls mount_engine
113
+ # twice for the same engine within one boot can't leave a duplicate
114
+ # entry for #commit_reload to publish.
115
+ @building_engines.delete_if { |engine| engine[:klass] == klass }
116
+ @building_engines << {klass:, options:}
84
117
  end
85
118
  end
86
119
 
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.0.13" unless const_defined?(:VERSION)
2
+ VERSION = "4.0.15" unless const_defined?(:VERSION)
3
3
  end
data/lib/avo.rb CHANGED
@@ -55,6 +55,11 @@ module Avo
55
55
 
56
56
  class ViewTypeComponentNotFoundError < StandardError; end
57
57
 
58
+ # Serializes Avo.boot so concurrent code reloads (config.to_prepare fires on
59
+ # every reload, from every request thread) can't interleave the plugin
60
+ # registry reset with its repopulation. See PluginManager#begin_reload.
61
+ @boot_mutex = Mutex.new
62
+
58
63
  # Exception raised when a resource is missing
59
64
  class MissingResourceError < StandardError
60
65
  def initialize(model_class, field)
@@ -92,16 +97,19 @@ module Avo
92
97
 
93
98
  # Runs when the app boots up
94
99
  def boot
95
- Turbo::Streams::TagBuilder.prepend(Avo::TurboStreamActionsHelper)
96
- @logger = Avo.configuration.logger
97
- @field_manager = Avo::Fields::FieldManager.build
98
- @view_type_manager = nil # force re-init with defaults on next access
99
- @cache_store = Avo.configuration.cache_store
100
- Avo.plugin_manager.reset
101
- # Run load hooks for plugins to include them in the app.
102
- # This is useful for plugins that need to include modules in the app that will be used on avo_boot hook.
103
- ActiveSupport.run_load_hooks(:avo_plugin_include, self)
104
- ActiveSupport.run_load_hooks(:avo_boot, self)
100
+ @boot_mutex.synchronize do
101
+ Turbo::Streams::TagBuilder.prepend(Avo::TurboStreamActionsHelper)
102
+ @logger = Avo.configuration.logger
103
+ @field_manager = Avo::Fields::FieldManager.build
104
+ @view_type_manager = nil # force re-init with defaults on next access
105
+ @cache_store = Avo.configuration.cache_store
106
+ Avo.plugin_manager.begin_reload
107
+ # Run load hooks for plugins to include them in the app.
108
+ # This is useful for plugins that need to include modules in the app that will be used on avo_boot hook.
109
+ ActiveSupport.run_load_hooks(:avo_plugin_include, self)
110
+ ActiveSupport.run_load_hooks(:avo_boot, self)
111
+ Avo.plugin_manager.commit_reload
112
+ end
105
113
  eager_load_actions
106
114
  end
107
115
 
@@ -118,6 +118,7 @@ Avo.configure do |config|
118
118
 
119
119
  ## == Customization ==
120
120
  config.click_row_to_view_record = true
121
+ # config.density = :normal # :tight, :normal, :relaxed
121
122
  # config.app_name = 'Avocadelicious'
122
123
  # config.timezone = 'UTC'
123
124
  # config.currency = 'USD'
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avo
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.13
4
+ version: 4.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin
8
8
  - Mihai Marin
9
9
  - Paul Bob
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2026-07-16 00:00:00.000000000 Z
12
+ date: 1980-01-02 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: activerecord
@@ -1199,8 +1198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1199
1198
  - !ruby/object:Gem::Version
1200
1199
  version: '0'
1201
1200
  requirements: []
1202
- rubygems_version: 3.5.22
1203
- signing_key:
1201
+ rubygems_version: 4.0.10
1204
1202
  specification_version: 4
1205
1203
  summary: Admin panel framework and Content Management System for Ruby on Rails.
1206
1204
  test_files: []