avo 3.10.6 → 3.10.7

Sign up to get free protection for your applications and to get access to all the features.
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