jav_madmin 2.6.2
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +294 -0
- data/Rakefile +30 -0
- data/app/assets/config/madmin_manifest.js +3 -0
- data/app/assets/stylesheets/madmin/actiontext.css +31 -0
- data/app/assets/stylesheets/madmin/application-sprockets.css +11 -0
- data/app/assets/stylesheets/madmin/application.css +9 -0
- data/app/assets/stylesheets/madmin/base.css +144 -0
- data/app/assets/stylesheets/madmin/buttons.css +53 -0
- data/app/assets/stylesheets/madmin/forms.css +90 -0
- data/app/assets/stylesheets/madmin/pagination.css +59 -0
- data/app/assets/stylesheets/madmin/reset.css +242 -0
- data/app/assets/stylesheets/madmin/sidebar.css +179 -0
- data/app/assets/stylesheets/madmin/tables.css +71 -0
- data/app/controllers/madmin/application_controller.rb +17 -0
- data/app/controllers/madmin/base_controller.rb +15 -0
- data/app/controllers/madmin/dashboard_controller.rb +6 -0
- data/app/controllers/madmin/resource_controller.rb +119 -0
- data/app/helpers/madmin/application_helper.rb +10 -0
- data/app/helpers/madmin/nav_helper.rb +34 -0
- data/app/helpers/madmin/sort_helper.rb +48 -0
- data/app/javascript/madmin/application.js +3 -0
- data/app/javascript/madmin/controllers/application.js +12 -0
- data/app/javascript/madmin/controllers/index.js +5 -0
- data/app/javascript/madmin/controllers/mobile_nav_controller.js +50 -0
- data/app/javascript/madmin/controllers/nested_form_controller.js +34 -0
- data/app/javascript/madmin/controllers/select_controller.js +37 -0
- data/app/views/layouts/madmin/application.html.erb +41 -0
- data/app/views/madmin/application/_flash.html.erb +13 -0
- data/app/views/madmin/application/_form.html.erb +24 -0
- data/app/views/madmin/application/_javascript.html.erb +8 -0
- data/app/views/madmin/application/_missing_resource.html.erb +8 -0
- data/app/views/madmin/application/_navigation.html.erb +22 -0
- data/app/views/madmin/application/edit.html.erb +11 -0
- data/app/views/madmin/application/index.html.erb +85 -0
- data/app/views/madmin/application/new.html.erb +11 -0
- data/app/views/madmin/application/show.html.erb +40 -0
- data/app/views/madmin/dashboard/show.html.erb +2 -0
- data/app/views/madmin/fields/attachment/_form.html.erb +11 -0
- data/app/views/madmin/fields/attachment/_index.html.erb +7 -0
- data/app/views/madmin/fields/attachment/_show.html.erb +9 -0
- data/app/views/madmin/fields/attachments/_form.html.erb +1 -0
- data/app/views/madmin/fields/attachments/_index.html.erb +1 -0
- data/app/views/madmin/fields/attachments/_show.html.erb +11 -0
- data/app/views/madmin/fields/belongs_to/_form.html.erb +1 -0
- data/app/views/madmin/fields/belongs_to/_index.html.erb +4 -0
- data/app/views/madmin/fields/belongs_to/_show.html.erb +7 -0
- data/app/views/madmin/fields/boolean/_form.html.erb +3 -0
- data/app/views/madmin/fields/boolean/_index.html.erb +1 -0
- data/app/views/madmin/fields/boolean/_show.html.erb +1 -0
- data/app/views/madmin/fields/currency/_form.html.erb +1 -0
- data/app/views/madmin/fields/currency/_index.html.erb +1 -0
- data/app/views/madmin/fields/currency/_show.html.erb +1 -0
- data/app/views/madmin/fields/date/_form.html.erb +1 -0
- data/app/views/madmin/fields/date/_index.html.erb +1 -0
- data/app/views/madmin/fields/date/_show.html.erb +1 -0
- data/app/views/madmin/fields/date_time/_form.html.erb +1 -0
- data/app/views/madmin/fields/date_time/_index.html.erb +1 -0
- data/app/views/madmin/fields/date_time/_show.html.erb +1 -0
- data/app/views/madmin/fields/decimal/_form.html.erb +1 -0
- data/app/views/madmin/fields/decimal/_index.html.erb +1 -0
- data/app/views/madmin/fields/decimal/_show.html.erb +1 -0
- data/app/views/madmin/fields/enum/_form.html.erb +1 -0
- data/app/views/madmin/fields/enum/_index.html.erb +1 -0
- data/app/views/madmin/fields/enum/_show.html.erb +1 -0
- data/app/views/madmin/fields/file/_form.html.erb +1 -0
- data/app/views/madmin/fields/file/_index.html.erb +1 -0
- data/app/views/madmin/fields/file/_show.html.erb +6 -0
- data/app/views/madmin/fields/float/_form.html.erb +1 -0
- data/app/views/madmin/fields/float/_index.html.erb +1 -0
- data/app/views/madmin/fields/float/_show.html.erb +1 -0
- data/app/views/madmin/fields/has_many/_form.html.erb +1 -0
- data/app/views/madmin/fields/has_many/_index.html.erb +1 -0
- data/app/views/madmin/fields/has_many/_show.html.erb +18 -0
- data/app/views/madmin/fields/has_one/_form.html.erb +1 -0
- data/app/views/madmin/fields/has_one/_index.html.erb +3 -0
- data/app/views/madmin/fields/has_one/_show.html.erb +7 -0
- data/app/views/madmin/fields/integer/_form.html.erb +1 -0
- data/app/views/madmin/fields/integer/_index.html.erb +1 -0
- data/app/views/madmin/fields/integer/_show.html.erb +1 -0
- data/app/views/madmin/fields/json/_form.html.erb +1 -0
- data/app/views/madmin/fields/json/_index.html.erb +1 -0
- data/app/views/madmin/fields/json/_show.html.erb +1 -0
- data/app/views/madmin/fields/nested_has_many/_fields.html.erb +17 -0
- data/app/views/madmin/fields/nested_has_many/_form.html.erb +28 -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 +18 -0
- data/app/views/madmin/fields/password/_form.html.erb +1 -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 +4 -0
- data/app/views/madmin/fields/polymorphic/_index.html.erb +3 -0
- data/app/views/madmin/fields/polymorphic/_show.html.erb +7 -0
- data/app/views/madmin/fields/rich_text/_form.html.erb +1 -0
- data/app/views/madmin/fields/rich_text/_index.html.erb +1 -0
- data/app/views/madmin/fields/rich_text/_show.html.erb +3 -0
- data/app/views/madmin/fields/select/_form.html.erb +1 -0
- data/app/views/madmin/fields/select/_index.html.erb +1 -0
- data/app/views/madmin/fields/select/_show.html.erb +1 -0
- data/app/views/madmin/fields/string/_form.html.erb +1 -0
- data/app/views/madmin/fields/string/_index.html.erb +1 -0
- data/app/views/madmin/fields/string/_show.html.erb +1 -0
- data/app/views/madmin/fields/text/_form.html.erb +1 -0
- data/app/views/madmin/fields/text/_index.html.erb +1 -0
- data/app/views/madmin/fields/text/_show.html.erb +1 -0
- data/app/views/madmin/fields/time/_form.html.erb +5 -0
- data/app/views/madmin/fields/time/_index.html.erb +1 -0
- data/app/views/madmin/fields/time/_show.html.erb +1 -0
- data/app/views/madmin/shared/_label.html.erb +4 -0
- data/config/importmap.rb +15 -0
- data/config/locales/el.yml +42 -0
- data/config/locales/en.yml +42 -0
- data/config/locales/fr.yml +42 -0
- data/config/locales/ko.yml +40 -0
- data/lib/generators/madmin/field/field_generator.rb +31 -0
- data/lib/generators/madmin/field/templates/_form.html.erb +1 -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 +55 -0
- data/lib/generators/madmin/install/templates/controller.rb.tt +15 -0
- data/lib/generators/madmin/install/templates/routes.rb.tt +3 -0
- data/lib/generators/madmin/resource/resource_generator.rb +73 -0
- data/lib/generators/madmin/resource/templates/controller.rb.tt +10 -0
- data/lib/generators/madmin/resource/templates/resource.rb.tt +33 -0
- data/lib/generators/madmin/views/edit_generator.rb +16 -0
- data/lib/generators/madmin/views/form_generator.rb +15 -0
- data/lib/generators/madmin/views/index_generator.rb +15 -0
- data/lib/generators/madmin/views/javascript_generator.rb +15 -0
- data/lib/generators/madmin/views/layout_generator.rb +21 -0
- data/lib/generators/madmin/views/navigation_generator.rb +15 -0
- data/lib/generators/madmin/views/new_generator.rb +16 -0
- data/lib/generators/madmin/views/show_generator.rb +15 -0
- data/lib/generators/madmin/views/views_generator.rb +16 -0
- data/lib/madmin/engine.rb +47 -0
- data/lib/madmin/field.rb +67 -0
- data/lib/madmin/fields/attachment.rb +6 -0
- data/lib/madmin/fields/attachments.rb +9 -0
- data/lib/madmin/fields/belongs_to.rb +29 -0
- data/lib/madmin/fields/boolean.rb +6 -0
- data/lib/madmin/fields/currency.rb +15 -0
- data/lib/madmin/fields/date.rb +6 -0
- data/lib/madmin/fields/date_time.rb +6 -0
- data/lib/madmin/fields/decimal.rb +6 -0
- data/lib/madmin/fields/enum.rb +9 -0
- data/lib/madmin/fields/file.rb +9 -0
- data/lib/madmin/fields/float.rb +6 -0
- data/lib/madmin/fields/has_many.rb +62 -0
- data/lib/madmin/fields/has_one.rb +10 -0
- data/lib/madmin/fields/integer.rb +6 -0
- data/lib/madmin/fields/json.rb +6 -0
- data/lib/madmin/fields/nested_has_many.rb +44 -0
- data/lib/madmin/fields/password.rb +6 -0
- data/lib/madmin/fields/polymorphic.rb +22 -0
- data/lib/madmin/fields/rich_text.rb +6 -0
- data/lib/madmin/fields/select.rb +9 -0
- data/lib/madmin/fields/string.rb +9 -0
- data/lib/madmin/fields/text.rb +9 -0
- data/lib/madmin/fields/time.rb +6 -0
- data/lib/madmin/generator_helpers.rb +59 -0
- data/lib/madmin/member_action.rb +19 -0
- data/lib/madmin/menu.rb +70 -0
- data/lib/madmin/namespace.rb +35 -0
- data/lib/madmin/resource.rb +295 -0
- data/lib/madmin/resource_builder.rb +88 -0
- data/lib/madmin/search.rb +62 -0
- data/lib/madmin/version.rb +3 -0
- data/lib/madmin/view_generator.rb +43 -0
- data/lib/madmin.rb +114 -0
- data/lib/tasks/madmin_tasks.rake +11 -0
- metadata +284 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Madmin
|
|
2
|
+
module GeneratorHelpers
|
|
3
|
+
ROUTES_FILE = {default: "config/routes.rb", separated: "config/routes/madmin.rb"}.freeze
|
|
4
|
+
|
|
5
|
+
def call_generator(generator, *args)
|
|
6
|
+
Rails::Generators.invoke(generator, args, generator_options)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def route_namespace_exists?
|
|
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)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Method copied from Rails 6.1 master
|
|
18
|
+
def route(routing_code, namespace: nil, sentinel: nil, indentation: 2, file: default_routes_file)
|
|
19
|
+
routing_code = Array(namespace).reverse.reduce(routing_code) { |code, ns|
|
|
20
|
+
"namespace :#{ns} do\n#{indent(code, 2)}\nend"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
log :route, routing_code
|
|
24
|
+
sentinel ||= default_sentinel(file)
|
|
25
|
+
|
|
26
|
+
in_root do
|
|
27
|
+
inject_into_file file, optimize_indentation(routing_code, indentation), after: sentinel, verbose: false, force: false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Method copied from Rails 6.1 master
|
|
32
|
+
def optimize_indentation(value, amount = 0)
|
|
33
|
+
return "#{value}\n" unless value.is_a?(String)
|
|
34
|
+
"#{value.strip_heredoc.indent(amount).chomp}\n"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
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[^\n]*do\s*\n/m
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def default_routes_file
|
|
48
|
+
if rails6_1_and_up? && File.exist?(ROUTES_FILE[:separated])
|
|
49
|
+
ROUTES_FILE[:separated]
|
|
50
|
+
else
|
|
51
|
+
ROUTES_FILE[:default]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def generator_options
|
|
56
|
+
{behavior: behavior}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Madmin
|
|
2
|
+
# Wraps a member action block along with its options so views can decide
|
|
3
|
+
# where to render it. Responds to `to_proc` and `call` so it can be used
|
|
4
|
+
# anywhere the raw block was.
|
|
5
|
+
class MemberAction
|
|
6
|
+
attr_reader :block
|
|
7
|
+
|
|
8
|
+
def initialize(collection: false, &block)
|
|
9
|
+
@collection = collection
|
|
10
|
+
@block = block
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def collection? = @collection
|
|
14
|
+
|
|
15
|
+
def call(*args, &blk) = block.call(*args, &blk)
|
|
16
|
+
|
|
17
|
+
def to_proc = block
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/madmin/menu.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Madmin
|
|
2
|
+
class Menu
|
|
3
|
+
def initialize
|
|
4
|
+
@children = {}
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def reset
|
|
8
|
+
@children = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def before_render(&block)
|
|
12
|
+
if block_given?
|
|
13
|
+
@before_render = block
|
|
14
|
+
else
|
|
15
|
+
@before_render
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render(&block)
|
|
20
|
+
instance_eval(&@before_render) if @before_render
|
|
21
|
+
|
|
22
|
+
# Ensure all the resources have been added to the menu
|
|
23
|
+
Madmin.resources.each do |resource|
|
|
24
|
+
next if resource.menu_options == false
|
|
25
|
+
add resource.menu_options
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
items.each(&block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module Node
|
|
32
|
+
def add(options)
|
|
33
|
+
options = options.dup
|
|
34
|
+
|
|
35
|
+
if (parent = options.delete(:parent))
|
|
36
|
+
@children[parent] ||= Item.new(label: parent)
|
|
37
|
+
@children[parent].add options
|
|
38
|
+
else
|
|
39
|
+
item = Item.new(**options)
|
|
40
|
+
@children[item.label] = item
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def items
|
|
45
|
+
@children.values.sort do |a, b|
|
|
46
|
+
result = a.position <=> b.position
|
|
47
|
+
result = a.label <=> b.label if result == 0 # sort alphabetically for the same position
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
include Node
|
|
54
|
+
|
|
55
|
+
class Item
|
|
56
|
+
include Node
|
|
57
|
+
|
|
58
|
+
attr_reader :label, :url, :position, :parent, :children
|
|
59
|
+
|
|
60
|
+
def initialize(label:, url: nil, position: 99, parent: nil, **options)
|
|
61
|
+
@label = label
|
|
62
|
+
@url = url
|
|
63
|
+
@position = position
|
|
64
|
+
@parent = parent
|
|
65
|
+
@if = options.delete(:if)
|
|
66
|
+
@children = {}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Madmin
|
|
2
|
+
class Namespace
|
|
3
|
+
def initialize(namespace)
|
|
4
|
+
@namespace = namespace
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def resources
|
|
8
|
+
@resources ||= routes.map(&:first).uniq.map { |path|
|
|
9
|
+
Resource.new(namespace, path)
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def routes
|
|
14
|
+
@routes ||= all_routes.select { |controller, _action|
|
|
15
|
+
controller.starts_with?("#{namespace}/")
|
|
16
|
+
}.map { |controller, action|
|
|
17
|
+
[controller.gsub(/^#{namespace}\//, ""), action]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resources_with_index_route
|
|
22
|
+
routes.select { |_resource, route| route == "index" }.map(&:first).uniq
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :namespace
|
|
28
|
+
|
|
29
|
+
def all_routes
|
|
30
|
+
Rails.application.routes.routes.map do |route|
|
|
31
|
+
route.defaults.values_at(:controller, :action).map(&:to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
module Madmin
|
|
2
|
+
class Resource
|
|
3
|
+
Attribute = Data.define(:name, :type, :field)
|
|
4
|
+
|
|
5
|
+
class_attribute :attributes, default: ActiveSupport::OrderedHash.new
|
|
6
|
+
class_attribute :member_actions, default: []
|
|
7
|
+
class_attribute :collection_actions, default: []
|
|
8
|
+
class_attribute :scopes, default: []
|
|
9
|
+
class_attribute :menu_options, instance_reader: false
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def inherited(base)
|
|
13
|
+
base.attributes = attributes.dup
|
|
14
|
+
base.member_actions = member_actions.dup
|
|
15
|
+
base.collection_actions = collection_actions.dup
|
|
16
|
+
base.scopes = scopes.dup
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def model(value = nil)
|
|
21
|
+
if value
|
|
22
|
+
@model = value
|
|
23
|
+
else
|
|
24
|
+
@model ||= model_name.constantize
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def model_find(id)
|
|
29
|
+
friendly_model? ? model.friendly.find(id) : model.find(id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def model_name
|
|
33
|
+
to_s.chomp("Resource").classify
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def scope(name)
|
|
37
|
+
scopes << name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Label for a scope button on the index. Looks up
|
|
41
|
+
# madmin.scopes.<param_key>.<name>, then madmin.scopes.<name> in I18n,
|
|
42
|
+
# falling back to the humanized scope name. Override for custom labels.
|
|
43
|
+
def scope_label(name)
|
|
44
|
+
I18n.t("madmin.scopes.#{param_key}.#{name}", default: [:"madmin.scopes.#{name}", name.to_s.humanize])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_attribute(name)
|
|
48
|
+
attributes[name]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def attribute(name, type = nil, **options)
|
|
52
|
+
type ||= infer_type(name)
|
|
53
|
+
field = options.delete(:field) || field_for_type(type)
|
|
54
|
+
|
|
55
|
+
if field.nil?
|
|
56
|
+
Rails.logger.warn <<~MESSAGE
|
|
57
|
+
WARNING: Madmin could not infer a field type for `#{name}` attribute in `#{self.name}`. Defaulting to a String type.
|
|
58
|
+
#{caller.find { _1.start_with? Rails.root.to_s }}
|
|
59
|
+
MESSAGE
|
|
60
|
+
field = Fields::String
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
config = ActiveSupport::OrderedOptions.new.merge(options)
|
|
64
|
+
yield config if block_given?
|
|
65
|
+
|
|
66
|
+
# Form is an alias for new & edit but shouldn't override their values if explicitly set (true/false)
|
|
67
|
+
config.new = config[:form] if config[:new].nil?
|
|
68
|
+
config.edit = config[:form] if config[:edit].nil?
|
|
69
|
+
|
|
70
|
+
# New/create and edit/update need to match
|
|
71
|
+
config.create = config.new
|
|
72
|
+
config.update = config.edit
|
|
73
|
+
|
|
74
|
+
# Remove any nil values to use the defaults
|
|
75
|
+
config.compact!
|
|
76
|
+
|
|
77
|
+
attributes[name] = Attribute.new(
|
|
78
|
+
name: name,
|
|
79
|
+
type: type,
|
|
80
|
+
field: field.new(attribute_name: name, model: model, resource: self, options: config)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns singular name
|
|
85
|
+
# For example: "Forum::Post" -> "Post"
|
|
86
|
+
def friendly_name
|
|
87
|
+
model.model_name.human
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Support for isolated namespaces
|
|
91
|
+
# Finds parent module class to include in polymorphic urls
|
|
92
|
+
def route_namespace
|
|
93
|
+
return @route_namespace if instance_variable_defined?(:@route_namespace)
|
|
94
|
+
namespace = model.module_parents.detect do |n|
|
|
95
|
+
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
|
96
|
+
end
|
|
97
|
+
@route_namespace = (namespace ? namespace.name.underscore.to_sym : nil)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def index_path(options = {})
|
|
101
|
+
url_helpers.polymorphic_path([:madmin, route_namespace, model], options)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def new_path
|
|
105
|
+
url_helpers.polymorphic_path([:madmin, route_namespace, model], action: :new)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def show_path(record)
|
|
109
|
+
url_helpers.polymorphic_path([:madmin, route_namespace, becomes(record)])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def edit_path(record)
|
|
113
|
+
url_helpers.polymorphic_path([:madmin, route_namespace, becomes(record)], action: :edit)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def becomes(record)
|
|
117
|
+
record.instance_of?(model) ? record : record.becomes(model)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def param_key
|
|
121
|
+
model.model_name.param_key
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def permitted_params
|
|
125
|
+
attributes.values.filter { |a| a.field.visible?(:form) }.map { |a| a.field.to_param }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def display_name(record)
|
|
129
|
+
"#{record.model_name.human} ##{record.id}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def friendly_model?
|
|
133
|
+
model.respond_to? :friendly
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def readonly?
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def sortable_columns
|
|
141
|
+
model_column_names
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def model_column_names
|
|
145
|
+
model.column_names
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def searchable_attributes
|
|
149
|
+
attributes.values.select { |a| a.field.searchable? }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def member_action(collection: false, &block)
|
|
153
|
+
member_actions << MemberAction.new(collection: collection, &block)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Member actions that should also render in each row on the index page
|
|
157
|
+
def collection_member_actions
|
|
158
|
+
member_actions.select { |action| action.respond_to?(:collection?) && action.collection? }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def collection_action(&block)
|
|
162
|
+
collection_actions << block
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def field_for_type(type)
|
|
166
|
+
{
|
|
167
|
+
binary: Fields::String,
|
|
168
|
+
blob: Fields::Text,
|
|
169
|
+
boolean: Fields::Boolean,
|
|
170
|
+
currency: Fields::Currency,
|
|
171
|
+
date: Fields::Date,
|
|
172
|
+
datetime: Fields::DateTime,
|
|
173
|
+
decimal: Fields::Decimal,
|
|
174
|
+
enum: Fields::Enum,
|
|
175
|
+
float: Fields::Float,
|
|
176
|
+
hstore: Fields::Json,
|
|
177
|
+
integer: Fields::Integer,
|
|
178
|
+
json: Fields::Json,
|
|
179
|
+
jsonb: Fields::Json,
|
|
180
|
+
primary_key: Fields::String,
|
|
181
|
+
select: Fields::Select,
|
|
182
|
+
string: Fields::String,
|
|
183
|
+
text: Fields::Text,
|
|
184
|
+
time: Fields::Time,
|
|
185
|
+
timestamp: Fields::Time,
|
|
186
|
+
timestamptz: Fields::Time,
|
|
187
|
+
password: Fields::Password,
|
|
188
|
+
file: Fields::File,
|
|
189
|
+
|
|
190
|
+
# Postgres specific types
|
|
191
|
+
bit: Fields::String,
|
|
192
|
+
bit_varying: Fields::String,
|
|
193
|
+
box: Fields::String,
|
|
194
|
+
cidr: Fields::String,
|
|
195
|
+
circle: Fields::String,
|
|
196
|
+
citext: Fields::Text,
|
|
197
|
+
daterange: Fields::String,
|
|
198
|
+
inet: Fields::String,
|
|
199
|
+
int4range: Fields::String,
|
|
200
|
+
int8range: Fields::String,
|
|
201
|
+
interval: Fields::String,
|
|
202
|
+
line: Fields::String,
|
|
203
|
+
lseg: Fields::String,
|
|
204
|
+
ltree: Fields::String,
|
|
205
|
+
macaddr: Fields::String,
|
|
206
|
+
money: Fields::String,
|
|
207
|
+
numrange: Fields::String,
|
|
208
|
+
oid: Fields::String,
|
|
209
|
+
path: Fields::String,
|
|
210
|
+
point: Fields::String,
|
|
211
|
+
polygon: Fields::String,
|
|
212
|
+
tsrange: Fields::String,
|
|
213
|
+
tstzrange: Fields::String,
|
|
214
|
+
tsvector: Fields::String,
|
|
215
|
+
uuid: Fields::String,
|
|
216
|
+
xml: Fields::Text,
|
|
217
|
+
|
|
218
|
+
# Associations
|
|
219
|
+
attachment: Fields::Attachment,
|
|
220
|
+
attachments: Fields::Attachments,
|
|
221
|
+
belongs_to: Fields::BelongsTo,
|
|
222
|
+
polymorphic: Fields::Polymorphic,
|
|
223
|
+
has_many: Fields::HasMany,
|
|
224
|
+
has_one: Fields::HasOne,
|
|
225
|
+
rich_text: Fields::RichText,
|
|
226
|
+
nested_has_many: Fields::NestedHasMany
|
|
227
|
+
}[type]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def infer_type(name)
|
|
231
|
+
name_string = name.to_s
|
|
232
|
+
|
|
233
|
+
if model.attribute_types.include?(name_string)
|
|
234
|
+
column_type = model.attribute_types[name_string]
|
|
235
|
+
if column_type.is_a? ::ActiveRecord::Enum::EnumType
|
|
236
|
+
:enum
|
|
237
|
+
else
|
|
238
|
+
column_type.type || :string
|
|
239
|
+
end
|
|
240
|
+
elsif (association = model.reflect_on_association(name))
|
|
241
|
+
type_for_association(association)
|
|
242
|
+
elsif model.reflect_on_association(:"rich_text_#{name_string}")
|
|
243
|
+
:rich_text
|
|
244
|
+
elsif model.reflect_on_association(:"#{name_string}_attachment")
|
|
245
|
+
:attachment
|
|
246
|
+
elsif model.reflect_on_association(:"#{name_string}_attachments")
|
|
247
|
+
:attachments
|
|
248
|
+
|
|
249
|
+
# has_secure_password
|
|
250
|
+
elsif model.attribute_types.include?("#{name_string}_digest") || name_string.ends_with?("_confirmation")
|
|
251
|
+
:password
|
|
252
|
+
|
|
253
|
+
# ActiveRecord Store
|
|
254
|
+
elsif model_store_accessors.include?(name)
|
|
255
|
+
:string
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def type_for_association(association)
|
|
260
|
+
if association.has_one?
|
|
261
|
+
:has_one
|
|
262
|
+
elsif association.collection?
|
|
263
|
+
:has_many
|
|
264
|
+
elsif association.polymorphic?
|
|
265
|
+
:polymorphic
|
|
266
|
+
else
|
|
267
|
+
:belongs_to
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def url_helpers
|
|
272
|
+
@url_helpers ||= Rails.application.routes.url_helpers
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def model_store_accessors
|
|
276
|
+
return [] unless model.respond_to?(:stored_attributes)
|
|
277
|
+
|
|
278
|
+
store_accessors = model.stored_attributes.values
|
|
279
|
+
store_accessors.flatten
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def menu(options)
|
|
283
|
+
@menu_options = options
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def menu_options
|
|
287
|
+
return false if @menu_options == false
|
|
288
|
+
@menu_options ||= {}
|
|
289
|
+
@menu_options.with_defaults(label: model.model_name.i18n_key.to_s, url: index_path)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
ActiveSupport.run_load_hooks(:madmin_resource, self)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module Madmin
|
|
2
|
+
class ResourceBuilder
|
|
3
|
+
attr_reader :model
|
|
4
|
+
|
|
5
|
+
def initialize(model)
|
|
6
|
+
@model = model
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def associations
|
|
10
|
+
return [] unless model.respond_to?(:reflections)
|
|
11
|
+
|
|
12
|
+
model.reflections.reject { |name, association|
|
|
13
|
+
# Hide these special associations
|
|
14
|
+
name.starts_with?("rich_text") ||
|
|
15
|
+
name.ends_with?("_attachment") ||
|
|
16
|
+
name.ends_with?("_attachments") ||
|
|
17
|
+
name.ends_with?("_blob") ||
|
|
18
|
+
name.ends_with?("_blobs")
|
|
19
|
+
}.keys
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def attributes
|
|
23
|
+
model.attribute_names + virtual_attributes - redundant_attributes
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def store_accessors
|
|
27
|
+
return [] unless model.respond_to?(:stored_attributes)
|
|
28
|
+
|
|
29
|
+
model.stored_attributes.values.flatten
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def virtual_attributes
|
|
33
|
+
return [] unless model.respond_to?(:attribute_types)
|
|
34
|
+
|
|
35
|
+
virtual = []
|
|
36
|
+
|
|
37
|
+
# has_secure_password columns
|
|
38
|
+
password_attributes = model.attribute_types.keys.select { |k| k.ends_with?("_digest") }.map { |k| k.delete_suffix("_digest") }
|
|
39
|
+
virtual += password_attributes.map { |attr| [attr, "#{attr}_confirmation"] }.flatten
|
|
40
|
+
|
|
41
|
+
# ActiveRecord Store columns
|
|
42
|
+
virtual += store_accessors.map(&:to_s)
|
|
43
|
+
|
|
44
|
+
# Add virtual attributes for ActionText and ActiveStorage
|
|
45
|
+
model.reflections.each do |name, association|
|
|
46
|
+
if name.starts_with?("rich_text")
|
|
47
|
+
virtual << name.split("rich_text_").last
|
|
48
|
+
elsif name.ends_with?("_attachment")
|
|
49
|
+
virtual << name.split("_attachment").first
|
|
50
|
+
elsif name.ends_with?("_attachments")
|
|
51
|
+
virtual << name.split("_attachments").first
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
virtual
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def redundant_attributes
|
|
59
|
+
return [] unless model.respond_to?(:attribute_types)
|
|
60
|
+
|
|
61
|
+
redundant = []
|
|
62
|
+
|
|
63
|
+
# has_secure_password columns
|
|
64
|
+
redundant += model.attribute_types.keys.select { |k| k.ends_with?("_digest") }
|
|
65
|
+
|
|
66
|
+
# ActiveRecord Store columns
|
|
67
|
+
store_columns = model.stored_attributes.keys
|
|
68
|
+
redundant += store_columns.map(&:to_s)
|
|
69
|
+
|
|
70
|
+
model.reflections.each do |name, association|
|
|
71
|
+
if association.has_one?
|
|
72
|
+
next
|
|
73
|
+
elsif association.collection?
|
|
74
|
+
next
|
|
75
|
+
elsif association.polymorphic?
|
|
76
|
+
redundant << "#{name}_id"
|
|
77
|
+
redundant << "#{name}_type"
|
|
78
|
+
elsif name.starts_with?("rich_text")
|
|
79
|
+
redundant << name
|
|
80
|
+
else # belongs to
|
|
81
|
+
redundant << "#{name}_id"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
redundant
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
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.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_table_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
|
+
|
|
60
|
+
ActiveSupport.run_load_hooks(:madmin_search, self)
|
|
61
|
+
end
|
|
62
|
+
end
|