avo 4.0.25 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -1
  3. data/app/assets/builds/avo/application.css +9 -21
  4. data/app/assets/builds/avo/application.js +71 -71
  5. data/app/assets/builds/avo/application.js.map +3 -3
  6. data/app/assets/stylesheets/css/components/modal.css +7 -1
  7. data/app/assets/stylesheets/css/components/ui/tabs.css +10 -0
  8. data/app/components/avo/actions_component.rb +12 -4
  9. data/app/components/avo/modal_component.html.erb +0 -1
  10. data/app/components/avo/modal_component.rb +1 -1
  11. data/app/components/avo/resource_component.rb +3 -0
  12. data/app/components/avo/row_selector_component.rb +3 -3
  13. data/app/controllers/avo/actions_controller.rb +1 -3
  14. data/app/controllers/avo/appearance_settings_controller.rb +1 -1
  15. data/app/controllers/avo/array_controller.rb +1 -1
  16. data/app/controllers/avo/associations_controller.rb +1 -3
  17. data/app/controllers/avo/attachments_controller.rb +1 -3
  18. data/app/controllers/avo/base_controller.rb +1 -3
  19. data/app/controllers/avo/charts_controller.rb +1 -3
  20. data/app/controllers/avo/debug_controller.rb +1 -3
  21. data/app/controllers/avo/home_controller.rb +1 -3
  22. data/app/controllers/avo/media_library_controller.rb +1 -1
  23. data/app/controllers/avo/private_controller.rb +1 -3
  24. data/app/controllers/avo/resources_controller.rb +1 -3
  25. data/app/controllers/avo/search_controller.rb +1 -3
  26. data/app/javascript/js/controllers/modal_size_controller.js +9 -49
  27. data/app/javascript/js/controllers/record_selector_controller.js +25 -8
  28. data/app/views/avo/media_library/index.html.erb +4 -1
  29. data/avo.gemspec +1 -1
  30. data/lib/avo/resources/controls/base_control.rb +13 -0
  31. data/lib/avo/version.rb +1 -1
  32. metadata +2 -2
@@ -170,7 +170,7 @@
170
170
  }
171
171
 
172
172
  .modal__body-content {
173
- @apply w-full min-h-48 rounded-card-wrapper border border-tertiary bg-primary;
173
+ @apply w-full min-h-48 rounded-card-wrapper border border-tertiary bg-primary p-4;
174
174
  }
175
175
 
176
176
  /* Belongs-to "Create new" embeds an edit form in the modal. Drop the nested
@@ -233,6 +233,12 @@
233
233
  }
234
234
  }
235
235
 
236
+ .modal--width-5xl {
237
+ .modal__card {
238
+ @apply w-5xl;
239
+ }
240
+ }
241
+
236
242
  .modal--width-25 {
237
243
  .modal__card {
238
244
  @apply w-[25dvw];
@@ -184,6 +184,16 @@
184
184
  }
185
185
  }
186
186
 
187
+ /* The breakout above assumes the container has padding to give up. The
188
+ belongs-to "Create new" modal has exactly as much as the breakout consumes,
189
+ so the dashed border ends up flush against the modal edge while the sibling
190
+ panels stay inset. Keep the group in flow there and let its own padding
191
+ hold the content off the border. AVO-1365. */
192
+ .modal-form-body .tab-group {
193
+ width: 100%;
194
+ margin-inline: 0;
195
+ }
196
+
187
197
  /* ============================================
188
198
  ACTIVE INDICATOR VIEW TRANSITION
189
199
  ============================================ */
@@ -78,10 +78,18 @@ class Avo::ActionsComponent < Avo::BaseComponent
78
78
  when defined?(Avo::CustomControls::Resources::Controls::Action) && Avo::CustomControls::Resources::Controls::Action
79
79
  render_action_link(action.action, icon: action.icon)
80
80
  when defined?(Avo::CustomControls::Resources::Controls::LinkTo) && Avo::CustomControls::Resources::Controls::LinkTo
