avo 3.10.6 → 3.10.7

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 +2 -2
  3. data/README.md +4 -9
  4. data/app/components/avo/fields/boolean_field/index_component.html.erb +1 -1
  5. data/app/components/avo/fields/boolean_field/show_component.html.erb +1 -1
  6. data/app/components/avo/fields/boolean_group_field/edit_component.rb +1 -1
  7. data/app/components/avo/fields/common/boolean_group_component.html.erb +3 -6
  8. data/app/components/avo/resource_component.rb +0 -2
  9. data/app/components/avo/turbo_frame_wrapper_component.html.erb +1 -1
  10. data/app/controllers/avo/base_controller.rb +9 -2
  11. data/lib/avo/asset_manager.rb +8 -3
  12. data/lib/avo/base_resource.rb +2 -601
  13. data/lib/avo/fields/boolean_group_field.rb +1 -1
  14. data/lib/avo/licensing/request.rb +3 -1
  15. data/lib/avo/resources/base.rb +607 -0
  16. data/lib/avo/resources/items/item_group.rb +2 -0
  17. data/lib/avo/resources/items/tab.rb +2 -0
  18. data/lib/avo/version.rb +1 -1
  19. data/lib/avo.rb +1 -0
  20. data/lib/generators/avo/eject_generator.rb +1 -1
  21. data/lib/generators/avo/templates/locales/avo.de.yml +120 -0
  22. data/lib/generators/avo/templates/locales/avo.it.yml +120 -0
  23. data/lib/generators/avo/templates/locales/avo.nl.yml +120 -0
  24. data/lib/generators/avo/templates/locales/avo.pl.yml +120 -0
  25. data/lib/generators/avo/templates/locales/avo.ru.yml +120 -0
  26. data/lib/generators/avo/templates/locales/avo.uk.yml +120 -0
  27. data/lib/generators/avo/templates/locales/avo.zh.yml +120 -0
  28. data/public/avo-assets/avo.base.css +3 -71
  29. data/public/avo-assets/avo.base.js +230 -227
  30. data/public/avo-assets/avo.base.js.map +4 -4
  31. data/public/avo-assets/logo-on-white.png +0 -0
  32. metadata +10 -2
