avo 2.18.1.pre.1.eagerloaddirs → 2.19.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -2
  3. data/Gemfile.lock +2 -5
  4. data/app/components/avo/actions_component.html.erb +1 -1
  5. data/app/components/avo/actions_component.rb +4 -4
  6. data/app/components/avo/base_component.rb +2 -1
  7. data/app/components/avo/field_wrapper_component.rb +7 -1
  8. data/app/components/avo/fields/has_one_field/show_component.rb +1 -2
  9. data/app/components/avo/filters_component.html.erb +1 -1
  10. data/app/components/avo/filters_component.rb +2 -2
  11. data/app/components/avo/sidebar_profile_component.rb +3 -1
  12. data/app/components/avo/views/resource_show_component.html.erb +1 -1
  13. data/app/controllers/avo/base_controller.rb +14 -7
  14. data/app/helpers/avo/application_helper.rb +5 -1
  15. data/app/javascript/js/controllers/search_controller.js +11 -2
  16. data/app/views/avo/partials/_branding.html.erb +1 -0
  17. data/bin/init +11 -1
  18. data/bin/test +1 -0
  19. data/db/factories.rb +5 -0
  20. data/lib/avo/base_action.rb +17 -7
  21. data/lib/avo/concerns/filters_session_handler.rb +43 -0
  22. data/lib/avo/configuration/branding.rb +9 -1
  23. data/lib/avo/configuration.rb +3 -0
  24. data/lib/avo/engine.rb +5 -0
  25. data/lib/avo/fields/base_field.rb +20 -11
  26. data/lib/avo/fields/concerns/has_default.rb +17 -0
  27. data/lib/avo/fields/concerns/is_readonly.rb +1 -1
  28. data/lib/avo/fields/concerns/is_required.rb +2 -2
  29. data/lib/avo/fields/key_value_field.rb +1 -1
  30. data/lib/avo/filters/base_filter.rb +17 -2
  31. data/lib/avo/hosts/resource_view_record_host.rb +7 -0
  32. data/lib/avo/hosts/visibility_host.rb +11 -0
  33. data/lib/avo/resources/controls/action.rb +1 -3
  34. data/lib/avo/services/authorization_clients/nil_client.rb +37 -0
  35. data/lib/avo/services/authorization_service.rb +11 -1
  36. data/lib/avo/version.rb +1 -1
  37. data/lib/avo.rb +2 -0
  38. data/lib/generators/avo/resource_generator.rb +281 -0
  39. data/lib/generators/avo/templates/action.tt +3 -0
  40. data/lib/generators/avo/templates/filters/boolean_filter.tt +3 -0
  41. data/lib/generators/avo/templates/filters/multiple_select_filter.tt +3 -0
  42. data/lib/generators/avo/templates/filters/select_filter.tt +3 -0
  43. data/lib/generators/avo/templates/filters/text_filter.tt +3 -0
  44. data/lib/generators/avo/templates/initializer/avo.tt +4 -0
  45. data/lib/generators/avo/templates/resource/controller.tt +1 -1
  46. data/lib/generators/avo/templates/resource/resource.tt +2 -2
  47. data/lib/generators/model_generator.rb +10 -0
  48. data/lib/generators/rails/avo_resource_generator.rb +11 -0
  49. data/public/avo-assets/avo.base.js +1 -1
  50. data/public/avo-assets/avo.base.js.map +2 -2
  51. data/public/avo-assets/favicon.ico +0 -0
  52. metadata +12 -5
  53. data/config/master.key +0 -1
@@ -14,9 +14,7 @@ module Avo
14
14
  end
15
15
 
16
16
  def action
17
- return @instance if @instance.present?
18
-
19
- @instance = @klass.new(model: @model, resource: @resource, view: @view)
17
+ @instance ||= @klass.new(model: @model, resource: @resource, view: @view)
20
18
  end
21
19
 
22
20
  def path
@@ -0,0 +1,37 @@
1
+ module Avo
2
+ module Services
3
+ module AuthorizationClients
4
+ class NilClient
5
+ def authorize(user, record, action, policy_class: nil)
6
+ true
7
+ end
8
+
9
+ def policy(user, record)
10
+ NilPolicy.new
11
+ end
12
+
13
+ def policy!(user, record)
14
+ NilPolicy.new
15
+ end
16
+
17
+ def apply_policy(user, model, policy_class: nil)
18
+ model
19
+ end
20
+
21
+ class NilPolicy
22
+ def initialize(user = nil, record = nil)
23
+ end
24
+ # rubocop:enable Style/RedundantInitialize
25
+
26
+ def method_missing(method, *args, &block)
27
+ self
28
+ end
29
+
30
+ def respond_to_missing?
31
+ true
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -9,8 +9,12 @@ module Avo
9
9
  def client