81
- link_to action.args[:path],
82
- class: action.args.delete(:class),
83
- **action.args.except(:path, :label, :icon) do
84
- raw("#{icon(action.args[:icon])} #{action.args[:label]}")
81
+ # Dropdown items render as plain links, not ButtonComponents, so the styling
82
+ # options have nothing to land in — spreading the raw kwargs leaked them into
83
+ # the markup (color: :fuchsia rendered as color="fuchsia"). Excluding
84
+ # CONTROL_OPTIONS drops those and the internal plumbing while still forwarding
85
+ # genuine HTML attributes like rel:.
86
+ link_to action.path,
87
+ **action.args.except(*Avo::Resources::Controls::BaseControl::CONTROL_OPTIONS),
88
+ class: action.classes,
89
+ title: action.title,
90
+ target: action.target,
91
+ data: action.data do
92
+ raw("#{icon(action.icon)} #{action.label}")
85
93
  end
86
94
  end
87
95
  end
@@ -16,7 +16,6 @@
16
16
  role="dialog"
17
17
  aria-modal="true"
18
18
  class="modal__card-container"
19
- data-modal-size-target="container"
20
19
  >
21
20
  <div class="modal__card" data-<%= ctrl %>-target="card">
22
21
  <div class="card">
@@ -4,7 +4,7 @@ class Avo::ModalComponent < Avo::BaseComponent
4
4
  renders_one :heading
5
5
  renders_one :controls
6
6
 
7
- prop :width, default: :xl # :sm, :md, :lg, :xl, :2xl, :3xl, :4xl, :full
7
+ prop :width, default: :xl # :sm, :md, :lg, :xl, :2xl, :3xl, :4xl, :5xl, :full
8
8
  prop :height, default: :auto # :auto, :sm, :md, :lg, :xl, :2xl, :3xl, :4xl, :full
9
9
  prop :body_class
10
10
  prop :close_modal_on_backdrop_click, default: true, reader: :public
@@ -305,7 +305,10 @@ class Avo::ResourceComponent < Avo::BaseComponent
305
305
  end
306
306
 
307
307
  def render_link_to(link)
308
+ # Anything outside CONTROL_OPTIONS is a plain HTML attribute (rel:, id:…) and
309
+ # rides ButtonComponent's `prop :args, kind: :**` wildcard onto the <a> tag.
308
310
  a_link link.path,
311
+ **link.args.except(*Avo::Resources::Controls::BaseControl::CONTROL_OPTIONS),
309
312
  color: link.color,
310
313
  style: link.style,
311
314
  icon: link.icon,
@@ -5,12 +5,12 @@ class Avo::RowSelectorComponent < Avo::BaseComponent
5
5
  prop :index
6
6
  prop :checked, default: false
7
7
 
8
+ # Shift-select is wired on table rows only: grid items render this component without an index and
9
+ # have no table row for the controller to walk up to.
8
10
  def data_action
9
11
  data = "input->item-selector#toggle input->item-select-all#selectRow"
10
12
 
11
- if Avo.plugin_manager.installed?("avo-pro")
12
- data += " click->record-selector#toggleMultiple"
13
- end
13
+ data += " click->record-selector#toggleMultiple" if @index.present?
14
14
 
15
15
  data
16
16
  end
@@ -1,7 +1,5 @@
1
- require_dependency "avo/application_controller"
2
-
3
1
  module Avo
4
- class ActionsController < ApplicationController
2
+ class ActionsController < Avo::ApplicationController
5
3
  before_action :set_resource_name, :set_resource
6
4
  before_action :set_query, :set_record, :set_action, :verify_authorization, only: [:show, :handle]
7
5
  before_action :set_fields, only: :handle
@@ -1,5 +1,5 @@
1
1
  module Avo
2
- class AppearanceSettingsController < ApplicationController
2
+ class AppearanceSettingsController < Avo::ApplicationController
3
3
  def update
4
4
  appearance = Avo.configuration.appearance
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Avo
2
- class ArrayController < BaseController
2
+ class ArrayController < Avo::BaseController
3
3
  def set_query
4
4
  @query ||= @resource.fetch_records
5
5
  end
@@ -1,7 +1,5 @@
1
- require_dependency "avo/base_controller"
2
-
3
1
  module Avo
4
- class AssociationsController < BaseController
2
+ class AssociationsController < Avo::BaseController
5
3
  before_action :set_record, only: [:show, :index, :new, :create, :destroy]
6
4
  before_action :set_related_resource_name
7
5
  before_action :set_related_resource, only: [:show, :index, :new, :create, :destroy]
