avo 4.3.4 → 4.3.5
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 +3 -0
- data/app/assets/stylesheets/css/components/field-wrapper.css +6 -0
- data/app/components/avo/debug/status_component.html.erb +8 -1
- data/app/controllers/avo/actions_controller.rb +6 -17
- data/app/controllers/avo/associations_controller.rb +28 -15
- data/app/controllers/avo/base_application_controller.rb +3 -0
- data/app/controllers/avo/base_controller.rb +3 -0
- data/lib/avo/fields/belongs_to_field.rb +28 -3
- data/lib/avo/fields/files_field.rb +3 -2
- data/lib/avo/resources/array_resource.rb +9 -6
- data/lib/avo/resources/base.rb +65 -23
- data/lib/avo/skills/avo-associations/SKILL.md +1 -0
- data/lib/avo/skills/avo-resources/SKILL.md +1 -1
- data/lib/avo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ddab6468262c61b878d497c3c6c35847b0e646d131c06ccb32e7bf89f98a328
|
|
4
|
+
data.tar.gz: 3d6e374b3b15f9272d319bec1dbbd58ae6e4bc8f1f22d7c560ae2d05cd771b53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87458e20763b4c2c4b094d195cbfb7b7dbc23cb7dccd9bb5120b8e7dec6122f0bf258de1fdf13f76acc9a10da11bb7660f6118b403fd2448505edb1cd50fbc1a
|
|
7
|
+
data.tar.gz: f36f83193fed39820345714623eccaafe8c6cce3f8074656758bf3214be30c4bde406af75c6723c394336851e091bb0b364ee704f57bd9f6879d46ef9a234891
|
data/Gemfile.lock
CHANGED
|
@@ -7920,6 +7920,9 @@ tag.tagify__tag {
|
|
|
7920
7920
|
width: calc(var(--spacing) * 52);
|
|
7921
7921
|
}
|
|
7922
7922
|
}
|
|
7923
|
+
.field-wrapper__label .field_with_errors {
|
|
7924
|
+
display: inline;
|
|
7925
|
+
}
|
|
7923
7926
|
.field-wrapper__help {
|
|
7924
7927
|
margin-top: calc(var(--spacing) * 2);
|
|
7925
7928
|
font-size: var(--text-xs);
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
@apply flex self-start items-center shrink-0 w-full px-4 font-medium text-content text-sm md:w-52 min-h-10;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/* Rails' field_error_proc wraps an errored label in a block div, which would push the
|
|
10
|
+
required asterisk onto its own line. */
|
|
11
|
+
.field-wrapper__label .field_with_errors {
|
|
12
|
+
@apply inline;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
.field-wrapper__help {
|
|
10
16
|
@apply text-content-secondary mt-2 text-xs leading-4;
|
|
11
17
|
}
|
|
@@ -46,7 +46,14 @@
|
|
|
46
46
|
<ul>
|
|
47
47
|
<li>Avo <%= Avo::VERSION %></li>
|
|
48
48
|
<% Avo.plugin_manager.plugins.each do |plugin| %>
|
|
49
|
-
<li
|
|
49
|
+
<li>
|
|
50
|
+
<% if plugin.gem_name.present? %>
|
|
51
|
+
<%= link_to plugin.name, "https://avohq.io/gems/#{plugin.gem_name}", target: "_blank", rel: "noopener", class: "underline" %>
|
|
52
|
+
<% else %>
|
|
53
|
+
<%= plugin.name %>
|
|
54
|
+
<% end %>
|
|
55
|
+
- <%= plugin.version %>
|
|
56
|
+
</li>
|
|
50
57
|
<% end %>
|
|
51
58
|
</ul>
|
|
52
59
|
</div>
|
|
@@ -78,24 +78,13 @@ module Avo
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
def find_records_from_resource_ids
|
|
82
|
-
if (ids = action_params[:fields]&.dig(:avo_resource_ids)&.split(",") || []).any?
|
|
83
|
-
@resource.find_record(ids, params: params)
|
|
84
|
-
else
|
|
85
|
-
[]
|
|
86
|
-
end
|
|
87
81
|
# A record may be deleted between the moment it was checked on the index and
|
|
88
|
-
# the moment the action is submitted
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
ids.filter_map do |id|
|
|
95
|
-
@resource.find_record(id, params: params)
|
|
96
|
-
rescue ActiveRecord::RecordNotFound
|
|
97
|
-
nil
|
|
98
|
-
end
|
|
82
|
+
# the moment the action is submitted. Only the records that are really gone drop out.
|
|
83
|
+
def find_records_from_resource_ids
|
|
84
|
+
ids = action_params[:fields]&.dig(:avo_resource_ids)&.split(",") || []
|
|
85
|
+
return [] if ids.empty?
|
|
86
|
+
|
|
87
|
+
@resource.find_records(ids, params: params).compact.uniq
|
|
99
88
|
end
|
|
100
89
|
|
|
101
90
|
def set_fields
|
|
@@ -53,12 +53,7 @@ module Avo
|
|
|
53
53
|
@resource.hydrate(record: @record)
|
|
54
54
|
|
|
55
55
|
if @field.present? && !@field.is_searchable?
|
|
56
|
-
query =
|
|
57
|
-
|
|
58
|
-
# Add the association scope to the query scope
|
|
59
|
-
if @field.attach_scope.present?
|
|
60
|
-
query = Avo::ExecutionContext.new(target: @field.attach_scope, query: query, parent: @record).handle
|
|
61
|
-
end
|
|
56
|
+
query = attachable_query
|
|
62
57
|
|
|
63
58
|
@options = if attach_using_checkbox_list?
|
|
64
59
|
checkbox_list_options(query)
|
|
@@ -100,6 +95,10 @@ module Avo
|
|
|
100
95
|
attach_record(association_name, attachment_record)
|
|
101
96
|
end
|
|
102
97
|
end
|
|
98
|
+
rescue Avo::Fields::BelongsToField::TargetNotFound => error
|
|
99
|
+
# Worded on the join record, which owns the fields; `@record` has no attribute to put them on
|
|
100
|
+
join_record = @reflection.through_reflection.klass.new
|
|
101
|
+
error.field_ids.each { |field_id| @record.errors.add(:base, ActiveModel::Error.new(join_record, field_id, :invalid).full_message) }
|
|
103
102
|
end
|
|
104
103
|
end
|
|
105
104
|
|
|
@@ -150,13 +149,10 @@ module Avo
|
|
|
150
149
|
end
|
|
151
150
|
|
|
152
151
|
def set_attachment_record
|
|
153
|
-
if action_name == "create"
|
|
154
|
-
|
|
155
|
-
@related_resource.find_record id, params: params
|
|
156
|
-
end
|
|
157
|
-
@attachment_record = @attachment_records.first
|
|
152
|
+
@attachment_record = if action_name == "create"
|
|
153
|
+
attachment_records.first
|
|
158
154
|
else
|
|
159
|
-
@
|
|
155
|
+
@related_resource.find_record attachment_id, params: params
|
|
160
156
|
end
|
|
161
157
|
end
|
|
162
158
|
|
|
@@ -174,12 +170,25 @@ module Avo
|
|
|
174
170
|
Array(attachment_id).reject(&:blank?)
|
|
175
171
|
end
|
|
176
172
|
|
|
173
|
+
# Whether the finder raises (`find`) or answers nil (`find_by`), a refused id raises like a missing one
|
|
177
174
|
def attachment_records
|
|
178
|
-
@attachment_records ||=
|
|
179
|
-
|
|
175
|
+
@attachment_records ||= begin
|
|
176
|
+
query = attachable_query
|
|
177
|
+
|
|
178
|
+
attachment_ids.map do |id|
|
|
179
|
+
@related_resource.find_record(id, query:, params:) || raise(ActiveRecord::RecordNotFound, "Couldn't find #{@attachment_class} with '#{id}'")
|
|
180
|
+
end
|
|
180
181
|
end
|
|
181
182
|
end
|
|
182
183
|
|
|
184
|
+
# What the attach picker offers, and so the only records the save accepts
|
|
185
|
+
def attachable_query
|
|
186
|
+
query = @related_authorization.apply_policy @attachment_class
|
|
187
|
+
return query if @field.attach_scope.blank?
|
|
188
|
+
|
|
189
|
+
Avo::ExecutionContext.new(target: @field.attach_scope, query: query, parent: @record).handle
|
|
190
|
+
end
|
|
191
|
+
|
|
183
192
|
def reflection_class
|
|
184
193
|
if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
|
185
194
|
@reflection.through_reflection.class
|
|
@@ -277,15 +286,19 @@ module Avo
|
|
|
277
286
|
end
|
|
278
287
|
|
|
279
288
|
def additional_params
|
|
280
|
-
@additional_params ||= params[:fields].slice(*@attach_fields
|
|
289
|
+
@additional_params ||= params[:fields].slice(*@resource.fields_by_param(Array(@attach_fields)).compact.keys)
|
|
281
290
|
end
|
|
282
291
|
|
|
283
292
|
def set_attach_fields
|
|
284
293
|
@attach_fields = if @field.attach_fields.present?
|
|
294
|
+
# Attach fields describe the join record, so a belongs_to among them finds its association there
|
|
295
|
+
join_record = @reflection.through_reflection&.klass&.new
|
|
296
|
+
|
|
285
297
|
Avo::FieldsExecutionContext.new(target: @field.attach_fields)
|
|
286
298
|
.detect_fields
|
|
287
299
|
.items_holder
|
|
288
300
|
.items
|
|
301
|
+
.each { |field| field.hydrate(record: join_record) }
|
|
289
302
|
end
|
|
290
303
|
end
|
|
291
304
|
|
|
@@ -193,6 +193,9 @@ module Avo
|
|
|
193
193
|
|
|
194
194
|
@record = @resource.fill_record(@record_to_fill, cast_nullable(model_params), extra_params: extra_params)
|
|
195
195
|
assign_default_value_to_disabled_fields if @view.create?
|
|
196
|
+
rescue Avo::Fields::BelongsToField::TargetNotFound => error
|
|
197
|
+
@record = @record_to_fill
|
|
198
|
+
error.field_ids.each { |field_id| @record.errors.add(field_id, :invalid) }
|
|
196
199
|
end
|
|
197
200
|
|
|
198
201
|
def is_attach_action?
|
|
@@ -231,6 +231,9 @@ module Avo
|
|
|
231
231
|
end
|
|
232
232
|
|
|
233
233
|
def save_record
|
|
234
|
+
# Saving runs the validations, which would clear an error that filling the record left on it
|
|
235
|
+
return false if @record.errors.any?
|
|
236
|
+
|
|
234
237
|
perform_action_and_record_errors do
|
|
235
238
|
save_record_action
|
|
236
239
|
end
|
|
@@ -57,6 +57,20 @@ module Avo
|
|
|
57
57
|
# - is_disabled?
|
|
58
58
|
|
|
59
59
|
class BelongsToField < BaseField
|
|
60
|
+
# A value that matches no record the field may attach, which a form shows on the field.
|
|
61
|
+
# A RecordNotFound, so code that already rescues one keeps working.
|
|
62
|
+
class TargetNotFound < ActiveRecord::RecordNotFound
|
|
63
|
+
attr_reader :field_id
|
|
64
|
+
# This field and every other one the same fill could not resolve, so a form shows them all
|
|
65
|
+
attr_accessor :field_ids
|
|
66
|
+
|
|
67
|
+
def initialize(message, model, id, field_id:)
|
|
68
|
+
@field_id = field_id
|
|
69
|
+
@field_ids = [field_id]
|
|
70
|
+
super(message, model, nil, id)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
include Avo::Fields::Concerns::IsSearchable
|
|
61
75
|
include Avo::Fields::Concerns::UseResource
|
|
62
76
|
|
|
@@ -204,6 +218,16 @@ module Avo
|
|
|
204
218
|
if polymorphic_as.present?
|
|
205
219
|
valid_model_class = valid_polymorphic_class params[:"#{polymorphic_as}_type"]
|
|
206
220
|
|
|
221
|
+
# A type sent without its id keeps the current record. A new type can't take the current id, which names a record only
|
|
222
|
+
# under its own type. With no current id (a create through a parent fills it in later), the type is set as before.
|
|
223
|
+
if valid_model_class.present? && !params.key?(id_input_foreign_key)
|
|
224
|
+
return record if valid_model_class.to_s == record.public_send(type_input_foreign_key)
|
|
225
|
+
|
|
226
|
+
if record.public_send(id_input_foreign_key).present?
|
|
227
|
+
raise TargetNotFound.new("Couldn't find #{valid_model_class} without #{id_input_foreign_key}", valid_model_class.name, nil, field_id: id)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
207
231
|
record.send(:"#{polymorphic_as}_type=", valid_model_class)
|
|
208
232
|
|
|
209
233
|
# If the type is blank, reset the id too.
|
|
@@ -331,10 +355,11 @@ module Avo
|
|
|
331
355
|
|
|
332
356
|
private
|
|
333
357
|
|
|
334
|
-
#
|
|
358
|
+
# Whether the finder raises (`find`) or answers nil (`find_by`), the field raises TargetNotFound
|
|
335
359
|
def find_target!(target, record, id)
|
|
336
|
-
target.find_record(id, query: scoped_target_query(target, record, for_find: true)) ||
|
|
337
|
-
|
|
360
|
+
target.find_record(id, query: scoped_target_query(target, record, for_find: true)) || raise(ActiveRecord::RecordNotFound)
|
|
361
|
+
rescue ActiveRecord::RecordNotFound
|
|
362
|
+
raise TargetNotFound.new("Couldn't find #{target.model_class.name} with '#{id}'", target.model_class.name, id, field_id: self.id)
|
|
338
363
|
end
|
|
339
364
|
|
|
340
365
|
def scoped_target_query(resource, parent_record, for_find: false)
|
|
@@ -45,8 +45,9 @@ module Avo
|
|
|
45
45
|
def fill_field(record, key, value, params)
|
|
46
46
|
return record unless record.methods.include? key.to_sym
|
|
47
47
|
|
|
48
|
-
# attach files
|
|
49
|
-
|
|
48
|
+
# `attach` without its immediate save on a persisted record, so the files are saved with the record and a form
|
|
49
|
+
# that fails saves none. One assignment avoids an index_active_storage_attachments_uniqueness violation.
|
|
50
|
+
record.public_send(:"#{key}=", record.public_send(key).blobs + value.compact_blank)
|
|
50
51
|
|
|
51
52
|
record
|
|
52
53
|
end
|
|
@@ -55,12 +55,15 @@ module Avo
|
|
|
55
55
|
|
|
56
56
|
return super(id, query: fetched_records, params:) if is_active_record_relation?(fetched_records)
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
fetched_records.find { |record| record.id.to_s == id.to_s }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def find_records(ids, query: nil, params: nil)
|
|
62
|
+
fetched_records = fetch_records
|
|
63
|
+
|
|
64
|
+
return super(ids, query: fetched_records, params:) if is_active_record_relation?(fetched_records)
|
|
65
|
+
|
|
66
|
+
ids.map { |id| fetched_records.find { |record| record.id.to_s == id.to_s } }
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def fetch_records(array_of_records = nil)
|
data/lib/avo/resources/base.rb
CHANGED
|
@@ -319,6 +319,37 @@ module Avo
|
|
|
319
319
|
).handle
|
|
320
320
|
end
|
|
321
321
|
|
|
322
|
+
# The records `ids` name, each the way `find_record` finds one: by its `to_param`, through
|
|
323
|
+
# `find_record_method`. One entry per id, in order, nil where an id names no record in `query`.
|
|
324
|
+
#
|
|
325
|
+
# The finder is asked for the whole set first, then once per id it did not name: a finder
|
|
326
|
+
# that takes one id at a time, or a missing record.
|
|
327
|
+
def find_records(ids, query: nil, params: nil)
|
|
328
|
+
query ||= find_scope
|
|
329
|
+
ids = ids.map(&:to_s)
|
|
330
|
+
|
|
331
|
+
found = begin
|
|
332
|
+
ids.many? ? Array.wrap(find_record(ids.uniq, query:, params:)).index_by { |record| record.to_param.to_s } : {}
|
|
333
|
+
rescue
|
|
334
|
+
{}
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
(ids - found.keys).each do |id|
|
|
338
|
+
found[id] = find_record(id, query:, params:)
|
|
339
|
+
rescue ActiveRecord::ActiveRecordError, RangeError, ArgumentError, TypeError
|
|
340
|
+
nil
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Avo's own finder reads through `query`. A custom one may ignore it, so only what `query`
|
|
344
|
+
# also holds is kept.
|
|
345
|
+
unless find_record_method.equal?(Avo::Resources::Base.find_record_method)
|
|
346
|
+
in_query = query.where(model_class.primary_key => found.values.compact.map(&:id)).index_by(&:id)
|
|
347
|
+
found.transform_values! { |record| record if in_query.key?(record&.id) }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
ids.map { |id| found[id] }
|
|
351
|
+
end
|
|
352
|
+
|
|
322
353
|
def search_query
|
|
323
354
|
search.dig(:query)
|
|
324
355
|
end
|
|
@@ -335,7 +366,7 @@ module Avo
|
|
|
335
366
|
delegate :plural_name, to: :class
|
|
336
367
|
delegate :underscore_name, to: :class
|
|
337
368
|
delegate :to_param, to: :class
|
|
338
|
-
delegate :find_record, to: :class
|
|
369
|
+
delegate :find_record, :find_records, to: :class
|
|
339
370
|
delegate :model_key, to: :class
|
|
340
371
|
delegate :translation_key, to: :class
|
|
341
372
|
delegate :tab, to: :items_holder
|
|
@@ -646,33 +677,37 @@ module Avo
|
|
|
646
677
|
end
|
|
647
678
|
end
|
|
648
679
|
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
680
|
+
# Every form param, mapped to the field that writes it: each non-computed field's params, and each
|
|
681
|
+
# top-level `extra_params` name, which no field writes (nil). A field hidden from the form keeps its
|
|
682
|
+
# params, as a resource tool can render it. When several fields take one param (a field declared
|
|
683
|
+
# once per view), the one this form shows to this user writes it. `fields` narrows the candidates.
|
|
684
|
+
def fields_by_param(fields = get_field_definitions)
|
|
685
|
+
form = Avo::ViewInquirer.new(view).form?
|
|
686
|
+
shown, hidden = fields.reject(&:computed).partition { |field| form && field.visible_in_view?(view:) && field.visible? }
|
|
687
|
+
extra = Array.wrap(self.class.extra_params).select { |param| param.is_a?(Symbol) || param.is_a?(String) }
|
|
688
|
+
|
|
689
|
+
# Shown fields last, so they win a param they share
|
|
690
|
+
(hidden + shown)
|
|
691
|
+
.flat_map { |field| flatten_keys(Array.wrap(field.to_permitted_param)).map { |param| [param.to_s, field] } }
|
|
659
692
|
.to_h
|
|
693
|
+
.merge(extra.to_h { |param| [param.to_s, nil] }) { |_param, field| field }
|
|
660
694
|
end
|
|
661
695
|
|
|
662
696
|
def fill_record(record, permitted_params, extra_params: [], fields: nil)
|
|
663
|
-
|
|
697
|
+
fields_by_key = fields.present? ? fields_by_param(fields) : fields_by_param
|
|
698
|
+
unresolved = []
|
|
664
699
|
|
|
665
|
-
# Write
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
end
|
|
700
|
+
# Write each field once, whichever of its params arrived. A param left out keeps the record's current value,
|
|
701
|
+
# except a polymorphic id, which its field keeps only under the current type.
|
|
702
|
+
permitted_params.keys.filter_map { |key| fields_by_key[key.to_s] }.uniq.each do |field|
|
|
703
|
+
left_out = fields_by_key.filter_map { |key, owner| key if owner == field && !permitted_params.key?(key) && record.respond_to?(key) && key != field.try(:id_input_foreign_key) }
|
|
704
|
+
params = permitted_params.reverse_merge(left_out.index_with { |key| record.public_send(key) })
|
|
705
|
+
key = field.database_id.to_s
|
|
672
706
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
707
|
+
record = field.fill_field record, key, params[key], params
|
|
708
|
+
rescue Avo::Fields::BelongsToField::TargetNotFound => error
|
|
709
|
+
# Raised once every field is filled, so the form that shows the error keeps the user's other edits
|
|
710
|
+
unresolved << error
|
|
676
711
|
end
|
|
677
712
|
|
|
678
713
|
# Write the user configured extra params to the record
|
|
@@ -685,7 +720,14 @@ module Avo
|
|
|
685
720
|
record.assign_attributes extra_attributes
|
|
686
721
|
end
|
|
687
722
|
|
|
688
|
-
safe_call(:fill_nested_records, record:, permitted_params:) || record
|
|
723
|
+
record = safe_call(:fill_nested_records, record:, permitted_params:) || record
|
|
724
|
+
|
|
725
|
+
if unresolved.any?
|
|
726
|
+
unresolved.first.field_ids = unresolved.map(&:field_id)
|
|
727
|
+
raise unresolved.first
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
record
|
|
689
731
|
end
|
|
690
732
|
|
|
691
733
|
# On the resource so one that isn't backed by a model can build, save and delete its own records,
|
|
@@ -168,6 +168,7 @@ Turning a long `belongs_to` / `has_many` picker into a type-to-search field is t
|
|
|
168
168
|
- **A `belongs_to` field is disabled when you arrive through that same association.** Editing a Comment reached via a Post's comments panel disables the `post` field. Set `allow_via_detaching: true` to re-enable it.
|
|
169
169
|
- **`polymorphic_as` and `types` are a pair** — using one without the other raises. `types` is an array of model classes (`[::Post, ::Project]`), matching a `polymorphic: true` `belongs_to`.
|
|
170
170
|
- **`attach_scope` does not filter listed rows**, only the attach picker. To hide rows from a `has_many`/HABTM table, use `scope` or a Pundit policy scope.
|
|
171
|
+
- **Build `attach_scope` on `query` to keep the policy scope.** `query` arrives already narrowed by the target's policy scope. A fresh relation (`-> { ::User.all }`) widens past it on purpose. On every association the save accepts exactly the records the picker offers, so a record outside the `attach_scope` is refused like a missing one.
|
|
171
172
|
- **STI child resources must set `self.model_class`.** When a parent class and STI children share a table, give each child resource `self.model_class = "SuperUser"` so Avo maps it correctly. To route association rows / index clicks to the child resource instead of the parent, use `self.link_to_child_resource = true` on the parent resource or `link_to_child_resource:` on the field.
|
|
172
173
|
- **Searchable can look "broken" but be misconfigured** — no query source means an empty picker, not an error. And it's a paid add-on.
|
|
173
174
|
|
|
@@ -286,7 +286,7 @@ Search (`self.search`), grid/map view types, record reordering, and i18n live on
|
|
|
286
286
|
- **Two resources, one model → wrong one wins.** Avo resolves the default alphabetically. Set `config.model_resource_mapping` and/or `use_resource:` on associations.
|
|
287
287
|
- **Secondary / namespaced / oddly-named resources need `self.model_class`** (or the matching namespace) or Avo can't infer the model. Namespaced resources whose namespace matches the model's namespace infer automatically.
|
|
288
288
|
- **Array resources are Beta:** no sorting, and `records` re-runs every request. Cache inside `records` for large sets, or move to an HTTP Resource.
|
|
289
|
-
- **`find_record_method` in batch contexts:** `id` arrives as an Array for bulk actions — return a collection (`query.where(...)`) in that branch, not a single record.
|
|
289
|
+
- **`find_record_method` in batch contexts:** `id` arrives as an Array for bulk actions — return a collection (`query.where(...)`) in that branch, not a single record. For every id that branch doesn't return (it raised, as `query.find(id)` does when one record was deleted between selection and submit, or it takes one id at a time), Avo retries the **scalar** branch once per id and drops the ones that are gone — so keep the scalar branch able to handle every id the array branch receives. In a batch, a custom finder's records are kept only when `query` also holds them, so ignoring it can't reach past the policy scope; a single lookup (Show, Edit, `belongs_to`) uses the finder's answer as-is, so keep reading through `query`. In your own code, `Avo::Resources::Post.find_records(ids, query:)` does the same lookup: one entry per id, in order, `nil` where none.
|
|
290
290
|
- **`visible_on_sidebar` only affects the auto-generated menu.** If the app uses the menu editor, control visibility in its `visible` block instead.
|
|
291
291
|
- **System specs that leave an edited form without saving now meet a confirmation dialog.** `config.warn_on_unsaved_changes` is on by default — accept the dialog with `accept_custom_alert { ... }` from `Avo::TestHelpers`, or set the option to `false` in the test environment.
|
|
292
292
|
- **Don't re-invent fields/associations here.** Field DSL is the avo-fields skill; `belongs_to`/`has_many`/`use_resource` is avo-associations.
|
data/lib/avo/version.rb
CHANGED