10
10
  client = Avo.configuration.authorization_client
11
11
 
12
+ client = nil if Avo::App.license.lacks(:authorization)
13
+
12
14
  klass = case client
13
- when :pundit, nil
15
+ when nil
16
+ nil_client
17
+ when :pundit
14
18
  pundit_client
15
19
  else
16
20
  if client.is_a?(String)
@@ -92,8 +96,14 @@ module Avo
92
96
  end
93
97
 
94
98
  def pundit_client
99
+ raise Avo::MissingGemError.new("Please add `gem 'pundit'` to your Gemfile.") unless defined?(Pundit)
100
+
95
101
  Avo::Services::AuthorizationClients::PunditClient
96
102
  end
103
+
104
+ def nil_client
105
+ Avo::Services::AuthorizationClients::NilClient
106
+ end
97
107
  end
98
108
 
99
109
  def initialize(user = nil, record = nil, policy_class: nil)
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "2.18.1.pre.1.eagerloaddirs" unless const_defined?(:VERSION)
2
+ VERSION = "2.19.0" unless const_defined?(:VERSION)
3
3
  end
data/lib/avo.rb CHANGED
@@ -57,6 +57,8 @@ module Avo
57
57
  class NotAuthorizedError < StandardError; end
58
58
 
59
59
  class NoPolicyError < StandardError; end
60
+
61
+ class MissingGemError < StandardError; end
60
62
  end
61
63
 
62
64
  loader.eager_load
@@ -7,6 +7,11 @@ module Generators
7
7
 
8
8
  namespace "avo:resource"
9
9
 
10
+ class_option "model-class",
11
+ type: :string,
12
+ required: false,
13
+ desc: "The name of the model."
14
+
10
15
  def create
11
16
  template "resource/resource.tt", "app/avo/resources/#{resource_name}.rb"
12
17
  template "resource/controller.tt", "app/controllers/avo/#{controller_name}.rb"
@@ -33,6 +38,282 @@ module Generators
33
38
  model.capitalize.singularize.camelize
34
39
  end
35
40
  end