@@ -0,0 +1,607 @@
1
+ module Avo
2
+ module Resources
3
+ class Base
4
+ extend ActiveSupport::DescendantsTracker
5
+
6
+ include ActionView::Helpers::UrlHelper
7
+ include Avo::Concerns::HasItems
8
+ include Avo::Concerns::CanReplaceItems
9
+ include Avo::Concerns::HasControls
10
+ include Avo::Concerns::HasResourceStimulusControllers
11
+ include Avo::Concerns::ModelClassConstantized
12
+ include Avo::Concerns::HasDescription
13
+ include Avo::Concerns::HasCoverPhoto
14
+ include Avo::Concerns::HasProfilePhoto
15
+ include Avo::Concerns::HasHelpers
16
+ include Avo::Concerns::Hydration
17
+ include Avo::Concerns::Pagination
18
+
19
+ # Avo::Current methods
20
+ delegate :context, to: Avo::Current
21
+ def current_user
22
+ Avo::Current.user
23
+ end
24
+ delegate :params, to: Avo::Current
25
+ delegate :request, to: Avo::Current
26
+ delegate :view_context, to: Avo::Current
27
+
28
+ # view_context methods
29
+ delegate :simple_format, :content_tag, to: :view_context
30
+ delegate :main_app, to: :view_context
31
+ delegate :avo, to: :view_context
32
+ delegate :resource_path, to: :view_context
33
+ delegate :resources_path, to: :view_context
34
+
35
+ # I18n methods
36
+ delegate :t, to: ::I18n
37
+
38
+ # class methods
39
+ delegate :class_name, to: :class
40
+ delegate :route_key, to: :class
41
+ delegate :singular_route_key, to: :class
42
+
43
+ attr_accessor :view
44
+ attr_accessor :reflection
45
+ attr_accessor :user
46
+ attr_accessor :record
47
+
48
+ class_attribute :id, default: :id
49
+ class_attribute :title
50
+ class_attribute :search, default: {}
51
+ class_attribute :includes, default: []
52
+ class_attribute :attachments, default: []
53
+ class_attribute :single_includes, default: []
54
+ class_attribute :single_attachments, default: []
55
+ class_attribute :authorization_policy
56
+ class_attribute :translation_key
57
+ class_attribute :default_view_type, default: :table
58
+ class_attribute :devise_password_optional, default: false
59
+ class_attribute :scopes_loader
60
+ class_attribute :filters_loader
61
+ class_attribute :view_types
62
+ class_attribute :grid_view
63
+ class_attribute :visible_on_sidebar, default: true
64
+ class_attribute :index_query, default: -> {
65
+ query
66
+ }
67
+ class_attribute :find_record_method, default: -> {
68
+ query.find id
69
+ }
70
+ class_attribute :after_create_path, default: :show
71
+ class_attribute :after_update_path, default: :show
72
+ class_attribute :record_selector, default: true
73
+ class_attribute :keep_filters_panel_open, default: false
74
+ class_attribute :extra_params
75
+ class_attribute :link_to_child_resource, default: false
76
+ class_attribute :map_view
77
+ class_attribute :components, default: {}
78
+ class_attribute :default_sort_column, default: :created_at
79
+
80
+ # EXTRACT:
81
+ class_attribute :ordering
82
+
83
+ class << self
84
+ delegate :t, to: ::I18n
85
+ delegate :context, to: ::Avo::Current
86
+
87
+ def action(action_class, arguments: {})
88
+ deprecated_dsl_api __method__, "actions"
89
+ end
90
+
91
+ def filter(filter_class, arguments: {})
92
+ deprecated_dsl_api __method__, "filters"
93
+ end
94
+
95
+ def scope(scope_class)
96
+ deprecated_dsl_api __method__, "scopes"
97
+ end
98
+
99
+ # This resolves the scope when doing "where" queries (not find queries)
100
+ #
101
+ # It's used to apply the authorization feature.
102
+ def query_scope
103
+ authorization.apply_policy Avo::ExecutionContext.new(
104
+ target: index_query,
105
+ query: model_class
106
+ ).handle
107
+ end
108
+
109
+ # This resolves the scope when finding records (not "where" queries)
110
+ #
111
+ # It's used to apply the authorization feature.
112
+ def find_scope
113
+ authorization.apply_policy model_class
114
+ end
115
+
116
+ def authorization
117
+ Avo::Services::AuthorizationService.new Avo::Current.user, model_class, policy_class: authorization_policy
118
+ end
119
+
120
+ def valid_association_name(record, association_name)
121
+ association_name if record._reflections.with_indifferent_access[association_name].present?
122
+ end
123
+
124
+ def valid_attachment_name(record, association_name)
125
+ association_name if record.class.reflect_on_attachment(association_name).present?
126
+ end
127
+
128
+ def get_available_models
129
+ ApplicationRecord.descendants
130
+ end
131
+
132
+ def get_model_by_name(model_name)
133
+ get_available_models.find do |m|
134
+ m.to_s == model_name.to_s
135
+ end
136
+ end
137
+
138
+ # Returns the model class being used for this resource.
139
+ #
140
+ # The Resource instance has a model_class method too so it can support the STI use cases
141
+ # where we figure out the model class from the record
142
+ def model_class(record_class: nil)
143
+ # get the model class off of the static property
144
+ return @model_class if @model_class.present?
145
+
146
+ # get the model class off of the record for STI models
147
+ return record_class if record_class.present?
148
+
149
+ # generate a model class
150
+ class_name.safe_constantize
151
+ end
152
+
153
+ # This is used as the model class ID
154
+ # We use this instead of the route_key to maintain compatibility with uncountable models
155
+ # With uncountable models route key appends an _index suffix (Fish->fish_index)
156
+ # Example: User->users, MediaItem->media_items, Fish->fish
157
+ def model_key
158
+ @model_key ||= model_class.model_name.plural
159
+ end
160
+
161
+ def class_name
162
+ @class_name ||= to_s.demodulize
163
+ end
164
+
165
+ def route_key
166
+ class_name.underscore.pluralize
167
+ end
168
+
169
+ def singular_route_key
170
+ route_key.singularize
171
+ end
172
+
173
+ def translation_key
174
+ @translation_key || "avo.resource_translations.#{class_name.underscore}"
175
+ end
176
+
177
+ def name
178
+ @name ||= name_from_translation_key(count: 1, default: class_name.underscore.humanize)
179
+ end
180
+ alias_method :singular_name, :name
181
+
182
+ def plural_name
183
+ name_from_translation_key(count: 2, default: name.pluralize)
184
+ end
185
+
186
+ # Get the name from the translation_key and fallback to default
187
+ # It can raise I18n::InvalidPluralizationData when using only resource_translation without pluralization keys like: one, two or other key
188
+ # Example:
189
+ # ---
190
+ # en:
191
+ # avo:
192
+ # resource_translations:
193
+ # product:
194
+ # save: "Save product"
195
+ def name_from_translation_key(count:, default:)
196
+ t(translation_key, count:, default:).humanize
197
+ rescue I18n::InvalidPluralizationData
198
+ default
199
+ end
200
+
201
+ def underscore_name
202
+ return @name if @name.present?
203
+
204
+ name.demodulize.underscore
205
+ end
206
+
207
+ def navigation_label
208
+ plural_name.humanize
209
+ end
210
+
211
+ def find_record(id, query: nil, params: nil)
212
+ query ||= find_scope # If no record is given we'll use the default
213
+
214
+ if single_includes.present?
215
+ query = query.includes(*single_includes)
216
+ end
217
+
218
+ if single_attachments.present?
219
+ single_attachments.each do |attachment|
220
+ query = query.send(:"with_attached_#{attachment}")
221
+ end
222
+ end
223
+
224
+ Avo::ExecutionContext.new(
225
+ target: find_record_method,
226
+ query: query,
227
+ id: id,
228
+ params: params
229
+ ).handle
230
+ end
231
+
232
+ def search_query
233
+ search.dig(:query)
234
+ end
235
+
236
+ def search_results_count
237
+ search.dig(:results_count)
238
+ end
239
+
240
+ def fetch_search(key, record: nil)
241
+ # self.class.fetch_search
242
+ Avo::ExecutionContext.new(target: search[key], resource: self, record: record).handle
243
+ end
244
+ end
245
+
246
+ delegate :context, to: ::Avo::Current
247
+ delegate :name, to: :class
248
+ delegate :singular_name, to: :class
249
+ delegate :plural_name, to: :class
250
+ delegate :underscore_name, to: :class
251
+ delegate :to_param, to: :class
252
+ delegate :find_record, to: :class
253
+ delegate :model_key, to: :class
254
+ delegate :tab, to: :items_holder
255
+
256
+ def initialize(record: nil, view: nil, user: nil, params: nil)
257
+ @view = Avo::ViewInquirer.new(view) if view.present?
258
+ @user = user if user.present?
259
+ @params = params if params.present?
260
+
261
+ if record.present?
262
+ @record = record
263
+
264
+ hydrate_model_with_default_values if @view&.new?
265
+ end
266
+
267
+ unless self.class.model_class.present?
268
+ if model_class.present? && model_class.respond_to?(:base_class)
269
+ self.class.model_class = model_class.base_class
270
+ end
271
+ end
272
+ end
273
+
274
+ def detect_fields
275
+ self.items_holder = Avo::Resources::Items::Holder.new(parent: self)
276
+
277
+ # Used in testing to replace items
278
+ if temporary_items.present?
279
+ instance_eval(&temporary_items)
280
+ else
281
+ fetch_fields
282
+ end
283
+
284
+ self
285
+ end
286
+
287
+ VIEW_METHODS_MAPPING = {
288
+ index: [:index_fields, :display_fields],
289
+ show: [:show_fields, :display_fields],
290
+ edit: [:edit_fields, :form_fields],
291
+ update: [:edit_fields, :form_fields],
292
+ new: [:new_fields, :form_fields],
293
+ create: [:new_fields, :form_fields]
294
+ } unless defined? VIEW_METHODS_MAPPING
295
+
296
+ def fetch_fields
297
+ possible_methods_for_view = VIEW_METHODS_MAPPING[view.to_sym]
298
+
299
+ # Safe navigation operator is used because the view can be "destroy" or "preview"
300
+ possible_methods_for_view&.each do |method_for_view|
301
+ return send(method_for_view) if respond_to?(method_for_view)
302
+ end
303
+
304
+ fields
305
+ end
306
+
307
+ def fetch_cards
308
+ cards
309
+ end
310
+
311
+ def divider(label = nil)
312
+ entity_loader(:action).use({class: Divider, label: label}.compact)
313
+ end
314
+
315
+ # def fields / def cards
316
+ [:fields, :cards].each do |method_name|
317
+ define_method method_name do
318
+ # Empty method
319
+ end
320
+ end
321
+
322
+ [:action, :filter, :scope].each do |entity|
323
+ plural_entity = entity.to_s.pluralize
324
+
325
+ # def actions / def filters / def scopes
326
+ define_method plural_entity do
327
+ # blank entity method
328
+ end
329
+
330
+ # def action / def filter / def scope
331
+ define_method entity do |entity_class, arguments: {}, icon: nil|
332
+ entity_loader(entity).use({class: entity_class, arguments: arguments, icon: icon}.compact)
333
+ end
334
+
335
+ # def get_actions / def get_filters / def get_scopes
336
+ define_method "get_#{plural_entity}" do
337
+ return entity_loader(entity).bag if entity_loader(entity).present?
338
+
339
+ # ex: @actions_loader = Avo::Loaders::ActionsLoader.new
340
+ instance_variable_set(
341
+ "@#{plural_entity}_loader",
342
+ "Avo::Loaders::#{plural_entity.humanize}Loader".constantize.new
343
+ )
344
+
345
+ send plural_entity
346
+
347
+ entity_loader(entity).bag
348
+ end
349
+
350
+ # def get_action_arguments / def get_filter_arguments / def get_scope_arguments
351
+ define_method "get_#{entity}_arguments" do |entity_class|
352
+ klass = send("get_#{plural_entity}").find { |entity| entity[:class].to_s == entity_class.to_s }
353
+
354
+ raise "Couldn't find '#{entity_class}' in the 'def #{plural_entity}' method on your '#{self.class}' resource." if klass.nil?
355
+
356
+ klass[:arguments]
357
+ end
358
+ end
359
+
360
+ def hydrate(...)
361
+ super(...)
362
+
363
+ if @record.present?
364
+ hydrate_model_with_default_values if @view&.new?
365
+ end
366
+
367
+ self
368
+ end
369
+
370
+ def default_panel_name
371
+ return @params[:related_name].capitalize if @params.present? && @params[:related_name].present?
372
+
373
+ case @view.to_sym
374
+ when :show
375
+ record_title
376
+ when :edit
377
+ record_title
378
+ when :new
379
+ t("avo.create_new_item", item: name.humanize(capitalize: false)).upcase_first
380
+ end
381
+ end
382
+
383
+ # Returns the model class being used for this resource.
384
+ #
385
+ # We use the class method as a fallback but we pass it the record too so it can support the STI use cases
386
+ # where we figure out the model class from that record.
387
+ def model_class
388
+ record_class = @record&.class
389
+
390
+ self.class.model_class record_class: record_class
391
+ end
392
+
393
+ def record_title
394
+ return name if @record.nil?
395
+
396
+ # Get the title from the record if title is not set, try to get the name, title or label, or fallback to the id
397
+ return @record.try(:name) || @record.try(:title) || @record.try(:label) || @record.id if title.nil?
398
+
399
+ # If the title is a symbol, get the value from the record else execute the block/string
400
+ case title
401
+ when Symbol
402
+ @record.send title
403
+ when Proc
404
+ Avo::ExecutionContext.new(target: title, resource: self, record: @record).handle
405
+ end
406
+ end
407
+
408
+ def available_view_types
409
+ if self.class.view_types.present?
410
+ return Array(
411
+ Avo::ExecutionContext.new(
412
+ target: self.class.view_types,
413
+ resource: self,
414
+ record: record
415
+ ).handle
416
+ )
417
+ end
418
+
419
+ view_types = [:table]
420
+
421
+ view_types << :grid if self.class.grid_view.present?
422
+ view_types << :map if map_view.present?
423
+
424
+ view_types
425
+ end
426
+
427
+ def attachment_fields
428
+ get_field_definitions.select do |field|
429
+ [Avo::Fields::FileField, Avo::Fields::FilesField].include? field.class
430
+ end
431
+ end
432
+
433
+ # Map the received params to their actual fields
434
+ def fields_by_database_id
435
+ get_field_definitions
436
+ .reject do |field|
437
+ field.computed
438
+ end
439
+ .map do |field|
440
+ [field.database_id.to_s, field]
441
+ end
442
+ .to_h
443
+ end
444
+
445
+ def fill_record(record, params, extra_params: [])
446
+ # Write the field values
447
+ params.each do |key, value|
448
+ field = fields_by_database_id[key]
449
+
450
+ next unless field.present?
451
+
452
+ record = field.fill_field record, key, value, params
453
+ end
454
+
455
+ # Write the user configured extra params to the record
456
+ if extra_params.present?
457
+ # Let Rails fill in the rest of the params
458
+ record.assign_attributes params.permit(extra_params)
459
+ end
460
+
461
+ record
462
+ end
463
+
464
+ def authorization(user: nil)
465
+ current_user = user || Avo::Current.user
466
+ Avo::Services::AuthorizationService.new(current_user, record || model_class, policy_class: authorization_policy)
467
+ end
468
+
469
+ def file_hash
470
+ content_to_be_hashed = ""
471
+
472
+ resource_path = Rails.root.join("app", "avo", "resources", "#{file_name}.rb").to_s
473
+ if File.file? resource_path
474
+ content_to_be_hashed += File.read(resource_path)
475
+ end
476
+
477
+ # policy file hash
478
+ policy_path = Rails.root.join("app", "policies", "#{file_name.gsub("_resource", "")}_policy.rb").to_s
479
+ if File.file? policy_path
480
+ content_to_be_hashed += File.read(policy_path)
481
+ end
482
+
483
+ Digest::MD5.hexdigest(content_to_be_hashed)
484
+ end
485
+
486
+ def file_name
487
+ @file_name ||= self.class.underscore_name.tr(" ", "_")
488
+ end
489
+
490
+ def cache_hash(parent_record)
491
+ result = [record, file_hash]
492
+
493
+ if parent_record.present?
494
+ result << parent_record
495
+ end
496
+
497
+ result
498
+ end
499
+
500
+ # We will not overwrite any attributes that come pre-filled in the record.
501
+ def hydrate_model_with_default_values
502
+ default_values = get_fields
503
+ .select do |field|
504
+ !field.computed && !field.is_a?(Avo::Fields::HeadingField)
505
+ end
506
+ .map do |field|
507
+ value = field.value
508
+
509
+ if field.type == "belongs_to"
510
+
511
+ reflection = @record._reflections.with_indifferent_access[@params[:via_relation]]
512
+
513
+ if field.polymorphic_as.present? && field.types.map(&:to_s).include?(@params[:via_relation_class])
514
+ # set the value to the actual record
515
+ via_resource = Avo.resource_manager.get_resource_by_model_class(@params[:via_relation_class])
516
+ value = via_resource.find_record(@params[:via_record_id])
517
+ elsif reflection.present? && reflection.foreign_key.present? && field.id.to_s == @params[:via_relation].to_s
518
+ resource = Avo.resource_manager.get_resource_by_model_class params[:via_relation_class]
519
+ record = resource.find_record @params[:via_record_id], params: params
520
+ id_param = reflection.options[:primary_key] || :id
521
+
522
+ value = record.send(id_param)
523
+ end
524
+ end
525
+
526
+ [field, value]
527
+ end
528
+ .to_h
529
+ .select do |_, value|
530
+ value.present?
531
+ end
532
+
533
+ default_values.each do |field, value|
534
+ field.assign_value record: @record, value: value
535
+ end
536
+ end
537
+
538
+ def model_name
539
+ model_class.model_name
540
+ end
541
+
542
+ def singular_model_key
543
+ model_class.model_name.singular
544
+ end
545
+
546
+ def record_path
547
+ resource_path(record: record, resource: self)
548
+ end
549
+
550
+ def records_path
551
+ resources_path(resource: self)
552
+ end
553
+
554
+ def avatar_field
555
+ get_field_definitions.find do |field|
556
+ field.as_avatar.present?
557
+ end
558
+ rescue
559
+ nil
560
+ end
561
+
562
+ def avatar
563
+ return avatar_field.to_image if avatar_field.respond_to? :to_image
564
+
565
+ return avatar_field.value.variant(resize_to_limit: [480, 480]) if avatar_field.type == "file"
566
+
567
+ avatar_field.value
568
+ rescue
569
+ nil
570
+ end
571
+
572
+ def avatar_type
573
+ avatar_field.as_avatar
574
+ rescue
575
+ nil
576
+ end
577
+
578
+ def form_scope
579
+ model_class.base_class.to_s.underscore.downcase
580
+ end
581
+
582
+ def has_record_id?
583
+ record.present? && record_id.present?
584
+ end
585
+
586
+ def id_attribute
587
+ :id
588
+ end
589
+
590
+ def record_id
591
+ record.send(id_attribute)
592
+ end
593
+
594
+ def description_attributes
595
+ {
596
+ view: view,
597
+ resource: self,
598
+ record: record
599
+ }
600
+ end
601
+
602
+ def entity_loader(entity)
603
+ instance_variable_get("@#{entity.to_s.pluralize}_loader")
604
+ end
605
+ end
606
+ end
607
+ end
@@ -5,6 +5,7 @@ class Avo::Resources::Items::ItemGroup
5
5
  include Avo::Concerns::HasItemType