@@ -1,7 +1,5 @@
1
- require_dependency "avo/application_controller"
2
-
3
1
  module Avo
4
- class AttachmentsController < ApplicationController
2
+ class AttachmentsController < Avo::ApplicationController
5
3
  before_action :set_resource_name, only: [:destroy, :create]
6
4
  before_action :set_resource, only: [:destroy, :create]
7
5
  before_action :set_record, only: [:destroy, :create]
@@ -1,7 +1,5 @@
1
- require_dependency "avo/application_controller"
2
-
3
1
  module Avo
4
- class BaseController < ApplicationController
2
+ class BaseController < Avo::ApplicationController
5
3
  include Avo::Concerns::FiltersSessionHandler
6
4
  include Avo::Concerns::AssociationQueryScope
7
5
  include Avo::Concerns::SafeCall
@@ -1,7 +1,5 @@
1
- require_dependency "avo/base_controller"
2
-
3
1
  module Avo
4
- class ChartsController < BaseController
2
+ class ChartsController < Avo::BaseController
5
3
  # Summarizable charts reuse the index query pipeline (including policy scope via
6
4
  # `query_scope`). They were never meant to require `distribution_chart?` /
7
5
  # `distribution_chart_full?` policy methods — those only surfaced once
@@ -1,7 +1,5 @@
1
- require_dependency "avo/application_controller"
2
-
3
1
  module Avo
4
- class DebugController < ApplicationController
2
+ class DebugController < Avo::ApplicationController
5
3
  before_action :authenticate_developer_or_admin!
6
4
 
7
5
  def status
@@ -1,7 +1,5 @@
1
- require_dependency "avo/application_controller"
2
-
3
1
  module Avo
4
- class HomeController < ApplicationController
2
+ class HomeController < Avo::ApplicationController
5
3
  def index
6
4
  if Avo.configuration.home_path.present?
7
5
  # If the home_path is a block run it, if not, just use it
@@ -1,5 +1,5 @@
1
1
  module Avo
2
- class MediaLibraryController < ApplicationController
2
+ class MediaLibraryController < Avo::ApplicationController
3
3
  include Pagy::Method
4
4
 
5
5
  before_action :authorize_access!
@@ -1,7 +1,5 @@
1
- require_dependency "avo/application_controller"
2
-
3
1
  module Avo
4
- class PrivateController < ApplicationController
2
+ class PrivateController < Avo::ApplicationController
5
3
  before_action :authenticate_developer_or_admin!
6
4
 
7
5
  def design
@@ -1,6 +1,4 @@
1
- require_dependency "avo/base_controller"
2
-
3
1
  module Avo
4
- class ResourcesController < BaseController
2
+ class ResourcesController < Avo::BaseController
5
3
  end
6
4
  end
@@ -3,10 +3,8 @@
3
3
  # This controller is currently only used for searchable fields, it keeps the old search controller without any changes
4
4
  # Possibly have much more logic that it uses, but it's not worth the effort to refactor it since we're going to replace it with a new searchable fields way
5
5
 
6
- require_dependency "avo/application_controller"
7
-
8
6
  module Avo
9
- class SearchController < ApplicationController
7
+ class SearchController < Avo::ApplicationController
10
8
  include Rails.application.routes.url_helpers
11
9
  include ActionView::Helpers::TextHelper
12
10
 