41
+
42
+ def class_from_args
43
+ @class_from_args ||= options["model-class"]&.capitalize
44
+ end
45
+
46
+ def model_class_from_args
47
+ "\n self.model_class = ::#{class_from_args}" if class_from_args.present?
48
+ end
49
+
50
+ private
51
+
52
+ def model_class
53
+ @model_class ||= class_from_args || singular_name
54
+ end
55
+
56
+ def model
57
+ @model ||= model_class.classify.safe_constantize
58
+ end
59
+
60
+ def model_db_columns
61
+ @model_db_columns ||= model.columns_hash.reject { |name, _| db_columns_to_ignore.include? name }
62
+ end
63
+
64
+ def db_columns_to_ignore
65
+ %w[id encrypted_password reset_password_token reset_password_sent_at remember_created_at created_at updated_at]
66
+ end
67
+
68
+ def reflections
69
+ @reflections ||= model.reflections.reject do |name, _|
70
+ reflections_sufixes_to_ignore.include?(name.split("_").pop) || reflections_to_ignore.include?(name)
71
+ end
72
+ end
73
+
74
+ def reflections_sufixes_to_ignore
75
+ %w[blob blobs tags]
76
+ end
77
+
78
+ def reflections_to_ignore
79
+ %w[taggings]
80
+ end
81
+
82
+ def attachments
83
+ @attachments ||= reflections.select do |_, reflection|
84
+ reflection.options[:class_name] == "ActiveStorage::Attachment"
85
+ end
86
+ end
87
+
88
+ def tags
89
+ @tags ||= reflections.select { |_, reflection| reflection.options[:as] == :taggable }
90
+ end
91
+
92
+ def associations
93
+ @associations ||= reflections.reject { |key| attachments.key?(key) || tags.key?(key) }
94
+ end
95
+
96
+ def fields
97
+ @fields ||= {}
98
+ end
99
+
100
+ def invoked_by_model_generator?
101
+ @options.dig("from_model_generator")
102
+ end
103
+
104
+ def generate_fields
105
+ return generate_fields_from_args if invoked_by_model_generator?
106
+
107
+ if model.blank?
108
+ puts "Can't generate fields from model. '#{model_class}.rb' not found!"
109
+ return
110
+ end
111
+
112
+ fields_from_model_db_columns
113
+ fields_from_model_enums
114
+ fields_from_model_attachements
115
+ fields_from_model_associations
116
+ fields_from_model_tags
117
+
118
+ generated_fields_template
119
+ end
120
+
121
+ def generated_fields_template
122
+ return if fields.blank?
123
+
124
+ fields_string = "\n # Fields generated from the model"
125
+
126
+ fields.each do |field_name, field_options|
127
+ options = ""
128
+ field_options[:options].each { |k, v| options += ", #{k}: #{v}" } if field_options[:options].present?
129
+
130
+ fields_string += "\n #{field_string field_name, field_options[:field], options}"
131
+ end
132
+
133
+ fields_string
134
+ end
135
+
136
+ def field_string(name, type, options)
137
+ "field :#{name}, as: :#{type}#{options}"
138
+ end
139
+
140
+ def generate_fields_from_args
141
+ @args.each do |arg|
142
+ name, type = arg.split(":")
143
+ type = "string" if type.blank?
144
+ fields[name] = field(name, type.to_sym)
145
+ end
146
+
147
+ generated_fields_template
148
+ end
149
+
150
+ def fields_from_model_tags
151
+ tags.each do |name, _|
152
+ fields[(remove_last_word_from name).pluralize] = {field: "tags"}
153
+ end
154
+ end
155
+
156
+ def fields_from_model_associations
157
+ associations.each do |name, association|
158
+ fields[name] = associations_mapping[association.class]
159
+
160
+ if association.is_a? ActiveRecord::Reflection::ThroughReflection
161
+ fields[name][:options][:through] = ":#{association.options[:through]}"
162
+ end
163
+ end
164
+ end
165
+
166
+ def fields_from_model_attachements
167
+ attachments.each do |name, attachment|
168
+ fields[remove_last_word_from name] = attachments_mapping[attachment.class]
169
+ end
170
+ end
171
+
172
+ # "hello_world_hehe".split('_') => ['hello', 'world', 'hehe']
173
+ # ['hello', 'world', 'hehe'].pop => ['hello', 'world']
174
+ # ['hello', 'world'].join('_') => "hello_world"
175
+ def remove_last_word_from(snake_case_string)
176
+ snake_case_string = snake_case_string.split("_")
177
+ snake_case_string.pop
178
+ snake_case_string.join("_")
179
+ end
180
+
181
+ def fields_from_model_enums
182
+ model.defined_enums.each_key do |enum|
183
+ fields[enum] = {
184
+ field: "select",
185
+ options: {
186
+ enum: "::#{model_class.capitalize}.#{enum.pluralize}"
187
+ }
188
+ }
189
+ end
190
+ end
191
+
192
+ def fields_from_model_db_columns
193
+ model_db_columns.each do |name, data|
194
+ fields[name] = field(name, data.type)
195
+ end
196
+ end
197
+
198
+ def field(name, type)
199
+ names_mapping[name.to_sym] || fields_mapping[type.to_sym] || {field: "text"}
200
+ end
201
+
202
+ def associations_mapping
203
+ {
204
+ ActiveRecord::Reflection::BelongsToReflection => {
205
+ field: "belongs_to"
206
+ },
207
+ ActiveRecord::Reflection::HasOneReflection => {
208
+ field: "has_one"
209
+ },
210
+ ActiveRecord::Reflection::HasManyReflection => {
211
+ field: "has_many"
212
+ },
213
+ ActiveRecord::Reflection::ThroughReflection => {
214
+ field: "has_many",
215
+ options: {
216
+ through: ":..."
217
+ }
218
+ },
219
+ ActiveRecord::Reflection::HasAndBelongsToManyReflection => {
220
+ field: "has_and_belongs_to_many"
221
+ }
222
+ }
223
+ end
224
+
225
+ def attachments_mapping
226
+ {
227
+ ActiveRecord::Reflection::HasOneReflection => {
228
+ field: "file"
229
+ },
230
+ ActiveRecord::Reflection::HasManyReflection => {
231
+ field: "files"
232
+ }
233
+ }
234
+ end
235
+
236
+ def names_mapping
237
+ {
238
+ id: {
239
+ field: "id"
240
+ },
241
+ description: {
242
+ field: "textarea"
243
+ },
244
+ gravatar: {
245
+ field: "gravatar"
246
+ },
247
+ email: {
248
+ field: "text"
249
+ },
250
+ password: {
251
+ field: "password"
252
+ },
253
+ password_confirmation: {
254
+ field: "password"
255
+ },
256
+ stage: {
257
+ field: "select"
258
+ },
259
+ budget: {
260
+ field: "currency"
261
+ },
262
+ money: {
263
+ field: "currency"
264
+ },
265
+ country: {
266
+ field: "country"
267
+ }
268
+ }
269
+ end
270
+
271
+ def fields_mapping
272
+ {
273
+ primary_key: {
274
+ field: "id"
275
+ },
276
+ string: {
277
+ field: "text"
278
+ },
279
+ text: {
280
+ field: "textarea"
281
+ },
282
+ integer: {
283
+ field: "number"
284
+ },
285
+ float: {
286
+ field: "number"
287
+ },
288
+ decimal: {
289
+ field: "number"
290
+ },
291
+ datetime: {
292
+ field: "datetime"
293
+ },
294
+ timestamp: {
295
+ field: "datetime"
296
+ },
297
+ time: {
298
+ field: "datetime"
299
+ },
300
+ date: {
301
+ field: "date"
302
+ },
303
+ binary: {
304
+ field: "number"
305
+ },
306
+ boolean: {
307
+ field: "boolean"
308
+ },
309
+ references: {
310
+ field: "belongs_to"
311
+ },
312
+ json: {
313
+ field: "code"
314
+ }
315
+ }
316
+ end
36
317
  end
