avo 4.3.8 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/assets/builds/avo/application.css +12 -0
- data/app/assets/builds/avo/application.js +54 -54
- data/app/assets/builds/avo/application.js.map +3 -3
- data/app/components/avo/view_types/base_view_type_component.rb +44 -0
- data/app/components/avo/view_types/grid_component.html.erb +65 -11
- data/app/components/avo/view_types/table_component.rb +0 -44
- data/app/controllers/avo/actions_controller.rb +17 -2
- data/app/controllers/avo/base_application_controller.rb +12 -7
- data/app/javascript/js/controllers/item_select_all_controller.js +24 -4
- data/lib/avo/base_action.rb +7 -3
- data/lib/avo/skills/avo-actions/SKILL.md +1 -1
- data/lib/avo/skills/avo-setup/SKILL.md +1 -1
- data/lib/avo/skills/avo-troubleshoot/SKILL.md +6 -1
- data/lib/avo/version.rb +1 -1
- data/lib/tasks/avo_tasks.rake +30 -2
- metadata +1 -1
|
@@ -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
|
-
<%#
|
|
3
|
-
<%#
|
|
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: "
|
|
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
|
-
<% @
|
|
17
|
-
|
|
18
|
-
<%=
|
|
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
|
|
@@ -71,7 +71,7 @@ module Avo
|
|
|
71
71
|
def set_query
|
|
72
72
|
# If the user selected all records, use the decrypted index query
|
|
73
73
|
# Otherwise, find the records from the resource ids
|
|
74
|
-
@query = if
|
|
74
|
+
@query = if selected_all?
|
|
75
75
|
decrypted_index_query
|
|
76
76
|
else
|
|
77
77
|
find_records_from_resource_ids
|
|
@@ -252,8 +252,23 @@ module Avo
|
|
|
252
252
|
end
|
|
253
253
|
end
|
|
254
254
|
|
|
255
|
+
def selected_all?
|
|
256
|
+
action_params[:fields]&.dig(:avo_selected_all) == "true"
|
|
257
|
+
end
|
|
258
|
+
|
|
255
259
|
def verify_authorization
|
|
256
|
-
raise Avo::NotAuthorizedError.new unless @action.authorized?
|
|
260
|
+
raise Avo::NotAuthorizedError.new unless @action.authorized? && act_on_authorized?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The Actions button asks `act_on?`, so the run asks it too, of every record it names: the one in
|
|
264
|
+
# the URL and the selected ids. With none, the model class stands in, as on the index. A select-all
|
|
265
|
+
# run is the index query the button already allowed, and is not loaded here to ask it row by row.
|
|
266
|
+
def act_on_authorized?
|
|
267
|
+
records = [@record, *(@query unless selected_all?)].compact
|
|
268
|
+
|
|
269
|
+
(records.presence || [@resource.model_class]).all? do |record|
|
|
270
|
+
@resource.authorization.authorize_action(:act_on, record:, raise_exception: false)
|
|
271
|
+
end
|
|
257
272
|
end
|
|
258
273
|
end
|
|
259
274
|
end
|
|
@@ -245,16 +245,21 @@ module Avo
|
|
|
245
245
|
Avo.configuration.locale = locale if locale
|
|
246
246
|
end
|
|
247
247
|
|
|
248
|
+
# With nowhere to go back to, the refusal renders here: redirecting to the root loops when the
|
|
249
|
+
# home page is the one refused.
|
|
248
250
|
def render_unauthorized(exception)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
root_url
|
|
253
|
-
else
|
|
254
|
-
request.referrer
|
|
251
|
+
if request.referrer.present? && request.referrer != request.url
|
|
252
|
+
flash[:notice] = t "avo.not_authorized"
|
|
253
|
+
return redirect_to(request.referrer)
|
|
255
254
|
end
|
|
256
255
|
|
|
257
|
-
|
|
256
|
+
respond_to do |format|
|
|
257
|
+
format.html do
|
|
258
|
+
Avo.error_manager.add t("avo.not_authorized")
|
|
259
|
+
render html: "", layout: "avo/application", status: :forbidden
|
|
260
|
+
end
|
|
261
|
+
format.any { head :forbidden }
|
|
262
|
+
end
|
|
258
263
|
end
|
|
259
264
|
|
|
260
265
|
def set_authorization
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
}
|
data/lib/avo/base_action.rb
CHANGED
|
@@ -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
|
|
|
@@ -260,7 +260,7 @@ When an index spans multiple pages, checking "Select all" offers to select **eve
|
|
|
260
260
|
|
|
261
261
|
- **`query` is always an array.** Even a single-record action gets `[record]`. Use `query.first` for the one-record case; don't call record methods on `query` directly. `records` is an alias.
|
|
262
262
|
- **Records deleted after selection are omitted.** Another user may delete a checked record before the action request arrives. Avo drops that missing record and still passes every surviving selection to `handle`; action authors do not need to rescue `ActiveRecord::RecordNotFound` for this race. `query` can therefore come back **empty** when every selected record is gone — guard with `return error "No record selected" if query.blank?` if that matters.
|
|
263
|
-
- **"My action doesn't show up" is usually the policy.** With Pundit, `act_on?` in the resource's policy gates
|
|
263
|
+
- **"My action doesn't show up", or is refused, is usually the policy.** With Pundit, `act_on?` in the resource's policy gates the Actions dropdown and every run: Avo asks it of the record in the URL and of each selected record (of the model class when there are none, and for a "Select all" run), and one refusal refuses the whole run. An action started from an association panel shows under the parent's `act_on_<assoc>?`, but still runs only on children whose own `act_on?` allows it. `authorize` on the action gates it further. Check the policy first. See the **`avo-authorization`** skill.
|
|
264
264
|
- **`view` is the view the action was started from.** In `authorize`, `visible`, the modal text blocks, `fields`, and `handle`, `view` (and `resource.view`) is `index`, `show`, or `edit` — the page the user opened the action from — both while the modal renders and while the action runs. It is never `new`; the modal is not a view. It arrives with the request and is checked against the route (`show` and `edit` need a record in the URL), so use it to adapt behavior (`reload_records if view.index?`), never as the only gate on who may run the action: gate people on `current_user`, and single-versus-bulk on `query.size` or `record`.
|
|
265
265
|
- **Every field in `fields` renders in the modal.** `hide_on`/`show_on` marks are for resource views and are ignored here. A field with a form component (text, select, hidden...) renders as an input; a display-only field (badge, key/value) renders read-only, as on Show. The one mark that counts: a badge declared with `show_on: :forms` renders as a select. The modal focuses its first input; a field with `autofocus: true` takes the focus instead (avo-fields lists the fields that support it). Use it to show context next to the inputs.
|
|
266
266
|
- **The modal is a NEW request.** Params from the Index/Show page that opened it are **not** available in `fields`/`handle`. To prefill from the triggering page, parse `request.referer`:
|
|
@@ -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
|
|
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
|
|
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
data/lib/tasks/avo_tasks.rake
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
|
|
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)"
|