@@ -2,8 +2,6 @@ import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  // Connects to data-controller="modal-size"
4
4
  export default class extends Controller {
5
- static targets = ['container']
6
-
7
5
  static values = {
8
6
  currentWidth: String,
9
7
  currentHeight: String,
@@ -28,56 +26,18 @@ export default class extends Controller {
28
26
  }
29
27
 
30
28
  updateWidthClass(width) {
31
- // Remove all modal width classes from the modal element
32
- this.modalElement.classList.remove(
33
- 'modal--width-sm',
34
- 'modal--width-md',
35
- 'modal--width-lg',
36
- 'modal--width-xl',
37
- 'modal--width-2xl',
38
- 'modal--width-3xl',
39
- 'modal--width-4xl',
40
- 'modal--width-full',
41
- 'modal--width-25',
42
- 'modal--width-50',
43
- 'modal--width-75',
44
- 'modal--width-100',
45
- )
46
-
47
- // Remove all width classes from the container
48
- this.containerTarget.classList.remove('w-sm', 'w-md', 'w-lg', 'w-xl', 'w-2xl', 'w-3xl', 'w-4xl', 'w-full')
49
-
50
- // Add the new modal width class to the modal element
51
- this.modalElement.classList.add(`modal--width-${width}`)
52
-
53
- // Add the corresponding width class to the container
54
- if (width === 'full') {
55
- this.containerTarget.classList.add('w-full')
56
- } else {
57
- this.containerTarget.classList.add(`w-${width}`)
58
- }
29
+ this.#swapModifier('modal--width-', width)
59
30
  }
60
31
 
61
32
  updateHeightClass(height) {
62
- // Remove all modal height classes from the modal element
63
- this.modalElement.classList.remove(
64
- 'modal--height-auto',
65
- 'modal--height-sm',
66
- 'modal--height-md',
67
- 'modal--height-lg',
68
- 'modal--height-xl',
69
- 'modal--height-2xl',
70
- 'modal--height-3xl',
71
- 'modal--height-4xl',
72
- 'modal--height-full',
73
- 'modal--height-25',
74
- 'modal--height-50',
75
- 'modal--height-75',
76
- 'modal--height-100',
77
- )
33
+ this.#swapModifier('modal--height-', height)
34
+ }
78
35
 
79
- // Add the new modal height class to the modal element
80
- // The CSS will handle applying the appropriate height to .modal__card
81
- this.modalElement.classList.add(`modal--height-${height}`)
36
+ // The CSS owns the sizes: each `modal--width-*` / `modal--height-*` modifier
37
+ // sizes `.modal__card`. Swapping by prefix means a new size token is a CSS
38
+ // rule and nothing else — no list here to keep in sync with the stylesheet.
39
+ #swapModifier(prefix, value) {
40
+ this.modalElement.classList.remove(...[...this.modalElement.classList].filter((name) => name.startsWith(prefix)))
41
+ this.modalElement.classList.add(`${prefix}${value}`)
82
42
  }
83
43
  }
@@ -9,8 +9,19 @@ export default class extends Controller {
9
9
 
10
10
  autoClicking = false
11
11
 
12
+ // Every lookup in here is scoped to this controller's element. A page can hold several tables (a
13
+ // show page renders one per has_many association) and each numbers its rows from zero, so a
14
+ // document-wide lookup by index always lands in the first table.
12
15
  get itemSelectorCells() {
13
- return document.querySelectorAll('.item-selector-cell')
16
+ return this.element.querySelectorAll('.item-selector-cell')
17
+ }
18
+
19
+ #rowAt(index) {
20
+ return this.element.querySelector(`tr[data-index="${index}"]`)
21
+ }
22
+
23
+ #checkboxAt(index) {
24
+ return this.element.querySelector(`input[type="checkbox"][data-index="${index}"]`)
14
25
  }
15
26
 