37
318
  end
38
319
  end
@@ -1,5 +1,8 @@
1
1
  class <%= class_name.camelize %> < Avo::BaseAction
2
2
  self.name = "<%= name.underscore.humanize %>"
3
+ # self.visible = -> do
4
+ # true
5
+ # end
3
6
 
4
7
  def handle(**args)
5
8
  models, fields, current_user, resource = args.values_at(:models, :fields, :current_user, :resource)
@@ -1,5 +1,8 @@
1
1
  class <%= class_name.camelize %> < Avo::Filters::BooleanFilter
2
2
  self.name = "<%= name.underscore.humanize %>"
3
+ # self.visible = -> do
4
+ # true
5
+ # end
3
6
 
4
7
  def apply(request, query, values)
5
8
  query
@@ -1,5 +1,8 @@
1
1
  class <%= class_name.camelize %> < Avo::Filters::MultipleSelectFilter
2
2
  self.name = "<%= name.underscore.humanize %>"
3
+ # self.visible = -> do
4
+ # true
5
+ # end
3
6
 
4
7
  def apply(request, query, value)
5
8
  query
@@ -1,5 +1,8 @@
1
1
  class <%= class_name.camelize %> < Avo::Filters::SelectFilter
2
2
  self.name = "<%= name.underscore.humanize %>"
3
+ # self.visible = -> do
4
+ # true
5
+ # end
3
6
 
4
7
  def apply(request, query, value)
5
8
  query
@@ -1,6 +1,9 @@
1
1
  class <%= class_name.camelize %> < Avo::Filters::TextFilter
2
2
  self.name = "<%= name.underscore.humanize %>"
3
3
  self.button_label = 'Filter by <%= class_name.underscore.humanize.downcase.gsub(' filter', '') %>'
4
+ # self.visible = -> do
5
+ # true
6
+ # end
4
7
 
5
8
  def apply(request, query, value)
6
9
  query
@@ -44,6 +44,10 @@ Avo.configure do |config|
44
44
  # config.via_per_page = 8
45
45
  # config.id_links_to_resource = false
46
46
  # config.cache_resources_on_index_view = true
47
+ ## permanent enable or disable cache_resource_filters, default value is false
48
+ # config.cache_resource_filters = false
49
+ ## provide a lambda to enable or disable cache_resource_filters per user/resource.
50
+ # config.cache_resource_filters = ->(current_user:, resource:) { current_user.cache_resource_filters?}
47
51
 
48
52
  ## == Customization ==
49
53
  # config.app_name = 'Avocadelicious'
@@ -1,4 +1,4 @@
1
1
  # This controller has been generated to enable Rails' resource routes.
2
- # You shouldn't need to modify it in order to use Avo.
2
+ # More information on https://docs.avohq.io/2.0/controllers.html
3
3
  class <%= controller_class %> < Avo::ResourcesController
4
4
  end
@@ -1,10 +1,10 @@
1
1
  class <%= resource_class %> < Avo::BaseResource
2
2
  self.title = :id
3
- self.includes = []
3
+ self.includes = []<%= model_class_from_args %>
4
4
  # self.search_query = -> do
5
5
  # scope.ransack(id_eq: params[:q], m: "or").result(distinct: false)
6
6
  # end
7
7
 
8
- field :id, as: :id
8
+ field :id, as: :id<%= generate_fields %>
9
9
  # add fields here
10
10
  end
@@ -0,0 +1,10 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/rails/model/model_generator'
3
+
4
+ module Rails
5
+ module Generators
6
+ class ModelGenerator
7
+ hook_for :avo_resource, type: :boolean, default: true unless ARGV.include?("--skip-avo-resource")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'rails/generators/active_record/model/model_generator'
2
+
3
+ module Rails
4
+ module Generators
5
+ class AvoResourceGenerator < ::Rails::Generators::Base
6
+ def invoke_avo_command
7
+ invoke "avo:resource", @args, {from_model_generator: true}
8
+ end
9
+ end
10
+ end
11
+ end