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