6
6
  include Avo::Concerns::VisibleItems
7
7
  include Avo::Concerns::VisibleInDifferentViews
8
+ include Avo::Concerns::IsVisible
8
9
 
9
10
  attr_reader :name
10
11
  attr_reader :description
@@ -17,6 +18,7 @@ class Avo::Resources::Items::ItemGroup
17
18
  @description = description
18
19
  @items_holder = Avo::Resources::Items::Holder.new
19
20
  @args = args
21
+ @visible = args[:visible]
20
22
 
21
23
  post_initialize if respond_to?(:post_initialize)
22
24
  end
@@ -4,6 +4,7 @@ class Avo::Resources::Items::Tab
4
4
  include Avo::Concerns::HasItems
5
5
  include Avo::Concerns::HasItemType
6
6
  include Avo::Concerns::VisibleItems
7
+ include Avo::Concerns::IsVisible
7
8
  include Avo::Concerns::VisibleInDifferentViews
8
9
 
9
10
  delegate :items, :add_item, to: :items_holder
@@ -16,6 +17,7 @@ class Avo::Resources::Items::Tab
16
17
  @items_holder = Avo::Resources::Items::Holder.new
17
18
  @view = Avo::ViewInquirer.new view
18
19
  @args = args
20
+ @visible = args[:visible]
19
21
 
20
22
  post_initialize if respond_to?(:post_initialize)
21
23
  end
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "3.10.6" unless const_defined?(:VERSION)
2
+ VERSION = "3.10.7" unless const_defined?(:VERSION)
3
3
  end
data/lib/avo.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "zeitwerk"
2
2
  require "ostruct"
3
+ require "net/http"
3
4
  require_relative "avo/version"
4
5
  require_relative "avo/engine" if defined?(Rails)
5
6
 
@@ -146,7 +146,7 @@ module Generators
146
146
  [dest_rb, dest_erb].each do |path|
147
147
  if component.starts_with?("avo/views/")
148
148
  modified_content = File.read(path).gsub("Avo::Views::", "Avo::Views::#{options[:scope].camelize}::")
149
- elsif component.starts_with?("avo/fields/")
149
+ elsif component.starts_with?("avo/fields/") && options["field-components"].present?
150
150
  modified_content = File.read(path).gsub("#{options["field-components"].camelize}Field", "#{options[:scope].camelize}::#{options["field-components"].camelize}Field")
151
151
  end
152
152