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.
Files changed (172) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +294 -0
  4. data/Rakefile +30 -0
  5. data/app/assets/config/madmin_manifest.js +3 -0
  6. data/app/assets/stylesheets/madmin/actiontext.css +31 -0
  7. data/app/assets/stylesheets/madmin/application-sprockets.css +11 -0
  8. data/app/assets/stylesheets/madmin/application.css +9 -0
  9. data/app/assets/stylesheets/madmin/base.css +144 -0
  10. data/app/assets/stylesheets/madmin/buttons.css +53 -0
  11. data/app/assets/stylesheets/madmin/forms.css +90 -0
  12. data/app/assets/stylesheets/madmin/pagination.css +59 -0
  13. data/app/assets/stylesheets/madmin/reset.css +242 -0
  14. data/app/assets/stylesheets/madmin/sidebar.css +179 -0
  15. data/app/assets/stylesheets/madmin/tables.css +71 -0
  16. data/app/controllers/madmin/application_controller.rb +17 -0
  17. data/app/controllers/madmin/base_controller.rb +15 -0
  18. data/app/controllers/madmin/dashboard_controller.rb +6 -0
  19. data/app/controllers/madmin/resource_controller.rb +119 -0
  20. data/app/helpers/madmin/application_helper.rb +10 -0
  21. data/app/helpers/madmin/nav_helper.rb +34 -0
  22. data/app/helpers/madmin/sort_helper.rb +48 -0
  23. data/app/javascript/madmin/application.js +3 -0
  24. data/app/javascript/madmin/controllers/application.js +12 -0
  25. data/app/javascript/madmin/controllers/index.js +5 -0
  26. data/app/javascript/madmin/controllers/mobile_nav_controller.js +50 -0
  27. data/app/javascript/madmin/controllers/nested_form_controller.js +34 -0
  28. data/app/javascript/madmin/controllers/select_controller.js +37 -0
  29. data/app/views/layouts/madmin/application.html.erb +41 -0
  30. data/app/views/madmin/application/_flash.html.erb +13 -0
  31. data/app/views/madmin/application/_form.html.erb +24 -0
  32. data/app/views/madmin/application/_javascript.html.erb +8 -0
  33. data/app/views/madmin/application/_missing_resource.html.erb +8 -0
  34. data/app/views/madmin/application/_navigation.html.erb +22 -0
  35. data/app/views/madmin/application/edit.html.erb +11 -0
  36. data/app/views/madmin/application/index.html.erb +85 -0
  37. data/app/views/madmin/application/new.html.erb +11 -0
  38. data/app/views/madmin/application/show.html.erb +40 -0
  39. data/app/views/madmin/dashboard/show.html.erb +2 -0
  40. data/app/views/madmin/fields/attachment/_form.html.erb +11 -0
  41. data/app/views/madmin/fields/attachment/_index.html.erb +7 -0
  42. data/app/views/madmin/fields/attachment/_show.html.erb +9 -0
  43. data/app/views/madmin/fields/attachments/_form.html.erb +1 -0
  44. data/app/views/madmin/fields/attachments/_index.html.erb +1 -0
  45. data/app/views/madmin/fields/attachments/_show.html.erb +11 -0
  46. data/app/views/madmin/fields/belongs_to/_form.html.erb +1 -0
  47. data/app/views/madmin/fields/belongs_to/_index.html.erb +4 -0
  48. data/app/views/madmin/fields/belongs_to/_show.html.erb +7 -0
  49. data/app/views/madmin/fields/boolean/_form.html.erb +3 -0
  50. data/app/views/madmin/fields/boolean/_index.html.erb +1 -0
  51. data/app/views/madmin/fields/boolean/_show.html.erb +1 -0
  52. data/app/views/madmin/fields/currency/_form.html.erb +1 -0
  53. data/app/views/madmin/fields/currency/_index.html.erb +1 -0
  54. data/app/views/madmin/fields/currency/_show.html.erb +1 -0
  55. data/app/views/madmin/fields/date/_form.html.erb +1 -0
  56. data/app/views/madmin/fields/date/_index.html.erb +1 -0
  57. data/app/views/madmin/fields/date/_show.html.erb +1 -0
  58. data/app/views/madmin/fields/date_time/_form.html.erb +1 -0
  59. data/app/views/madmin/fields/date_time/_index.html.erb +1 -0
  60. data/app/views/madmin/fields/date_time/_show.html.erb +1 -0
  61. data/app/views/madmin/fields/decimal/_form.html.erb +1 -0
  62. data/app/views/madmin/fields/decimal/_index.html.erb +1 -0
  63. data/app/views/madmin/fields/decimal/_show.html.erb +1 -0
  64. data/app/views/madmin/fields/enum/_form.html.erb +1 -0
  65. data/app/views/madmin/fields/enum/_index.html.erb +1 -0
  66. data/app/views/madmin/fields/enum/_show.html.erb +1 -0
  67. data/app/views/madmin/fields/file/_form.html.erb +1 -0
  68. data/app/views/madmin/fields/file/_index.html.erb +1 -0
  69. data/app/views/madmin/fields/file/_show.html.erb +6 -0
  70. data/app/views/madmin/fields/float/_form.html.erb +1 -0
  71. data/app/views/madmin/fields/float/_index.html.erb +1 -0
  72. data/app/views/madmin/fields/float/_show.html.erb +1 -0
  73. data/app/views/madmin/fields/has_many/_form.html.erb +1 -0
  74. data/app/views/madmin/fields/has_many/_index.html.erb +1 -0
  75. data/app/views/madmin/fields/has_many/_show.html.erb +18 -0
  76. data/app/views/madmin/fields/has_one/_form.html.erb +1 -0
  77. data/app/views/madmin/fields/has_one/_index.html.erb +3 -0
  78. data/app/views/madmin/fields/has_one/_show.html.erb +7 -0
  79. data/app/views/madmin/fields/integer/_form.html.erb +1 -0
  80. data/app/views/madmin/fields/integer/_index.html.erb +1 -0
  81. data/app/views/madmin/fields/integer/_show.html.erb +1 -0
  82. data/app/views/madmin/fields/json/_form.html.erb +1 -0
  83. data/app/views/madmin/fields/json/_index.html.erb +1 -0
  84. data/app/views/madmin/fields/json/_show.html.erb +1 -0
  85. data/app/views/madmin/fields/nested_has_many/_fields.html.erb +17 -0
  86. data/app/views/madmin/fields/nested_has_many/_form.html.erb +28 -0
  87. data/app/views/madmin/fields/nested_has_many/_index.html.erb +1 -0
  88. data/app/views/madmin/fields/nested_has_many/_show.html.erb +18 -0
  89. data/app/views/madmin/fields/password/_form.html.erb +1 -0
  90. data/app/views/madmin/fields/password/_index.html.erb +1 -0
  91. data/app/views/madmin/fields/password/_show.html.erb +1 -0
  92. data/app/views/madmin/fields/polymorphic/_form.html.erb +4 -0
  93. data/app/views/madmin/fields/polymorphic/_index.html.erb +3 -0
  94. data/app/views/madmin/fields/polymorphic/_show.html.erb +7 -0
  95. data/app/views/madmin/fields/rich_text/_form.html.erb +1 -0
  96. data/app/views/madmin/fields/rich_text/_index.html.erb +1 -0
  97. data/app/views/madmin/fields/rich_text/_show.html.erb +3 -0
  98. data/app/views/madmin/fields/select/_form.html.erb +1 -0
  99. data/app/views/madmin/fields/select/_index.html.erb +1 -0
  100. data/app/views/madmin/fields/select/_show.html.erb +1 -0
  101. data/app/views/madmin/fields/string/_form.html.erb +1 -0
  102. data/app/views/madmin/fields/string/_index.html.erb +1 -0
  103. data/app/views/madmin/fields/string/_show.html.erb +1 -0
  104. data/app/views/madmin/fields/text/_form.html.erb +1 -0
  105. data/app/views/madmin/fields/text/_index.html.erb +1 -0
  106. data/app/views/madmin/fields/text/_show.html.erb +1 -0
  107. data/app/views/madmin/fields/time/_form.html.erb +5 -0
  108. data/app/views/madmin/fields/time/_index.html.erb +1 -0
  109. data/app/views/madmin/fields/time/_show.html.erb +1 -0
  110. data/app/views/madmin/shared/_label.html.erb +4 -0
  111. data/config/importmap.rb +15 -0
  112. data/config/locales/el.yml +42 -0
  113. data/config/locales/en.yml +42 -0
  114. data/config/locales/fr.yml +42 -0
  115. data/config/locales/ko.yml +40 -0
  116. data/lib/generators/madmin/field/field_generator.rb +31 -0
  117. data/lib/generators/madmin/field/templates/_form.html.erb +1 -0
  118. data/lib/generators/madmin/field/templates/_index.html.erb +1 -0
  119. data/lib/generators/madmin/field/templates/_show.html.erb +1 -0
  120. data/lib/generators/madmin/field/templates/field.rb.tt +26 -0
  121. data/lib/generators/madmin/install/install_generator.rb +55 -0
  122. data/lib/generators/madmin/install/templates/controller.rb.tt +15 -0
  123. data/lib/generators/madmin/install/templates/routes.rb.tt +3 -0
  124. data/lib/generators/madmin/resource/resource_generator.rb +73 -0
  125. data/lib/generators/madmin/resource/templates/controller.rb.tt +10 -0
  126. data/lib/generators/madmin/resource/templates/resource.rb.tt +33 -0
  127. data/lib/generators/madmin/views/edit_generator.rb +16 -0
  128. data/lib/generators/madmin/views/form_generator.rb +15 -0
  129. data/lib/generators/madmin/views/index_generator.rb +15 -0
  130. data/lib/generators/madmin/views/javascript_generator.rb +15 -0
  131. data/lib/generators/madmin/views/layout_generator.rb +21 -0
  132. data/lib/generators/madmin/views/navigation_generator.rb +15 -0
  133. data/lib/generators/madmin/views/new_generator.rb +16 -0
  134. data/lib/generators/madmin/views/show_generator.rb +15 -0
  135. data/lib/generators/madmin/views/views_generator.rb +16 -0
  136. data/lib/madmin/engine.rb +47 -0
  137. data/lib/madmin/field.rb +67 -0
  138. data/lib/madmin/fields/attachment.rb +6 -0
  139. data/lib/madmin/fields/attachments.rb +9 -0
  140. data/lib/madmin/fields/belongs_to.rb +29 -0
  141. data/lib/madmin/fields/boolean.rb +6 -0
  142. data/lib/madmin/fields/currency.rb +15 -0
  143. data/lib/madmin/fields/date.rb +6 -0
  144. data/lib/madmin/fields/date_time.rb +6 -0
  145. data/lib/madmin/fields/decimal.rb +6 -0
  146. data/lib/madmin/fields/enum.rb +9 -0
  147. data/lib/madmin/fields/file.rb +9 -0
  148. data/lib/madmin/fields/float.rb +6 -0
  149. data/lib/madmin/fields/has_many.rb +62 -0
  150. data/lib/madmin/fields/has_one.rb +10 -0
  151. data/lib/madmin/fields/integer.rb +6 -0
  152. data/lib/madmin/fields/json.rb +6 -0
  153. data/lib/madmin/fields/nested_has_many.rb +44 -0
  154. data/lib/madmin/fields/password.rb +6 -0
  155. data/lib/madmin/fields/polymorphic.rb +22 -0
  156. data/lib/madmin/fields/rich_text.rb +6 -0
  157. data/lib/madmin/fields/select.rb +9 -0
  158. data/lib/madmin/fields/string.rb +9 -0
  159. data/lib/madmin/fields/text.rb +9 -0
  160. data/lib/madmin/fields/time.rb +6 -0
  161. data/lib/madmin/generator_helpers.rb +59 -0
  162. data/lib/madmin/member_action.rb +19 -0
  163. data/lib/madmin/menu.rb +70 -0
  164. data/lib/madmin/namespace.rb +35 -0
  165. data/lib/madmin/resource.rb +295 -0
  166. data/lib/madmin/resource_builder.rb +88 -0
  167. data/lib/madmin/search.rb +62 -0
  168. data/lib/madmin/version.rb +3 -0
  169. data/lib/madmin/view_generator.rb +43 -0
  170. data/lib/madmin.rb +114 -0
  171. data/lib/tasks/madmin_tasks.rake +11 -0
  172. metadata +284 -0
@@ -0,0 +1,6 @@
1
+ module Madmin
2
+ module Fields
3
+ class RichText < Field
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Madmin
2
+ module Fields
3
+ class Select < Field
4
+ def options_for_select(record)
5
+ options.collection
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Madmin
2
+ module Fields
3
+ class String < Field
4
+ def searchable?
5
+ options.fetch(:searchable, resource.model_column_names.include?(attribute_name.to_s))
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Madmin
2
+ module Fields
3
+ class Text < Field
4
+ def searchable?
5
+ options.fetch(:searchable, resource.model_column_names.include?(attribute_name.to_s))
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Madmin
2
+ module Fields
3
+ class Time < Field
4
+ end
5
+ end
6
+ end
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Madmin
2
+ VERSION = "2.6.2"
3
+ end