avo 4.3.9 → 4.3.10

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.
@@ -28,4 +28,48 @@ class Avo::ViewTypes::BaseViewTypeComponent < Avo::BaseComponent
28
28
  field_per_page: field&.per_page
29
29
  )
30
30
  end
31
+
32
+ def selected_page_label
33
+ if @resource.pagination_type.countless?
34
+ t "avo.x_records_selected_from_page_html", selected: @pagy.in
35
+ else
36
+ t "avo.x_records_selected_from_a_total_of_x_html", selected: @pagy.in, count: @pagy.count
37
+ end
38
+ end
39
+
40
+ def selected_all_label
41
+ if @resource.pagination_type.countless?
42
+ t "avo.records_selected_from_all_pages_html"
43
+ else
44
+ t "avo.x_records_selected_from_all_pages_html", count: @pagy.count
45
+ end
46
+ end
47
+
48
+ def encrypted_query
49
+ # A page cut by its source is all that is held, so there is nothing to select across pages
50
+ return :select_all_disabled if @query.is_a?(Avo::Resources::ArrayResource::Page)
51
+
52
+ # TODO: move this to the resource where we can apply the adapter pattern
53
+ if Module.const_defined?("Ransack::Search") && @query.instance_of?(Ransack::Search)
54
+ @query = @query.result
55
+ end
56
+
57
+ Avo::Services::EncryptionService.encrypt(message: @query, purpose: :select_all, serializer: Marshal)
58
+ rescue
59
+ disable_select_all
60
+ end
61
+
62
+ private
63
+
64
+ def disable_select_all
65
+ if Rails.env.development?
66
+ Avo.error_manager.add({
67
+ url: "https://docs.avohq.io/4.0/select-all.html#serialization-known-issues",
68
+ target: "_blank",
69
+ message: "An error occurred while serializing the query object. The Select All feature has been disabled because it depends on successful query serialization. For more details and troubleshooting steps, click here."
70
+ })
71
+ end
72
+
73
+ :select_all_disabled
74
+ end
31
75
  end
@@ -1,21 +1,75 @@
1
1
  <turbo-frame id="<%= list_frame_id %>" target="_top" class="relative flex w-full flex-col @container">
2
- <%# tabindex=-1 makes the wrapper a Shift+T focus anchor (top-level grids only, %>
3
- <%# never a has-many grid on a show page); from here Tab steps through the cards. %>
2
+ <%# The selection controller wraps the bar and the cards. The reorder gem sorts the grid wrapper's children, %>
3
+ <%# so the bar can't sit inside it. %>
4
4
  <%= content_tag :div,
5
- class: "grid-wrapper",
6
- tabindex: (@reflection.present? ? nil : "-1"),
5
+ class: "flex w-full flex-col gap-3",
7
6
  data: {
8
- component_name: self.class.to_s.underscore,
9
7
  controller: "item-select-all",
10
8
  resource_name: resource_name_for(@resource),
9
+ item_select_all_selected_all_value: false,
10
+ item_select_all_selected_all_query_value: encrypted_query,
11
+ item_select_all_page_count_value: @pagy.pages,
11
12
  selected_resources_name: resource_name_for(@resource),
12
- selected_resources: [],
13
- **(@reflection.present? ? {} : {"content-focus": ""}),
14
- **(try(:drag_reorder_data_attributes) || {})
13
+ selected_resources: []
15
14
  } do %>