16
27
  get hasLastCheckedIndex() {
@@ -32,6 +43,12 @@ export default class extends Controller {
32
43
  return
33
44
  }
34
45
 
46
+ // Ranges are addressed by row. Index view types outside a table (avo-notifications renders one)
47
+ // give this component an index but no row to walk up to, so there's nothing to span.
48
+ if (!event.target.closest('tr')) {
49
+ return
50
+ }
51
+
35
52
  // If there's no last checked index and the shift key isn't pressed, set the starting index
36
53
  if (!this.hasLastCheckedIndex) {
37
54
  this.#setStartingIndex(event)
@@ -57,7 +74,7 @@ export default class extends Controller {
57
74
 
58
75
  // Loop through the range of rows and toggle the checkboxes
59
76
  theRange.forEach((index) => {
60
- const checkbox = document.querySelector(`input[type="checkbox"][data-index="${index}"]`)
77
+ const checkbox = this.#checkboxAt(index)
61
78
 
62
79
  // Toggle the checkbox if it's not in the same state as the target checkbox
63
80
  if (checkbox.checked !== state) {
@@ -110,7 +127,8 @@ export default class extends Controller {
110
127
  #selectorMouseenterHandler(event) {
111
128
  // Add the highlighted-row class to the row that the mouse is over
112
129
  event.target.closest('tr').classList.add('highlighted-row')
113
- if (this.lastCheckedIndex) {
130
+ // Check against null, not truthiness — index 0 is a valid anchor
131
+ if (this.hasLastCheckedIndex) {
114
132
  // Highlight the range of rows between the last checked index and the current index
115
133
  this.#highlightRange(this.lastCheckedIndex, parseInt(event.target.closest('tr').dataset.index))
116
134
  }
@@ -120,17 +138,16 @@ export default class extends Controller {
120
138
  // Remove the highlighted-row class from the row that the mouse is over
121
139
  event.target.closest('tr').classList.remove('highlighted-row')
122
140
  // Remove the highlighted-row class from all rows
123
- document.querySelectorAll('tr[data-index]').forEach((tr) => {
141
+ this.element.querySelectorAll('tr[data-index]').forEach((tr) => {
124
142
  tr.classList.remove('highlighted-row')
125
143
  })
126
144
  }
127
145
 
128
146
  // Highlight the range of rows between the start index and the end index
129
147
  #highlightRange(startIndex, endIndex) {
130
- const theRange = difference(range(startIndex, endIndex))
131
- theRange.forEach((index) => {
132
- const tr = document.querySelector(`tr[data-index="${index}"]`)
133
- tr.classList.add('highlighted-row')
148
+ // `range` excludes the end index, but the hovered row is highlighted by the caller
149
+ range(startIndex, endIndex).forEach((index) => {
150
+ this.#rowAt(index).classList.add('highlighted-row')
134
151
  })
135
152
  }
136
153
 
@@ -1,6 +1,9 @@
1
1
  <%= render Avo::TurboFrameWrapperComponent.new(turbo_frame_request_id) do %>
2
2
  <% if turbo_frame_request_id %>
3
- <%= render Avo::ModalComponent.new(title: t("avo.media_library.title")) do |c| %>
3
+ <%# The library is a grid of media cards (13rem floor per card), so the default
4
+ :xl modal fits two columns and 4xl three, leaving a ragged last row. 5xl
5
+ fits four, so a page of 12 lands as three full rows. %>
6
+ <%= render Avo::ModalComponent.new(title: t("avo.media_library.title"), width: :"5xl") do |c| %>
4
7
  <div class="p-4">
5
8
  <%= render Avo::MediaLibrary::ListComponent.new attaching: @attaching %>
6
9
  </div>
data/avo.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  "public gem pushes."
29
29
  end
30
30
 
31
- spec.required_ruby_version = ">= 3.0.0"
31
+ spec.required_ruby_version = ">= 3.2.0"
32
32
  spec.post_install_message = "Thank you for using Avo 💪 Docs are available at https://docs.avohq.io"
33
33
 
34
34
  # NOTE: `public/` is rejected below — Avo 4 ships precompiled assets from
@@ -2,6 +2,19 @@ module Avo
2
2
  module Resources
3
3
  module Controls
4
4
  class BaseControl
5
+ unless defined?(CONTROL_OPTIONS)
6
+ # Keys the control consumes itself, either as an option it renders from
7
+ # (`style`, `icon`…) or as internal plumbing merged in by `execute_block`
8
+ # (`as_index_control`, `item`…). Everything a user passes *outside* this
9
+ # list is forwarded to the rendered element as an HTML attribute, so
10
+ # `link_to "Docs", url, rel: "noopener"` reaches the `<a>` tag.
11
+ CONTROL_OPTIONS = %i[
12
+ label path title target data class
13
+ style color size icon icon_class confirmation_message
14
+ as_index_control from_custom_list item
15
+ ].freeze
16
+ end
17
+
5
18
  attr_reader :label, :title, :color, :style, :icon, :icon_class, :confirmation_message, :size, :as_index_control
6
19
 
7
20
  def initialize(**args)
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.0.25" unless const_defined?(:VERSION)
2
+ VERSION = "4.1.0" 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.0.25
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin
@@ -1189,7 +1189,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
1189
1189
  requirements:
1190
1190
  - - ">="
1191
1191
  - !ruby/object:Gem::Version
1192
- version: 3.0.0
1192
+ version: 3.2.0
1193
1193
  required_rubygems_version: !ruby/object:Gem::Requirement
1194
1194
  requirements:
1195
1195
  - - ">="