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.
- checksums.yaml +4 -4
- data/Gemfile +1 -2
- data/Gemfile.lock +2 -5
- data/app/components/avo/actions_component.html.erb +1 -1
- data/app/components/avo/actions_component.rb +4 -4
- data/app/components/avo/base_component.rb +2 -1
- data/app/components/avo/field_wrapper_component.rb +7 -1
- data/app/components/avo/fields/has_one_field/show_component.rb +1 -2
- data/app/components/avo/filters_component.html.erb +1 -1
- data/app/components/avo/filters_component.rb +2 -2
- data/app/components/avo/sidebar_profile_component.rb +3 -1
- data/app/components/avo/views/resource_show_component.html.erb +1 -1
- data/app/controllers/avo/base_controller.rb +14 -7
- data/app/helpers/avo/application_helper.rb +5 -1
- data/app/javascript/js/controllers/search_controller.js +11 -2
- data/app/views/avo/partials/_branding.html.erb +1 -0
- data/bin/init +11 -1
- data/bin/test +1 -0
- data/db/factories.rb +5 -0
- data/lib/avo/base_action.rb +17 -7
- data/lib/avo/concerns/filters_session_handler.rb +43 -0
- data/lib/avo/configuration/branding.rb +9 -1
- data/lib/avo/configuration.rb +3 -0
- data/lib/avo/engine.rb +5 -0
- data/lib/avo/fields/base_field.rb +20 -11
- data/lib/avo/fields/concerns/has_default.rb +17 -0
- data/lib/avo/fields/concerns/is_readonly.rb +1 -1
- data/lib/avo/fields/concerns/is_required.rb +2 -2
- data/lib/avo/fields/key_value_field.rb +1 -1
- data/lib/avo/filters/base_filter.rb +17 -2
- data/lib/avo/hosts/resource_view_record_host.rb +7 -0
- data/lib/avo/hosts/visibility_host.rb +11 -0
- data/lib/avo/resources/controls/action.rb +1 -3
- data/lib/avo/services/authorization_clients/nil_client.rb +37 -0
- data/lib/avo/services/authorization_service.rb +11 -1
- data/lib/avo/version.rb +1 -1
- data/lib/avo.rb +2 -0
- data/lib/generators/avo/resource_generator.rb +281 -0
- data/lib/generators/avo/templates/action.tt +3 -0
- data/lib/generators/avo/templates/filters/boolean_filter.tt +3 -0
- data/lib/generators/avo/templates/filters/multiple_select_filter.tt +3 -0
- data/lib/generators/avo/templates/filters/select_filter.tt +3 -0
- data/lib/generators/avo/templates/filters/text_filter.tt +3 -0
- data/lib/generators/avo/templates/initializer/avo.tt +4 -0
- data/lib/generators/avo/templates/resource/controller.tt +1 -1
- data/lib/generators/avo/templates/resource/resource.tt +2 -2
- data/lib/generators/model_generator.rb +10 -0
- data/lib/generators/rails/avo_resource_generator.rb +11 -0
- data/public/avo-assets/avo.base.js +1 -1
- data/public/avo-assets/avo.base.js.map +2 -2
- data/public/avo-assets/favicon.ico +0 -0
- metadata +12 -5
- data/config/master.key +0 -1
|
@@ -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
|
|
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
data/lib/avo.rb
CHANGED
|
@@ -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,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,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
|