16
- <% @resources.each_with_index do |resource, index| %>
17
- <% cache_if Avo.configuration.cache_resources_on_index_view, resource.index_cache_key(@parent_record, cache_context:), expires_in: 1.day do %>
18
- <%= render(resource.resolve_component(Avo::Index::GridItemComponent).new(resource: resource, reflection: @reflection, parent_record: @parent_record, parent_resource: @parent_resource, actions: actions)) %>
15
+ <% if @resource.record_selector %>
16
+ <div class="flex min-h-7 flex-wrap items-center gap-x-3 gap-y-1 px-2 text-sm" data-component-name="avo/view_types/grid_select_all">
17
+ <%= content_tag :label, class: "flex items-center gap-2 cursor-pointer text-content-secondary" do %>
18
+ <%= tag :input,
19
+ type: "checkbox",
20
+ name: t("avo.select_all"),
21
+ autocomplete: :off,
22
+ data: {
23
+ action: "input->item-select-all#toggle",
24
+ item_select_all_target: "checkbox"
25
+ } %>
26
+ <%= t "avo.select_all" %>
27
+ <% end %>
28
+
29
+ <% if @pagy.pages > 1 %>
30
+ <div class="hidden border-s border-tertiary ps-3" data-item-select-all-target="selectAllOverlay">
31
+ <div data-item-select-all-target="unselectedMessage">
32
+ <%= selected_page_label %>
33
+
34
+ <%= a_link request.url,
35
+ size: :xs,
36
+ color: :primary,
37
+ class: "relative -top-[0.1rem] ms-2 cursor-pointer text-primary-500",
38
+ data: {action: "click->item-select-all#selectAll"} do %>
39
+ <%= t "avo.select_all_matching" %>
40
+ <% end %>
41
+ </div>
42
+
43
+ <div class="hidden" data-item-select-all-target="selectedMessage">
44
+ <%= selected_all_label %>
45
+
46
+ <%= a_link request.url,
47
+ size: :xs,
48
+ color: :primary,
49
+ class: "relative -top-[0.1rem] ms-2 cursor-pointer text-primary-500",
50
+ data: {action: "click->item-select-all#selectAll"} do %>
51
+ <%= t("avo.undo").humanize %>
52
+ <% end %>
53
+ </div>
54
+ </div>
55
+ <% end %>
56
+ </div>
57
+ <% end %>
58
+
59
+ <%# tabindex=-1 makes the wrapper a Shift+T focus anchor (top-level grids only, %>
60
+ <%# never a has-many grid on a show page); from here Tab steps through the cards. %>
61
+ <%= content_tag :div,
62
+ class: "grid-wrapper",
63
+ tabindex: (@reflection.present? ? nil : "-1"),
64
+ data: {
65
+ component_name: self.class.to_s.underscore,
66
+ **(@reflection.present? ? {} : {"content-focus": ""}),
67
+ **(try(:drag_reorder_data_attributes) || {})
68
+ } do %>
69
+ <% @resources.each_with_index do |resource, index| %>
70
+ <% cache_if Avo.configuration.cache_resources_on_index_view, resource.index_cache_key(@parent_record, cache_context:), expires_in: 1.day do %>
71
+ <%= render(resource.resolve_component(Avo::Index::GridItemComponent).new(resource: resource, reflection: @reflection, parent_record: @parent_record, parent_resource: @parent_resource, actions: actions)) %>
72
+ <% end %>
19
73
  <% end %>
20
74
  <% end %>
21
75
  <% end %>
@@ -24,36 +24,6 @@ class Avo::ViewTypes::TableComponent < Avo::ViewTypes::BaseViewTypeComponent
24
24
  }
25
25
  end
26
26
 
27
- def encrypted_query
28
- # A page cut by its source is all that is held, so there is nothing to select across pages
29
- return :select_all_disabled if @query.is_a?(Avo::Resources::ArrayResource::Page)
30
-
31
- # TODO: move this to the resource where we can apply the adapter pattern
32
- if Module.const_defined?("Ransack::Search") && @query.instance_of?(Ransack::Search)
33
- @query = @query.result
34
- end
35
-
36
- Avo::Services::EncryptionService.encrypt(message: @query, purpose: :select_all, serializer: Marshal)
37
- rescue
38
- disable_select_all
39
- end
40
-
41
- def selected_page_label
42
- if @resource.pagination_type.countless?
43
- t "avo.x_records_selected_from_page_html", selected: @pagy.in
44
- else
45
- t "avo.x_records_selected_from_a_total_of_x_html", selected: @pagy.in, count: @pagy.count
46
- end
47
- end
48
-
49
- def selected_all_label
50
- if @resource.pagination_type.countless?
51
- t "avo.records_selected_from_all_pages_html"
52
- else
53
- t "avo.x_records_selected_from_all_pages_html", count: @pagy.count
54
- end
55
- end
56
-
57
27
  # Table rows are not fragment-cached. A `cache_if` wrapped this until it was
58
28
  # removed: a `return` inside its block unwound past Action View's
