madmin 1.0.1 → 1.2.1
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/README.md +29 -1
- data/app/controllers/madmin/base_controller.rb +0 -5
- data/app/controllers/madmin/resource_controller.rb +23 -2
- data/app/helpers/madmin/application_helper.rb +4 -0
- data/app/helpers/madmin/nav_helper.rb +30 -0
- data/app/helpers/madmin/sort_helper.rb +32 -0
- data/app/views/layouts/madmin/application.html.erb +5 -5
- data/app/views/madmin/application/_form.html.erb +6 -8
- data/app/views/madmin/application/_javascript.html.erb +73 -7
- data/app/views/madmin/application/_navigation.html.erb +31 -5
- data/app/views/madmin/application/edit.html.erb +5 -1
- data/app/views/madmin/application/index.html.erb +34 -22
- data/app/views/madmin/application/new.html.erb +5 -1
- data/app/views/madmin/application/show.html.erb +24 -17
- data/app/views/madmin/fields/attachment/_form.html.erb +3 -1
- data/app/views/madmin/fields/attachment/_show.html.erb +7 -1
- data/app/views/madmin/fields/attachments/_form.html.erb +3 -1
- data/app/views/madmin/fields/attachments/_show.html.erb +7 -3
- data/app/views/madmin/fields/belongs_to/_form.html.erb +4 -2
- data/app/views/madmin/fields/belongs_to/_show.html.erb +1 -1
- data/app/views/madmin/fields/boolean/_form.html.erb +3 -1
- data/app/views/madmin/fields/date/_form.html.erb +3 -1
- data/app/views/madmin/fields/date_time/_form.html.erb +3 -1
- data/app/views/madmin/fields/decimal/_form.html.erb +3 -1
- data/app/views/madmin/fields/enum/_form.html.erb +4 -2
- data/app/views/madmin/fields/float/_form.html.erb +3 -1
- data/app/views/madmin/fields/has_many/_form.html.erb +4 -2
- data/app/views/madmin/fields/has_many/_show.html.erb +1 -1
- data/app/views/madmin/fields/has_one/_form.html.erb +3 -2
- data/app/views/madmin/fields/integer/_form.html.erb +3 -1
- data/app/views/madmin/fields/json/_form.html.erb +3 -1
- data/app/views/madmin/fields/nested_has_many/_fields.html.erb +18 -0
- data/app/views/madmin/fields/nested_has_many/_form.html.erb +32 -0
- data/app/views/madmin/fields/nested_has_many/_index.html.erb +1 -0
- data/app/views/madmin/fields/nested_has_many/_show.html.erb +5 -0
- data/app/views/madmin/fields/password/_form.html.erb +4 -0
- data/app/views/madmin/fields/password/_index.html.erb +1 -0
- data/app/views/madmin/fields/password/_show.html.erb +1 -0
- data/app/views/madmin/fields/polymorphic/_form.html.erb +3 -1
- data/app/views/madmin/fields/polymorphic/_show.html.erb +1 -1
- data/app/views/madmin/fields/rich_text/_form.html.erb +3 -1
- data/app/views/madmin/fields/string/_form.html.erb +3 -1
- data/app/views/madmin/fields/text/_form.html.erb +3 -1
- data/app/views/madmin/fields/time/_form.html.erb +3 -1
- data/app/views/madmin/shared/_label.html.erb +4 -0
- data/lib/generators/madmin/field/field_generator.rb +31 -0
- data/lib/generators/madmin/field/templates/_form.html.erb +2 -0
- data/lib/generators/madmin/field/templates/_index.html.erb +1 -0
- data/lib/generators/madmin/field/templates/_show.html.erb +1 -0
- data/lib/generators/madmin/field/templates/field.rb.tt +26 -0
- data/lib/generators/madmin/install/install_generator.rb +6 -1
- data/lib/generators/madmin/install/templates/routes.rb.tt +3 -0
- data/lib/generators/madmin/resource/resource_generator.rb +15 -4
- data/lib/generators/madmin/resource/templates/controller.rb.tt +6 -0
- data/lib/generators/madmin/resource/templates/resource.rb.tt +14 -0
- data/lib/generators/madmin/views/javascript_generator.rb +15 -0
- data/lib/generators/madmin/views/views_generator.rb +6 -5
- data/lib/madmin/engine.rb +1 -1
- data/lib/madmin/field.rb +4 -0
- data/lib/madmin/fields/belongs_to.rb +9 -5
- data/lib/madmin/fields/has_many.rb +10 -5
- data/lib/madmin/fields/nested_has_many.rb +40 -0
- data/lib/madmin/fields/password.rb +6 -0
- data/lib/madmin/fields/string.rb +3 -0
- data/lib/madmin/fields/text.rb +3 -0
- data/lib/madmin/generator_helpers.rb +22 -4
- data/lib/madmin/resource.rb +53 -19
- data/lib/madmin/search.rb +60 -0
- data/lib/madmin/version.rb +1 -1
- data/lib/madmin.rb +30 -14
- metadata +25 -5
@@ -8,4 +8,18 @@ class <%= class_name %>Resource < Madmin::Resource
|
|
8
8
|
<% associations.each do |association_name| -%>
|
9
9
|
attribute :<%= association_name %>
|
10
10
|
<% end -%>
|
11
|
+
|
12
|
+
# Uncomment this to customize the display name of records in the admin area.
|
13
|
+
# def self.display_name(record)
|
14
|
+
# record.name
|
15
|
+
# end
|
16
|
+
|
17
|
+
# Uncomment this to customize the default sort column and direction.
|
18
|
+
# def self.default_sort_column
|
19
|
+
# "created_at"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def self.default_sort_direction
|
23
|
+
# "desc"
|
24
|
+
# end
|
11
25
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "madmin/view_generator"
|
2
|
+
|
3
|
+
module Madmin
|
4
|
+
module Generators
|
5
|
+
module Views
|
6
|
+
class JavascriptGenerator < Madmin::ViewGenerator
|
7
|
+
source_root template_source_path
|
8
|
+
|
9
|
+
def copy_navigation
|
10
|
+
copy_resource_template("_javascript")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -4,11 +4,12 @@ module Madmin
|
|
4
4
|
module Generators
|
5
5
|
class ViewsGenerator < Madmin::ViewGenerator
|
6
6
|
def copy_templates
|
7
|
-
|
8
|
-
call_generator("
|
9
|
-
call_generator("
|
10
|
-
call_generator("
|
11
|
-
call_generator("
|
7
|
+
# Some generators duplicate templates, so not everything is present here
|
8
|
+
call_generator("madmin:views:edit", resource_path, "--namespace", namespace)
|
9
|
+
call_generator("madmin:views:index", resource_path, "--namespace", namespace)
|
10
|
+
call_generator("madmin:views:layout", resource_path, "--namespace", namespace)
|
11
|
+
call_generator("madmin:views:new", resource_path, "--namespace", namespace)
|
12
|
+
call_generator("madmin:views:show", resource_path, "--namespace", namespace)
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
data/lib/madmin/engine.rb
CHANGED
data/lib/madmin/field.rb
CHANGED
@@ -2,17 +2,21 @@ module Madmin
|
|
2
2
|
module Fields
|
3
3
|
class BelongsTo < Field
|
4
4
|
def options_for_select(record)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
[
|
5
|
+
if (record = record.send(attribute_name))
|
6
|
+
resource = Madmin.resource_for(record)
|
7
|
+
[[resource.display_name(record), record.id]]
|
8
|
+
else
|
9
|
+
[]
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
13
|
def to_param
|
14
14
|
"#{attribute_name}_id"
|
15
15
|
end
|
16
|
+
|
17
|
+
def index_path
|
18
|
+
Madmin.resource_by_name(model.reflect_on_association(attribute_name).klass).index_path(format: :json)
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
18
22
|
end
|
@@ -2,17 +2,22 @@ module Madmin
|
|
2
2
|
module Fields
|
3
3
|
class HasMany < Field
|
4
4
|
def options_for_select(record)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
if (records = record.send(attribute_name))
|
6
|
+
return [] unless records.first
|
7
|
+
resource = Madmin.resource_for(records.first)
|
8
|
+
records.map { |record| [resource.display_name(record), record.id] }
|
9
|
+
else
|
10
|
+
[]
|
10
11
|
end
|
11
12
|
end
|
12
13
|
|
13
14
|
def to_param
|
14
15
|
{"#{attribute_name.to_s.singularize}_ids".to_sym => []}
|
15
16
|
end
|
17
|
+
|
18
|
+
def index_path
|
19
|
+
Madmin.resource_by_name(model.reflect_on_association(attribute_name).klass).index_path(format: :json)
|
20
|
+
end
|
16
21
|
end
|
17
22
|
end
|
18
23
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Madmin
|
2
|
+
module Fields
|
3
|
+
class NestedHasMany < Field
|
4
|
+
DEFAULT_ATTRIBUTES = %w[_destroy id].freeze
|
5
|
+
def nested_attributes
|
6
|
+
resource.attributes.reject { |i| skipped_fields.include?(i[:name]) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def resource
|
10
|
+
"#{to_model.name}Resource".constantize
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_param
|
14
|
+
{"#{attribute_name}_attributes": permitted_fields}
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_partial_path(name)
|
18
|
+
unless %w[index show form fields].include? name
|
19
|
+
raise ArgumentError, "`partial` must be 'index', 'show', 'form' or 'fields'"
|
20
|
+
end
|
21
|
+
|
22
|
+
"/madmin/fields/#{self.class.field_type}/#{name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_model
|
26
|
+
attribute_name.to_s.singularize.classify.constantize
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def permitted_fields
|
32
|
+
(resource.permitted_params - skipped_fields + DEFAULT_ATTRIBUTES).uniq
|
33
|
+
end
|
34
|
+
|
35
|
+
def skipped_fields
|
36
|
+
options[:skip] || []
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/madmin/fields/string.rb
CHANGED
data/lib/madmin/fields/text.rb
CHANGED
@@ -1,24 +1,30 @@
|
|
1
1
|
module Madmin
|
2
2
|
module GeneratorHelpers
|
3
|
+
ROUTES_FILE = {default: "config/routes.rb", separated: "config/routes/madmin.rb"}.freeze
|
4
|
+
|
3
5
|
def call_generator(generator, *args)
|
4
6
|
Rails::Generators.invoke(generator, args, generator_options)
|
5
7
|
end
|
6
8
|
|
7
9
|
def route_namespace_exists?
|
8
|
-
File.readlines(Rails.root.join(
|
10
|
+
File.readlines(Rails.root.join(default_routes_file)).grep(/namespace :madmin/).size > 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def rails6_1_and_up?
|
14
|
+
Gem.loaded_specs["rails"].version >= Gem::Version.new(6.1)
|
9
15
|
end
|
10
16
|
|
11
17
|
# Method copied from Rails 6.1 master
|
12
|
-
def route(routing_code, namespace: nil, sentinel: nil, indentation: 2)
|
18
|
+
def route(routing_code, namespace: nil, sentinel: nil, indentation: 2, file: default_routes_file)
|
13
19
|
routing_code = Array(namespace).reverse.reduce(routing_code) { |code, ns|
|
14
20
|
"namespace :#{ns} do\n#{indent(code, 2)}\nend"
|
15
21
|
}
|
16
22
|
|
17
23
|
log :route, routing_code
|
18
|
-
sentinel ||=
|
24
|
+
sentinel ||= default_sentinel(file)
|
19
25
|
|
20
26
|
in_root do
|
21
|
-
inject_into_file
|
27
|
+
inject_into_file file, optimize_indentation(routing_code, indentation), after: sentinel, verbose: false, force: false
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
@@ -30,6 +36,18 @@ module Madmin
|
|
30
36
|
|
31
37
|
private
|
32
38
|
|
39
|
+
def separated_routes_file?
|
40
|
+
default_routes_file.eql?(ROUTES_FILE[:separated])
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_sentinel(file)
|
44
|
+
file.eql?(ROUTES_FILE[:default]) ? /\.routes\.draw do\s*\n/m : /namespace :madmin do\s*\n/m
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_routes_file
|
48
|
+
rails6_1_and_up? ? ROUTES_FILE[:separated] : ROUTES_FILE[:default]
|
49
|
+
end
|
50
|
+
|
33
51
|
def generator_options
|
34
52
|
{behavior: behavior}
|
35
53
|
end
|
data/lib/madmin/resource.rb
CHANGED
@@ -1,14 +1,10 @@
|
|
1
1
|
module Madmin
|
2
2
|
class Resource
|
3
|
-
class_attribute :attributes, default:
|
3
|
+
class_attribute :attributes, default: ActiveSupport::OrderedHash.new
|
4
4
|
class_attribute :scopes, default: []
|
5
5
|
|
6
6
|
class << self
|
7
7
|
def inherited(base)
|
8
|
-
# Remove any old references
|
9
|
-
Madmin.resources.delete(base)
|
10
|
-
Madmin.resources << base
|
11
|
-
|
12
8
|
base.attributes = attributes.dup
|
13
9
|
base.scopes = scopes.dup
|
14
10
|
super
|
@@ -18,6 +14,10 @@ module Madmin
|
|
18
14
|
model_name.constantize
|
19
15
|
end
|
20
16
|
|
17
|
+
def model_find(id)
|
18
|
+
friendly_model? ? model.friendly.find(id) : model.find(id)
|
19
|
+
end
|
20
|
+
|
21
21
|
def model_name
|
22
22
|
to_s.chomp("Resource").classify
|
23
23
|
end
|
@@ -26,11 +26,19 @@ module Madmin
|
|
26
26
|
scopes << name
|
27
27
|
end
|
28
28
|
|
29
|
+
def get_attribute(name)
|
30
|
+
attributes[name]
|
31
|
+
end
|
32
|
+
|
29
33
|
def attribute(name, type = nil, **options)
|
30
|
-
|
34
|
+
type ||= infer_type(name)
|
35
|
+
field = options[:field] || field_for_type(type)
|
36
|
+
|
37
|
+
attributes[name] = OpenStruct.new(
|
31
38
|
name: name,
|
32
|
-
|
33
|
-
|
39
|
+
type: type,
|
40
|
+
field: field.new(**options.merge(attribute_name: name, model: model))
|
41
|
+
)
|
34
42
|
end
|
35
43
|
|
36
44
|
def friendly_name
|
@@ -38,21 +46,27 @@ module Madmin
|
|
38
46
|
end
|
39
47
|
|
40
48
|
def index_path(options = {})
|
41
|
-
|
42
|
-
|
43
|
-
|
49
|
+
route_name = "madmin_#{model.model_name.plural}_path"
|
50
|
+
|
51
|
+
url_helpers.send(route_name, options)
|
44
52
|
end
|
45
53
|
|
46
54
|
def new_path
|
47
|
-
"
|
55
|
+
route_name = "new_madmin_#{model.model_name.singular}_path"
|
56
|
+
|
57
|
+
url_helpers.send(route_name)
|
48
58
|
end
|
49
59
|
|
50
60
|
def show_path(record)
|
51
|
-
"
|
61
|
+
route_name = "madmin_#{model.model_name.singular}_path"
|
62
|
+
|
63
|
+
url_helpers.send(route_name, record.to_param)
|
52
64
|
end
|
53
65
|
|
54
66
|
def edit_path(record)
|
55
|
-
"
|
67
|
+
route_name = "edit_madmin_#{model.model_name.singular}_path"
|
68
|
+
|
69
|
+
url_helpers.send(route_name, record.to_param)
|
56
70
|
end
|
57
71
|
|
58
72
|
def param_key
|
@@ -60,18 +74,28 @@ module Madmin
|
|
60
74
|
end
|
61
75
|
|
62
76
|
def permitted_params
|
63
|
-
attributes.
|
77
|
+
attributes.values.filter_map { |a| a.field.to_param if a.field.visible?(:form) }
|
64
78
|
end
|
65
79
|
|
66
80
|
def display_name(record)
|
67
81
|
"#{record.class} ##{record.id}"
|
68
82
|
end
|
69
83
|
|
70
|
-
|
84
|
+
def friendly_model?
|
85
|
+
model.respond_to? :friendly
|
86
|
+
end
|
71
87
|
|
72
|
-
def
|
73
|
-
|
88
|
+
def sortable_columns
|
89
|
+
model.column_names
|
90
|
+
end
|
91
|
+
|
92
|
+
def searchable_attributes
|
93
|
+
attributes.values.select { |a| a.field.searchable? }
|
94
|
+
end
|
74
95
|
|
96
|
+
private
|
97
|
+
|
98
|
+
def field_for_type(type)
|
75
99
|
{
|
76
100
|
binary: Fields::String,
|
77
101
|
blob: Fields::Text,
|
@@ -90,6 +114,7 @@ module Madmin
|
|
90
114
|
text: Fields::Text,
|
91
115
|
time: Fields::Time,
|
92
116
|
timestamp: Fields::Time,
|
117
|
+
password: Fields::Password,
|
93
118
|
|
94
119
|
# Postgres specific types
|
95
120
|
bit: Fields::String,
|
@@ -126,7 +151,8 @@ module Madmin
|
|
126
151
|
polymorphic: Fields::Polymorphic,
|
127
152
|
has_many: Fields::HasMany,
|
128
153
|
has_one: Fields::HasOne,
|
129
|
-
rich_text: Fields::RichText
|
154
|
+
rich_text: Fields::RichText,
|
155
|
+
nested_has_many: Fields::NestedHasMany
|
130
156
|
}.fetch(type)
|
131
157
|
rescue
|
132
158
|
raise ArgumentError, <<~MESSAGE
|
@@ -157,6 +183,10 @@ module Madmin
|
|
157
183
|
:attachment
|
158
184
|
elsif model.reflect_on_association(:"#{name_string}_attachments")
|
159
185
|
:attachments
|
186
|
+
|
187
|
+
# has_secure_password
|
188
|
+
elsif model.attribute_types.include?("#{name_string}_digest") || name_string.ends_with?("_confirmation")
|
189
|
+
:password
|
160
190
|
end
|
161
191
|
end
|
162
192
|
|
@@ -171,6 +201,10 @@ module Madmin
|
|
171
201
|
:belongs_to
|
172
202
|
end
|
173
203
|
end
|
204
|
+
|
205
|
+
def url_helpers
|
206
|
+
@url_helpers ||= Rails.application.routes.url_helpers
|
207
|
+
end
|
174
208
|
end
|
175
209
|
end
|
176
210
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# based on Administrate Search: https://github.com/thoughtbot/administrate/blob/main/lib/administrate/search.rb
|
2
|
+
|
3
|
+
module Madmin
|
4
|
+
class Search
|
5
|
+
attr_reader :query
|
6
|
+
|
7
|
+
def initialize(scoped_resource, resource, term)
|
8
|
+
@resource = resource
|
9
|
+
@scoped_resource = scoped_resource
|
10
|
+
@query = term
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
if query.blank?
|
15
|
+
@scoped_resource.all
|
16
|
+
else
|
17
|
+
search_results(@scoped_resource)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def search_results(resources)
|
24
|
+
resources.where(query_template, *query_values)
|
25
|
+
end
|
26
|
+
|
27
|
+
def query_template
|
28
|
+
search_attributes.map do |attr|
|
29
|
+
table_name = query_table_name(attr)
|
30
|
+
searchable_fields(attr).map do |field|
|
31
|
+
column_name = column_to_query(field)
|
32
|
+
"LOWER(CAST(#{table_name}.#{column_name} AS CHAR(256))) LIKE ?"
|
33
|
+
end.join(" OR ")
|
34
|
+
end.join(" OR ")
|
35
|
+
end
|
36
|
+
|
37
|
+
def searchable_fields(attr)
|
38
|
+
[attr[:name]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def query_values
|
42
|
+
fields_count = search_attributes.sum do |attr|
|
43
|
+
searchable_fields(attr).count
|
44
|
+
end
|
45
|
+
["%#{@query.mb_chars.downcase}%"] * fields_count
|
46
|
+
end
|
47
|
+
|
48
|
+
def search_attributes
|
49
|
+
@resource.searchable_attributes
|
50
|
+
end
|
51
|
+
|
52
|
+
def query_table_name(attr)
|
53
|
+
ActiveRecord::Base.connection.quote_column_name(@scoped_resource.table_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
def column_to_query(attr)
|
57
|
+
ActiveRecord::Base.connection.quote_column_name(attr)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|