59
29
  # `write_fragment`, so nothing was ever written, and a hit would have handed
@@ -97,18 +67,4 @@ class Avo::ViewTypes::TableComponent < Avo::ViewTypes::BaseViewTypeComponent
97
67
 
98
68
  [header_fields, table_row_components]
99
69
  end
100
-
101
- private
102
-
103
- def disable_select_all
104
- if Rails.env.development?
105
- Avo.error_manager.add({
106
- url: "https://docs.avohq.io/4.0/select-all.html#serialization-known-issues",
107
- target: "_blank",
108
- message: "An error occurred while serializing the query object. The Select All feature has been disabled because it depends on successful query serialization. For more details and troubleshooting steps, click here."
109
- })
110
- end
111
-
112
- :select_all_disabled
113
- end
114
70
  end
@@ -1,6 +1,8 @@
1
1
  import { AttributeObserver, Controller } from '@hotwired/stimulus'
2
2
  import { setSelection, takeSelection } from '../index_state'
3
3
 
4
+ const MAX_ACTION_URL_LENGTH = 6000
5
+
4
6
  export default class extends Controller {
5
7
  static targets = [
6
8
  'itemCheckbox',
@@ -24,6 +26,10 @@ export default class extends Controller {
24
26
  this.selectedResourcesObserver = new AttributeObserver(this.element, 'data-selected-resources', this)
25
27
  this.selectedResourcesObserver.start()
26
28
 
29
+ // The action links carry the index query from the start, so an action's fields and messages can read it
30
+ // even when nothing is selected.
31
+ this.updateLinks('resourceIds')
32
+
27
33
  // Wait for the rows' item-selector controllers to connect; they apply the selection.
28
34
  if (rememberedIds.length > 0) {
29
35
  Promise.resolve().then(() => this.restoreSelection(rememberedIds))
@@ -58,7 +64,8 @@ export default class extends Controller {
58
64
  if (!this.hasCheckboxTarget) return
59
65
 
60
66
  // If all are selected, mark the checkbox as checked.
61
- const rowCount = this.element.querySelectorAll('tbody tr').length
67
+ // Table rows and grid cards both carry an item checkbox.
68
+ const rowCount = this.itemCheckboxTargets.length
62
69
  // Reset the checkbox
63
70
  this.checkboxTarget.indeterminate = false
64
71
  this.checkboxTarget.checked = false
@@ -151,11 +158,19 @@ export default class extends Controller {
151
158
  url.searchParams.set('fields[avo_resource_ids]', resourceIds)
152
159
  url.searchParams.set('fields[avo_selected_all]', 'false')
153
160
  } else if (param === 'selectedQuery') {
154
- const selectedQuery = this.element.dataset.itemSelectAllSelectedAllQueryValue
155
- url.searchParams.set('fields[avo_index_query]', selectedQuery)
156
161
  url.searchParams.set('fields[avo_selected_all]', 'true')
157
162
  }
158
163
 
164
+ if (this.indexQueryAvailable()) {
165
+ url.searchParams.set('fields[avo_index_query]', this.selectedAllQueryValue)
166
+
167
+ // Puma and nginx reject request lines around 8-10 KB. Only select all needs the query to open the
168
+ // modal; without it, the modal texts see no index_query and handle still gets it from the form.
169
+ if (param !== 'selectedQuery' && url.toString().length > MAX_ACTION_URL_LENGTH) {
170
+ url.searchParams.delete('fields[avo_index_query]')
171
+ }
172
+ }
173
+
159
174
  link.href = url.toString()
160
175
  } catch (error) {
161
176
  console.error('Error updating link:', link, error)
@@ -179,6 +194,11 @@ export default class extends Controller {
179
194
 
180
195
  // True if there are more pages available and if query encryption run successfully
181
196
  selectAllEnabled() {
182
- return this.pageCountValue > 1 && this.selectedAllQueryValue !== 'select_all_disabled'
197
+ return this.pageCountValue > 1 && this.indexQueryAvailable()
198
+ }
199
+
200
+ // A view type renders 'select_all_disabled' when it can't encrypt the query, and a custom one may render none
201
+ indexQueryAvailable() {
202
+ return this.selectedAllQueryValue !== '' && this.selectedAllQueryValue !== 'select_all_disabled'
183
203
  }
184
204
  }
@@ -41,6 +41,7 @@ module Avo
41
41
  attr_reader :appended_turbo_streams
42
42
  attr_reader :records_to_reload
43
43
  attr_reader :query
44
+ attr_reader :index_query
44
45
 
45
46
  # TODO: find a differnet way to delegate this to the uninitialized Current variable
46
47
  delegate :context, to: Avo::Current
@@ -130,7 +131,8 @@ module Avo
130
131
  record: @record,
131
132
  view: @view,
132
133
  arguments: @arguments,
133
- query: @query
134
+ query: @query,
135
+ index_query: @index_query
134
136
  ).handle
135
137
  end
136
138
 
@@ -388,7 +390,8 @@ module Avo
388
390
  record: @record,
389
391
  view: @view,
390
392
  arguments:,
391
- query: @query
393
+ query: @query,
394
+ index_query: @index_query
392
395
  ).handle
393
396
  end
394
397
 
@@ -406,7 +409,8 @@ module Avo
406
409
  record: @record,
407
410
  view: @view,
408
411
  arguments: @arguments,
409
- query: @query
412
+ query: @query,
413
+ index_query: @index_query
410
414
  ).handle
411
415
  end
412
416
 
@@ -221,7 +221,7 @@ A license authorizes **one app, one production URL** (`Rails.env.production?`).
221
221
 
222
222
  Verify at the **status page**: `https://yourapp.com/<mount-path>/avo_private/status` (e.g. `.../avo/avo_private/status` or `.../admin/avo_private/status`). It shows whether the license authenticated and what the checking server returned; the key is masked unless you set `config.exclude_from_status = []`. The viewing user must be an Avo admin. Deep failure diagnosis (unlicensed after deploy, timeouts, test-suite blocking the check host) is the **avo-troubleshoot** skill.
223
223
 
224
- **Trials are full licenses.** Every add-on a trial covers behaves exactly as it does on a paid license while the trial runs — nothing is gated or degraded. The **Avo Status** indicator in the sidebar footer is green when all is well, **amber** when a trial needs attention, and orange when the license is invalid. Amber is not a failure; it means either no payment method is on file (access stops on the trial's end date) or the subscription behind the trial was cancelled (access stops on the cancellation date, and adding a payment method won't change that — the subscription has to be resumed). Both are fixed on https://avohq.io/licenses, and the status page names which one applies. Avo caches the license response between checks, so press **Refresh license** on the status page afterwards or the app keeps showing the older billing state. Trial state needs `avo-licensing` **4.0.10+**; the amber indicator needs Avo **4.0.20+** (an older Avo renders an uncolored dot rather than breaking).
224
+ **Trials are full licenses.** Every add-on a trial covers behaves exactly as it does on a paid license while the trial runs — nothing is gated or degraded. The **Avo Status** indicator in the sidebar footer is green when all is well, **amber** when a trial needs attention, and orange when the license is invalid. Amber is not a failure: the status page names why, and the fix is on https://avohq.io/licenses. The **avo-troubleshoot** skill lists each case and its fix. Avo caches the license response between checks, so press **Refresh license** on the status page afterwards or the app keeps showing the older billing state. Trial state needs `avo-licensing` **4.0.10+**; the amber indicator needs Avo **4.0.20+** (an older Avo renders an uncolored dot rather than breaking).
225
225
 
226
226
  ### 7. (Optional) Append your own routes inside the engine
227
227
 
@@ -210,7 +210,12 @@ self.search = {
210
210
  1. **Key not set on the server.** The most frequent cause. Confirm `config.license_key = ENV["AVO_LICENSE_KEY"]` and that the env var is actually present in **production** (not just locally).
211
211
  2. **Check the status page.** Every Avo app exposes `https://yourapp.com/<mount>/avo_private/status` — e.g. `/admin/avo_private/status` if you mounted Avo at `admin`. It shows whether the license authenticated and the raw response from the check server. The viewing user must be an Avo admin.
212
212
  3. **Key hidden on the status page.** The key is redacted by default; set `exclude_from_status = []` in the initializer if you need to see it while debugging.
213
- 4. **The sidebar indicator is amber, not orange.** That is not a license failure — it is a running trial that needs attention: either no payment method is on file, or the subscription behind the trial was cancelled. Access continues until the date the status page reports. Both are fixed on https://avohq.io/licenses (a cancelled subscription must be resumed — adding a payment method won't help), then press **Refresh license** on the status page, since Avo caches the license response between checks.
213
+ 4. **The sidebar indicator is amber, not orange.** That is not a license failure — it is a running trial that needs attention, and the status page shows an **Action needed before <date>** tag below the license name, the date being when access stops. Access continues until the date the status page reports. Each case is fixed on https://avohq.io/licenses:
214
+ - No payment method is on file: add one.
215
+ - The subscription behind the trial was cancelled: resume it. A payment method won't help.
216
+ - An Avo 4 trial runs beside an Avo 3 subscription: press **Migrate to Avo 4** before the trial's end date, or Avo 4 stops working in the app when the trial ends (the Avo 3 license stays active, so the app can move back to Avo 3). A payment method won't help. An older `avo-licensing` shows add-a-payment-method steps for this case; migrating is still the fix.
217
+
218
+ Then press **Refresh license** on the status page, since Avo caches the license response between checks.
214
219
 
215
220
  → `license-troubleshooting.html`
216
221
 
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.3.9" unless const_defined?(:VERSION)
2
+ VERSION = "4.3.10" unless const_defined?(:VERSION)
3
3
  end
@@ -1,5 +1,19 @@
1
- desc "Runs the update command for all Avo gems."
1
+ require "open3"
2
+
3
+ desc "Updates all Avo gems; set AVO_UPDATE_PUSH=1 to commit Gemfile.lock and push."
2
4
  task "avo:update" => :environment do
5
+ push = ENV["AVO_UPDATE_PUSH"] == "1"
6
+ lockfile = Bundler.default_lockfile.to_s
7
+
8
+ if push
9
+ status, result = Open3.capture2("git", "status", "--porcelain", "--untracked-files=no")
10
+ abort "[Avo->] AVO_UPDATE_PUSH requires a Git repository." unless result.success?
11
+ abort "[Avo->] AVO_UPDATE_PUSH requires no changes to tracked files." unless status.empty?
12
+ unless system("git", "rev-parse", "--verify", "@{upstream}", out: File::NULL, err: File::NULL)
13
+ abort "[Avo->] AVO_UPDATE_PUSH requires a branch with an existing upstream."
14
+ end
15
+ end
16
+
3
17
  # Start with avo core (it doesn't register itself as a plugin), then resolve
4
18
  # every registered plugin to its real gem name. Plugins register under
5
19
  # nicknames (e.g. `avo-rhino_field` registers as `:rhino`), so we ask each
@@ -20,7 +34,21 @@ task "avo:update" => :environment do
20
34
  # `bundle exec` settings (BUNDLE_GEMFILE, BUNDLE_FROZEN/--deployment, RUBYOPT),
21
35
  # which can make the update silently refuse. Exported credentials survive
22
36
  # because they're part of the original env Bundler snapshotted at boot.
23
- Bundler.with_original_env { system cmd }
37
+ abort "[Avo->] Gem update failed; nothing committed or pushed." unless Bundler.with_original_env { system cmd }
38
+
39
+ if push
40
+ abort "[Avo->] Could not stage #{lockfile}; nothing pushed." unless system("git", "add", "--", lockfile)
41
+ _, result = Open3.capture2("git", "diff", "--cached", "--quiet", "--", lockfile)
42
+ if result.success?
43
+ puts "[Avo->] Gemfile.lock is unchanged; nothing committed or pushed."
44
+ next
45
+ end
46
+ abort "[Avo->] Could not inspect Gemfile.lock; nothing pushed." unless result.exitstatus == 1
47
+ unless system("git", "commit", "--only", "-m", "Update Avo gems", "--", lockfile)
48
+ abort "[Avo->] Commit failed; nothing pushed."
49
+ end
50
+ abort "[Avo->] Push failed; the update commit is available locally." unless system("git", "push")
51
+ end
24
52
  end
25
53
 
26
54
  desc "Builds Avo (just assets for now)"
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.3.9
4
+ version: 4.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin