rails_admin_next 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +46 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +317 -0
  5. data/Rakefile +25 -0
  6. data/app/assets/javascripts/rails_admin/custom/ui.js +1 -0
  7. data/app/assets/stylesheets/rails_admin/framework.css +2375 -0
  8. data/app/assets/stylesheets/rails_admin/skin.css +472 -0
  9. data/app/assets/stylesheets/rails_admin/tokens.css +281 -0
  10. data/app/assets/stylesheets/rails_admin.css +47 -0
  11. data/app/controllers/rails_admin_next/application_controller.rb +98 -0
  12. data/app/controllers/rails_admin_next/main_controller.rb +185 -0
  13. data/app/helpers/rails_admin_next/application_helper.rb +254 -0
  14. data/app/helpers/rails_admin_next/form_builder.rb +173 -0
  15. data/app/helpers/rails_admin_next/main_helper.rb +63 -0
  16. data/app/views/layouts/rails_admin_next/_head.html.erb +29 -0
  17. data/app/views/layouts/rails_admin_next/_navigation.html.erb +18 -0
  18. data/app/views/layouts/rails_admin_next/_secondary_navigation.html.erb +27 -0
  19. data/app/views/layouts/rails_admin_next/_sidebar_navigation.html.erb +6 -0
  20. data/app/views/layouts/rails_admin_next/application.html.erb +42 -0
  21. data/app/views/layouts/rails_admin_next/content.html.erb +22 -0
  22. data/app/views/layouts/rails_admin_next/modal.js.erb +7 -0
  23. data/app/views/rails_admin_next/main/_dashboard_history.html.erb +46 -0
  24. data/app/views/rails_admin_next/main/_delete_notice.html.erb +36 -0
  25. data/app/views/rails_admin_next/main/_form_action_text.html.erb +2 -0
  26. data/app/views/rails_admin_next/main/_form_boolean.html.erb +15 -0
  27. data/app/views/rails_admin_next/main/_form_colorpicker.html.erb +6 -0
  28. data/app/views/rails_admin_next/main/_form_datetime.html.erb +14 -0
  29. data/app/views/rails_admin_next/main/_form_enumeration.html.erb +26 -0
  30. data/app/views/rails_admin_next/main/_form_field.html.erb +2 -0
  31. data/app/views/rails_admin_next/main/_form_file_upload.html.erb +18 -0
  32. data/app/views/rails_admin_next/main/_form_filtering_multiselect.html.erb +20 -0
  33. data/app/views/rails_admin_next/main/_form_filtering_select.html.erb +22 -0
  34. data/app/views/rails_admin_next/main/_form_multiple_file_upload.html.erb +21 -0
  35. data/app/views/rails_admin_next/main/_form_nested_many.html.erb +20 -0
  36. data/app/views/rails_admin_next/main/_form_nested_one.html.erb +19 -0
  37. data/app/views/rails_admin_next/main/_form_polymorphic_association.html.erb +24 -0
  38. data/app/views/rails_admin_next/main/_form_text.html.erb +2 -0
  39. data/app/views/rails_admin_next/main/_pagination.html.erb +54 -0
  40. data/app/views/rails_admin_next/main/_submit_buttons.html.erb +26 -0
  41. data/app/views/rails_admin_next/main/bulk_delete.html.erb +19 -0
  42. data/app/views/rails_admin_next/main/dashboard.html.erb +64 -0
  43. data/app/views/rails_admin_next/main/delete.html.erb +21 -0
  44. data/app/views/rails_admin_next/main/edit.html.erb +3 -0
  45. data/app/views/rails_admin_next/main/export.html.erb +149 -0
  46. data/app/views/rails_admin_next/main/history.html.erb +80 -0
  47. data/app/views/rails_admin_next/main/index.html.erb +186 -0
  48. data/app/views/rails_admin_next/main/new.html.erb +3 -0
  49. data/app/views/rails_admin_next/main/show.html.erb +30 -0
  50. data/config/brakeman.ignore +145 -0
  51. data/config/importmap.rails_admin.rb +29 -0
  52. data/config/initializers/active_record_extensions.rb +21 -0
  53. data/config/locales/rails_admin_next.en.yml +158 -0
  54. data/config/routes.rb +14 -0
  55. data/lib/generators/rails_admin_next/install_generator.rb +39 -0
  56. data/lib/generators/rails_admin_next/templates/initializer.erb +40 -0
  57. data/lib/generators/rails_admin_next/utils.rb +21 -0
  58. data/lib/rails_admin_next/abstract_model.rb +266 -0
  59. data/lib/rails_admin_next/adapters/active_record/association.rb +130 -0
  60. data/lib/rails_admin_next/adapters/active_record/object_extension.rb +13 -0
  61. data/lib/rails_admin_next/adapters/active_record/property.rb +58 -0
  62. data/lib/rails_admin_next/adapters/active_record.rb +371 -0
  63. data/lib/rails_admin_next/config/actions/base.rb +160 -0
  64. data/lib/rails_admin_next/config/actions/bulk_delete.rb +67 -0
  65. data/lib/rails_admin_next/config/actions/dashboard.rb +62 -0
  66. data/lib/rails_admin_next/config/actions/delete.rb +58 -0
  67. data/lib/rails_admin_next/config/actions/edit.rb +56 -0
  68. data/lib/rails_admin_next/config/actions/export.rb +47 -0
  69. data/lib/rails_admin_next/config/actions/history_index.rb +40 -0
  70. data/lib/rails_admin_next/config/actions/history_show.rb +40 -0
  71. data/lib/rails_admin_next/config/actions/index.rb +108 -0
  72. data/lib/rails_admin_next/config/actions/new.rb +69 -0
  73. data/lib/rails_admin_next/config/actions/show.rb +36 -0
  74. data/lib/rails_admin_next/config/actions/show_in_app.rb +37 -0
  75. data/lib/rails_admin_next/config/actions.rb +122 -0
  76. data/lib/rails_admin_next/config/configurable.rb +107 -0
  77. data/lib/rails_admin_next/config/const_load_suppressor.rb +78 -0
  78. data/lib/rails_admin_next/config/fields/association.rb +169 -0
  79. data/lib/rails_admin_next/config/fields/base.rb +391 -0
  80. data/lib/rails_admin_next/config/fields/collection_association.rb +90 -0
  81. data/lib/rails_admin_next/config/fields/factories/action_text.rb +14 -0
  82. data/lib/rails_admin_next/config/fields/factories/active_storage.rb +34 -0
  83. data/lib/rails_admin_next/config/fields/factories/association.rb +32 -0
  84. data/lib/rails_admin_next/config/fields/factories/encrypted_password.rb +28 -0
  85. data/lib/rails_admin_next/config/fields/factories/enum.rb +23 -0
  86. data/lib/rails_admin_next/config/fields/factories/password.rb +19 -0
  87. data/lib/rails_admin_next/config/fields/group.rb +77 -0
  88. data/lib/rails_admin_next/config/fields/singular_association.rb +62 -0
  89. data/lib/rails_admin_next/config/fields/types/action_text.rb +20 -0
  90. data/lib/rails_admin_next/config/fields/types/active_record_enum.rb +62 -0
  91. data/lib/rails_admin_next/config/fields/types/active_storage.rb +70 -0
  92. data/lib/rails_admin_next/config/fields/types/all.rb +32 -0
  93. data/lib/rails_admin_next/config/fields/types/belongs_to_association.rb +48 -0
  94. data/lib/rails_admin_next/config/fields/types/boolean.rb +70 -0
  95. data/lib/rails_admin_next/config/fields/types/citext.rb +15 -0
  96. data/lib/rails_admin_next/config/fields/types/color.rb +43 -0
  97. data/lib/rails_admin_next/config/fields/types/date.rb +38 -0
  98. data/lib/rails_admin_next/config/fields/types/datetime.rb +79 -0
  99. data/lib/rails_admin_next/config/fields/types/decimal.rb +23 -0
  100. data/lib/rails_admin_next/config/fields/types/enum.rb +52 -0
  101. data/lib/rails_admin_next/config/fields/types/file_upload.rb +87 -0
  102. data/lib/rails_admin_next/config/fields/types/float.rb +23 -0
  103. data/lib/rails_admin_next/config/fields/types/has_and_belongs_to_many_association.rb +16 -0
  104. data/lib/rails_admin_next/config/fields/types/has_many_association.rb +16 -0
  105. data/lib/rails_admin_next/config/fields/types/has_one_association.rb +35 -0
  106. data/lib/rails_admin_next/config/fields/types/hidden.rb +31 -0
  107. data/lib/rails_admin_next/config/fields/types/inet.rb +15 -0
  108. data/lib/rails_admin_next/config/fields/types/integer.rb +20 -0
  109. data/lib/rails_admin_next/config/fields/types/json.rb +37 -0
  110. data/lib/rails_admin_next/config/fields/types/multiple_active_storage.rb +82 -0
  111. data/lib/rails_admin_next/config/fields/types/multiple_file_upload.rb +134 -0
  112. data/lib/rails_admin_next/config/fields/types/numeric.rb +24 -0
  113. data/lib/rails_admin_next/config/fields/types/password.rb +45 -0
  114. data/lib/rails_admin_next/config/fields/types/polymorphic_association.rb +118 -0
  115. data/lib/rails_admin_next/config/fields/types/serialized.rb +28 -0
  116. data/lib/rails_admin_next/config/fields/types/string.rb +50 -0
  117. data/lib/rails_admin_next/config/fields/types/string_like.rb +25 -0
  118. data/lib/rails_admin_next/config/fields/types/text.rb +28 -0
  119. data/lib/rails_admin_next/config/fields/types/time.rb +39 -0
  120. data/lib/rails_admin_next/config/fields/types/timestamp.rb +16 -0
  121. data/lib/rails_admin_next/config/fields/types/uuid.rb +15 -0
  122. data/lib/rails_admin_next/config/fields/types.rb +29 -0
  123. data/lib/rails_admin_next/config/fields.rb +88 -0
  124. data/lib/rails_admin_next/config/groupable.rb +20 -0
  125. data/lib/rails_admin_next/config/has_description.rb +14 -0
  126. data/lib/rails_admin_next/config/has_fields.rb +144 -0
  127. data/lib/rails_admin_next/config/has_groups.rb +27 -0
  128. data/lib/rails_admin_next/config/hideable.rb +30 -0
  129. data/lib/rails_admin_next/config/inspectable.rb +39 -0
  130. data/lib/rails_admin_next/config/lazy_model.rb +74 -0
  131. data/lib/rails_admin_next/config/model.rb +126 -0
  132. data/lib/rails_admin_next/config/proxyable/proxy.rb +42 -0
  133. data/lib/rails_admin_next/config/proxyable.rb +26 -0
  134. data/lib/rails_admin_next/config/sections/base.rb +36 -0
  135. data/lib/rails_admin_next/config/sections/create.rb +13 -0
  136. data/lib/rails_admin_next/config/sections/edit.rb +13 -0
  137. data/lib/rails_admin_next/config/sections/export.rb +13 -0
  138. data/lib/rails_admin_next/config/sections/list.rb +63 -0
  139. data/lib/rails_admin_next/config/sections/modal.rb +12 -0
  140. data/lib/rails_admin_next/config/sections/nested.rb +12 -0
  141. data/lib/rails_admin_next/config/sections/show.rb +12 -0
  142. data/lib/rails_admin_next/config/sections/update.rb +12 -0
  143. data/lib/rails_admin_next/config/sections.rb +39 -0
  144. data/lib/rails_admin_next/config.rb +436 -0
  145. data/lib/rails_admin_next/engine.rb +91 -0
  146. data/lib/rails_admin_next/extension.rb +35 -0
  147. data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +90 -0
  148. data/lib/rails_admin_next/extensions/cancancan.rb +5 -0
  149. data/lib/rails_admin_next/extensions/controller_extension.rb +8 -0
  150. data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +192 -0
  151. data/lib/rails_admin_next/extensions/paper_trail.rb +5 -0
  152. data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +72 -0
  153. data/lib/rails_admin_next/extensions/pundit.rb +5 -0
  154. data/lib/rails_admin_next/extensions/url_for_extension.rb +15 -0
  155. data/lib/rails_admin_next/icons.rb +143 -0
  156. data/lib/rails_admin_next/support/composite_keys_serializer.rb +15 -0
  157. data/lib/rails_admin_next/support/csv_converter.rb +100 -0
  158. data/lib/rails_admin_next/support/hash_helper.rb +31 -0
  159. data/lib/rails_admin_next/support/paginated_collection.rb +79 -0
  160. data/lib/rails_admin_next/version.rb +17 -0
  161. data/lib/rails_admin_next.rb +72 -0
  162. data/lib/tasks/rails_admin.rake +40 -0
  163. data/src/rails_admin/application.js +15 -0
  164. data/src/rails_admin/base.js +12 -0
  165. data/src/rails_admin/controllers/alert_controller.js +28 -0
  166. data/src/rails_admin/controllers/collapse_controller.js +16 -0
  167. data/src/rails_admin/controllers/dropdown_controller.js +110 -0
  168. data/src/rails_admin/controllers/feedback_controller.js +73 -0
  169. data/src/rails_admin/controllers/filter_box_controller.js +346 -0
  170. data/src/rails_admin/controllers/filtering_multiselect_controller.js +386 -0
  171. data/src/rails_admin/controllers/filtering_select_controller.js +509 -0
  172. data/src/rails_admin/controllers/index.js +31 -0
  173. data/src/rails_admin/controllers/modal_controller.js +142 -0
  174. data/src/rails_admin/controllers/nested_form_controller.js +117 -0
  175. data/src/rails_admin/controllers/polymorphic_controller.js +26 -0
  176. data/src/rails_admin/controllers/remote_form_controller.js +173 -0
  177. data/src/rails_admin/controllers/sidescroll_controller.js +23 -0
  178. data/src/rails_admin/dom-ready.js +20 -0
  179. data/src/rails_admin/dom.js +19 -0
  180. data/src/rails_admin/i18n.js +20 -0
  181. data/src/rails_admin/icons.js +64 -0
  182. data/src/rails_admin/interactions.js +221 -0
  183. data/src/rails_admin/select_query.js +65 -0
  184. metadata +346 -0
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ module ApplicationHelper
5
+ def authorized?(action_name, abstract_model = nil, object = nil)
6
+ object = nil if object.try :new_record?
7
+ action(action_name, abstract_model, object).try(:authorized?)
8
+ end
9
+
10
+ def current_action
11
+ params[:action].in?(%w[create new]) ? "create" : "update"
12
+ end
13
+
14
+ def current_action?(action, abstract_model = @abstract_model, object = @object)
15
+ @action.custom_key == action.custom_key &&
16
+ abstract_model.try(:to_param) == @abstract_model.try(:to_param) &&
17
+ (@object.try(:persisted?) ? @object.id == object.try(:id) : !object.try(:persisted?))
18
+ end
19
+
20
+ def action(key, abstract_model = nil, object = nil)
21
+ RailsAdminNext::Config::Actions.find(key, controller: controller, abstract_model: abstract_model, object: object)
22
+ end
23
+
24
+ # Render an engine icon as inline SVG. `name` is a logical icon name (see
25
+ # RailsAdminNext::Icons::GLYPHS), e.g. :edit, :delete, :list. A nil/unknown
26
+ # name renders nothing rather than raising; a legacy Font Awesome class string
27
+ # is resolved best-effort and warns via RailsAdminNext.deprecator.
28
+ def rails_admin_icon(name, **html_options)
29
+ RailsAdminNext::Icons.svg(name, **html_options)&.html_safe || "".html_safe
30
+ end
31
+
32
+ def actions(scope = :all, abstract_model = nil, object = nil)
33
+ RailsAdminNext::Config::Actions.all(scope, controller: controller, abstract_model: abstract_model, object: object)
34
+ end
35
+
36
+ def edit_user_link
37
+ return nil unless _current_user.try(:email).present?
38
+ return nil unless (abstract_model = RailsAdminNext.config(_current_user.class).abstract_model)
39
+
40
+ edit_action = action(:edit, abstract_model, _current_user)
41
+ authorized = edit_action.try(:authorized?)
42
+ content = edit_user_link_label
43
+
44
+ if authorized
45
+ edit_url = rails_admin_next.url_for(
46
+ action: edit_action.action_name,
47
+ model_name: abstract_model.to_param,
48
+ controller: "rails_admin_next/main",
49
+ id: _current_user.id
50
+ )
51
+
52
+ link_to content, edit_url, class: "nav-link"
53
+ else
54
+ content_tag :span, content, class: "nav-link"
55
+ end
56
+ end
57
+
58
+ def logout_path
59
+ main_app.logout_path if main_app.respond_to?(:logout_path)
60
+ end
61
+
62
+ def logout_method
63
+ :delete
64
+ end
65
+
66
+ def wording_for(label, action = @action, abstract_model = @abstract_model, object = @object)
67
+ model_config = abstract_model.try(:config)
68
+ object = nil unless abstract_model && object.is_a?(abstract_model.model)
69
+ action = RailsAdminNext::Config::Actions.find(action.to_sym) if action.is_a?(Symbol) || action.is_a?(String)
70
+
71
+ I18n.t(
72
+ "admin.actions.#{action.i18n_key}.#{label}",
73
+ model_label: model_config&.label,
74
+ model_label_plural: model_config&.label_plural,
75
+ object_label: model_config && object.try(model_config.object_label_method)
76
+ )
77
+ end
78
+
79
+ def main_navigation
80
+ nodes_stack = RailsAdminNext::Config.visible_models(controller: controller)
81
+ node_model_names = nodes_stack.collect { |c| c.abstract_model.model_name }
82
+ parent_groups = nodes_stack.group_by { |n| n.parent&.to_s }
83
+
84
+ nodes_stack.group_by(&:navigation_label).collect do |navigation_label, nodes|
85
+ nodes = nodes.select { |n| n.parent.nil? || !n.parent.to_s.in?(node_model_names) }
86
+ li_stack = navigation parent_groups, nodes
87
+
88
+ label = navigation_label || t("admin.misc.navigation")
89
+
90
+ collapsible_stack(label, "main", li_stack)
91
+ end.join.html_safe
92
+ end
93
+
94
+ def root_navigation
95
+ actions(:root).select(&:show_in_sidebar).group_by(&:sidebar_label).collect do |label, nodes|
96
+ li_stack = nodes.map do |node|
97
+ url = rails_admin_next.url_for(action: node.action_name, controller: "rails_admin_next/main")
98
+ nav_icon = rails_admin_icon(node.link_icon)
99
+ content_tag :li do
100
+ link_to nav_icon + " " + wording_for(:menu, node), url, class: "nav-link"
101
+ end
102
+ end.join.html_safe
103
+ label ||= t("admin.misc.root_navigation")
104
+
105
+ collapsible_stack(label, "action", li_stack)
106
+ end.join.html_safe
107
+ end
108
+
109
+ def static_navigation
110
+ li_stack = RailsAdminNext::Config.navigation_static_links.collect do |title, url|
111
+ content_tag(:li, link_to(title.to_s, url, target: "_blank", rel: "noopener noreferrer", class: "nav-link"))
112
+ end.join.html_safe
113
+
114
+ label = RailsAdminNext::Config.navigation_static_label || t("admin.misc.navigation_static_label")
115
+ collapsible_stack(label, "static", li_stack) || ""
116
+ end
117
+
118
+ def navigation(parent_groups, nodes, level = 0)
119
+ nodes.collect do |node|
120
+ abstract_model = node.abstract_model
121
+ model_param = abstract_model.to_param
122
+ url = rails_admin_next.url_for(action: :index, controller: "rails_admin_next/main", model_name: model_param)
123
+ nav_icon = rails_admin_icon(node.navigation_icon)
124
+ css_classes = ["nav-link"]
125
+ css_classes.push("nav-level-#{level}") if level > 0
126
+ css_classes.push("active") if @action && current_action?(@action, model_param)
127
+ li = content_tag :li, data: {model: model_param} do
128
+ link_to nav_icon + " " + node.label_plural, url, class: css_classes.join(" ")
129
+ end
130
+ child_nodes = parent_groups[abstract_model.model_name]
131
+ child_nodes ? li + navigation(parent_groups, child_nodes, level + 1) : li
132
+ end.join.html_safe
133
+ end
134
+
135
+ def breadcrumb(action = @action, _acc = [])
136
+ begin
137
+ (parent_actions ||= []) << action
138
+ end while action.breadcrumb_parent && (action = action(*action.breadcrumb_parent)) # rubocop:disable Lint/Loop
139
+
140
+ content_tag(:ol, class: "breadcrumb") do
141
+ parent_actions.collect do |a|
142
+ am = a.send(:eval, "bindings[:abstract_model]")
143
+ o = a.send(:eval, "bindings[:object]")
144
+ content_tag(:li, class: ["breadcrumb-item", current_action?(a, am, o) && "active"]) do
145
+ if current_action?(a, am, o)
146
+ wording_for(:breadcrumb, a, am, o)
147
+ elsif a.http_methods.include?(:get)
148
+ link_to rails_admin_next.url_for(action: a.action_name, controller: "rails_admin_next/main", model_name: am.try(:to_param), id: o.try(:persisted?) && o.try(:id) || nil) do
149
+ wording_for(:breadcrumb, a, am, o)
150
+ end
151
+ else
152
+ content_tag(:span, wording_for(:breadcrumb, a, am, o))
153
+ end
154
+ end
155
+ end.reverse.join.html_safe
156
+ end
157
+ end
158
+
159
+ # parent => :root, :collection, :member
160
+ # perf matters here (no action view trickery)
161
+ def menu_for(parent, abstract_model = nil, object = nil, only_icon = false)
162
+ actions = actions(parent, abstract_model, object).select { |a| a.http_methods.include?(:get) && a.show_in_menu }
163
+ actions.collect do |action|
164
+ wording = wording_for(:menu, action)
165
+ li_class = ["nav-item", "icon", "#{action.key}_#{parent}_link"]
166
+ .concat(action.enabled? ? [] : ["disabled"])
167
+ content_tag(:li, {class: li_class}.merge(only_icon ? {title: wording, rel: "tooltip"} : {})) do
168
+ label = rails_admin_icon(action.link_icon) + " " + content_tag(:span, wording, (only_icon ? {style: "display:none"} : {}))
169
+ if action.enabled? || !only_icon
170
+ href =
171
+ if action.enabled?
172
+ rails_admin_next.url_for(action: action.action_name, controller: "rails_admin_next/main", model_name: abstract_model.try(:to_param), id: object.try(:persisted?) && object.try(:id) || nil)
173
+ else
174
+ "javascript:void(0)"
175
+ end
176
+ content_tag(:a, label, {href: href, target: action.link_target, class: ["nav-link", current_action?(action) && "active", !action.enabled? && "disabled"].compact}.merge(action.turbo? ? {} : {data: {turbo: "false"}}))
177
+ else
178
+ content_tag(:span, label)
179
+ end
180
+ end
181
+ end.join(" ").html_safe
182
+ end
183
+
184
+ def bulk_menu(abstract_model = @abstract_model)
185
+ actions = actions(:bulkable, abstract_model)
186
+ return "" if actions.empty?
187
+
188
+ content_tag :li, class: "nav-item dropdown" do
189
+ content_tag(:button, type: "button", id: "bulk_menu_toggle", class: "nav-link dropdown-toggle", popovertarget: "bulk_menu", data: {controller: "dropdown", action: "keydown->dropdown#onKeydown"}) { t("admin.misc.bulk_menu_title").html_safe + " " + '<b class="caret"></b>'.html_safe } +
190
+ content_tag(:ul, id: "bulk_menu", class: "dropdown-menu dropdown-menu-end", role: "menu", popover: "auto") do
191
+ actions.collect do |action|
192
+ content_tag :li, role: "none" do
193
+ link_to wording_for(:bulk_link, action, abstract_model), "#", class: "dropdown-item bulk-link", role: "menuitem", tabindex: "-1", data: {action: action.action_name}
194
+ end
195
+ end.join.html_safe
196
+ end
197
+ end.html_safe
198
+ end
199
+
200
+ def flash_alert_class(flash_key)
201
+ case flash_key.to_s
202
+ when "error" then "alert-danger"
203
+ when "alert" then "alert-warning"
204
+ when "notice" then "alert-info"
205
+ else "alert-#{flash_key}"
206
+ end
207
+ end
208
+
209
+ # Workaround for https://github.com/rails/rails/issues/31325
210
+ def image_tag(source, options = {})
211
+ if %w[ActiveStorage::Variant ActiveStorage::VariantWithRecord ActiveStorage::Preview].include? source.class.to_s
212
+ super(main_app.route_for(ActiveStorage.resolve_model_to_route, source), options)
213
+ else
214
+ super
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ def edit_user_link_label
221
+ [
222
+ RailsAdminNext::Config.show_gravatar &&
223
+ image_tag(gravatar_url(_current_user.email), alt: ""),
224
+
225
+ content_tag(:span, _current_user.email)
226
+ ].filter(&:present?).join.html_safe
227
+ end
228
+
229
+ def gravatar_url(email)
230
+ "https://secure.gravatar.com/avatar/#{Digest::MD5.hexdigest email}?s=30"
231
+ end
232
+
233
+ # Native disclosure: the group renders open and the browser owns toggling.
234
+ # The MD5-derived key is stable across renders, so a `turbo:before-render`
235
+ # hook (src/rails_admin/interactions.js) can carry a collapsed group's state
236
+ # into the next page. Find-in-page may auto-expand a collapsed group; that's
237
+ # accepted <details> platform behavior.
238
+ def collapsible_stack(label, class_prefix, li_stack)
239
+ return nil unless li_stack.present?
240
+
241
+ collapse_key = "#{class_prefix}-#{Digest::MD5.hexdigest(label)[0..7]}"
242
+ content_tag(:li, class: "mb-1") do
243
+ content_tag(:details, open: true, data: {collapse_key: collapse_key}) do
244
+ content_tag(:summary, class: "btn btn-toggle align-items-center rounded") do
245
+ rails_admin_icon(:collapse) + html_escape(" " + label)
246
+ end +
247
+ content_tag(:ul, class: "btn-toggle-nav list-unstyled fw-normal pb-1") do
248
+ li_stack
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ class FormBuilder < ::ActionView::Helpers::FormBuilder
5
+ include ::RailsAdminNext::ApplicationHelper
6
+
7
+ def generate(options = {})
8
+ without_field_error_proc_added_div do
9
+ options.reverse_merge!(
10
+ action: @template.controller.params[:action],
11
+ model_config: @template.instance_variable_get(:@model_config),
12
+ nested_in: false
13
+ )
14
+
15
+ object_infos +
16
+ visible_groups(options[:model_config], generator_action(options[:action], options[:nested_in])).collect do |fieldset|
17
+ fieldset_for fieldset, options[:nested_in]
18
+ end.join.html_safe +
19
+ (options[:nested_in] ? "" : @template.render(partial: "rails_admin_next/main/submit_buttons"))
20
+ end
21
+ end
22
+
23
+ def fieldset_for(fieldset, nested_in)
24
+ fields = fieldset.with(
25
+ form: self,
26
+ object: @object,
27
+ view: @template,
28
+ controller: @template.controller
29
+ ).visible_fields
30
+ return if fields.empty?
31
+
32
+ @template.content_tag :fieldset do
33
+ contents = []
34
+ chevron = RailsAdminNext::Icons.svg(:collapse, class: ("collapsed" unless fieldset.active?)).html_safe
35
+ legend = @template.safe_join([chevron, " #{fieldset.label}"])
36
+ contents << @template.content_tag(:legend, legend, style: (fieldset.name == :default) ? "display:none" : "")
37
+ contents << @template.content_tag(:p, fieldset.help) if fieldset.help.present?
38
+ contents << fields.collect { |field| field_wrapper_for(field, nested_in) }.join
39
+ contents.join.html_safe
40
+ end
41
+ end
42
+
43
+ def field_wrapper_for(field, nested_in)
44
+ # do not show nested field if the target is the origin
45
+ return if nested_field_association?(field, nested_in)
46
+
47
+ @template.content_tag(:div, class: "control-group row mb-3 #{field.type_css_class} #{field.css_class} #{"error" if field.errors.present?}", id: "#{dom_id(field)}_field") do
48
+ if field.label
49
+ label(field.method_name, field.label, class: "col-sm-2 col-form-label text-md-end") +
50
+ (field.nested_form ? field_for(field) : input_for(field))
51
+ else
52
+ field.nested_form ? field_for(field) : input_for(field)
53
+ end
54
+ end
55
+ end
56
+
57
+ def input_for(field)
58
+ css = "col-sm-10 controls"
59
+ css += " has-error" if field.errors.present?
60
+ @template.content_tag(:div, class: css) do
61
+ field_for(field) +
62
+ errors_for(field) +
63
+ help_for(field)
64
+ end
65
+ end
66
+
67
+ def errors_for(field)
68
+ field.errors.present? ? @template.content_tag(:span, field.errors.to_sentence, class: "help-inline text-danger") : "".html_safe
69
+ end
70
+
71
+ def help_for(field)
72
+ field.help.present? ? @template.content_tag(:div, field.help, class: "form-text") : "".html_safe
73
+ end
74
+
75
+ def field_for(field)
76
+ field.read_only? ? @template.content_tag(:div, field.pretty_value, class: "form-control-static") : field.render
77
+ end
78
+
79
+ # Wraps one nested-attributes subform (rendered by native `fields_for`) for
80
+ # the nested-form Stimulus controller: a `.fields` container, an optional
81
+ # `_destroy` hidden field + remove button (gated on `allow_destroy`, always
82
+ # present in the blank <template>), then the recursively generated fields.
83
+ def nested_field_set(nested_form, field, allow_destroy:)
84
+ @template.content_tag(:div, class: "fields tab-pane active", data: {"nested-form-target": "subform"}) do
85
+ contents = []
86
+ # Keep the persisted-record id inside the `.fields` wrapper (Rails would
87
+ # otherwise append it as a sibling); the controller reads it to tell a
88
+ # persisted subform from a brand-new one. Suppress the auto-id in the
89
+ # partials with `include_id: false` so it is not rendered twice.
90
+ contents << nested_form.hidden_field(:id) if nested_form.object.persisted?
91
+ if allow_destroy
92
+ contents << nested_form.hidden_field(:_destroy, data: {"nested-form-target": "destroy"})
93
+ contents << @template.content_tag(:button, RailsAdminNext::Icons.svg(:trash).html_safe, type: "button", class: "btn btn-danger remove_nested_fields", data: {action: "nested-form#remove"})
94
+ end
95
+ contents << nested_form.generate(action: :nested, model_config: field.associated_model_config, nested_in: field)
96
+ contents.join.html_safe
97
+ end
98
+ end
99
+
100
+ def object_infos
101
+ model_config = RailsAdminNext.config(object)
102
+ model_label = model_config.label
103
+ object_label =
104
+ if object.new_record?
105
+ I18n.t("admin.form.new_model", name: model_label)
106
+ else
107
+ object.send(model_config.object_label_method).presence || "#{model_config.label} ##{object.id}"
108
+ end
109
+
110
+ %(<span hidden class="object-infos" data-model-label="#{CGI.escapeHTML(model_label.to_s)}" data-object-label="#{CGI.escapeHTML(object_label.to_s)}"></span>).html_safe
111
+ end
112
+
113
+ def dom_id(field)
114
+ (@dom_id ||= {})[field.name] ||=
115
+ [
116
+ @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, ""),
117
+ options[:index],
118
+ field.method_name
119
+ ].reject(&:blank?).join("_")
120
+ end
121
+
122
+ def dom_name(field)
123
+ (@dom_name ||= {})[field.name] ||= %(#{@object_name}#{options[:index] && "[#{options[:index]}]"}[#{field.method_name}]#{"[]" if field.is_a?(Config::Fields::Association) && field.multiple?})
124
+ end
125
+
126
+ def hidden_field(method, options = {})
127
+ if method == :id && object.id.is_a?(Array)
128
+ super(method, {value: RailsAdminNext.config.composite_keys_serializer.serialize(object.id)})
129
+ else
130
+ super
131
+ end
132
+ end
133
+
134
+ protected
135
+
136
+ def generator_action(action, nested)
137
+ if nested
138
+ action = :nested
139
+ elsif @template.request.format == "text/javascript"
140
+ action = :modal
141
+ end
142
+
143
+ action
144
+ end
145
+
146
+ def visible_groups(model_config, action)
147
+ model_config.send(action).with(
148
+ form: self,
149
+ object: @object,
150
+ view: @template,
151
+ controller: @template.controller
152
+ ).visible_groups
153
+ end
154
+
155
+ def without_field_error_proc_added_div
156
+ default_field_error_proc = ::ActionView::Base.field_error_proc
157
+ begin
158
+ ::ActionView::Base.field_error_proc = proc { |html_tag, _instance| html_tag }
159
+ yield
160
+ ensure
161
+ ::ActionView::Base.field_error_proc = default_field_error_proc
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def nested_field_association?(field, nested_in)
168
+ field.inverse_of.presence && nested_in.presence && field.inverse_of == nested_in.name &&
169
+ (@template.instance_variable_get(:@model_config).abstract_model == field.abstract_model ||
170
+ field.name == nested_in.inverse_of)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ module MainHelper
5
+ def rails_admin_form_for(*args, &)
6
+ options = args.extract_options!.reverse_merge(builder: RailsAdminNext::FormBuilder)
7
+ (options[:html] ||= {})[:novalidate] ||= !RailsAdminNext::Config.browser_validations
8
+ html_data = (options[:html][:data] ||= {})
9
+ html_data[:controller] = [html_data[:controller], "feedback"].compact.join(" ")
10
+ html_data[:action] = [html_data[:action], "turbo:submit-start->feedback#start turbo:submit-end->feedback#end"].compact.join(" ")
11
+
12
+ form_for(*(args << options), &)
13
+ end
14
+
15
+ def get_indicator(percent)
16
+ return "" if percent < 0 # none
17
+ return "info" if percent < 34 # < 1/100 of max
18
+ return "success" if percent < 67 # < 1/10 of max
19
+ return "warning" if percent < 84 # < 1/3 of max
20
+
21
+ "danger" # > 1/3 of max
22
+ end
23
+
24
+ def filterable_fields
25
+ @filterable_fields ||= @model_config.list.fields.select(&:filterable?)
26
+ end
27
+
28
+ def ordered_filters
29
+ return @ordered_filters if @ordered_filters.present?
30
+
31
+ @index = 0
32
+ @ordered_filters = (params[:f].try(:permit!).try(:to_h) || @model_config.list.filters).each_with_object({}) do |filter, memo|
33
+ field_name = filter.is_a?(Array) ? filter.first : filter
34
+ (filter.is_a?(Array) ? filter.last : {(@index += 1) => {"v" => ""}}).each do |index, filter_hash|
35
+ if filter_hash["disabled"].blank?
36
+ memo[index] = {field_name => filter_hash}
37
+ else
38
+ params[:f].delete(field_name)
39
+ end
40
+ end
41
+ end.to_a.sort_by(&:first)
42
+ end
43
+
44
+ def ordered_filter_options
45
+ if ordered_filters
46
+ @ordered_filter_options ||= ordered_filters.map do |duplet|
47
+ filter_for_field = duplet[1]
48
+ filter_name = filter_for_field.keys.first
49
+ filter_hash = filter_for_field.values.first
50
+ unless (field = filterable_fields.find { |f| f.name == filter_name.to_sym }&.with({view: self}))
51
+ raise "#{filter_name} is not currently filterable; filterable fields are #{filterable_fields.map(&:name).join(", ")}"
52
+ end
53
+
54
+ field.filter_options.merge(
55
+ index: duplet[0],
56
+ operator: filter_hash["o"] || field.default_filter_operator,
57
+ value: filter_hash["v"]
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,29 @@
1
+ <%# locals: () -%>
2
+ <meta content="IE=edge" http-equiv="X-UA-Compatible">
3
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
4
+ <meta content="width=device-width, initial-scale=1" name="viewport; charset=utf-8">
5
+ <meta content="NONE,NOARCHIVE" name="robots">
6
+ <meta content="false" name="turbo-prefetch">
7
+ <%= csrf_meta_tag %>
8
+ <%# The scheme every light-dark() token pair in the engine CSS resolves against. Placed before the
9
+ stylesheet link so the scheme is known before first paint; a meta (not an inline style) keeps
10
+ CSP out of the picture and lets :light/:dark pins override the OS preference. %>
11
+ <meta content="<%= {auto: 'light dark', light: 'light', dark: 'dark'}.fetch(RailsAdminNext.config.color_scheme) %>" name="color-scheme">
12
+ <%# The entry stylesheet pulls tokens/framework/skin via @import, which the browser only discovers
13
+ after fetching and parsing the entry file. Preload the three targets so all four fetch in parallel. %>
14
+ <link rel="preload" as="style" href="<%= asset_path('rails_admin/tokens.css') %>">
15
+ <link rel="preload" as="style" href="<%= asset_path('rails_admin/framework.css') %>">
16
+ <link rel="preload" as="style" href="<%= asset_path('rails_admin/skin.css') %>">
17
+ <%# Date/time fields use native HTML5 inputs; the picker UI is the browser's. Icons are inline SVG (no icon font). %>
18
+ <%= stylesheet_link_tag "rails_admin", media: :all, data: {'turbo-track': 'reload'} %>
19
+ <%# Rich-text (ActionText) fields render a Trix editor; load its stylesheet so the toolbar is styled.
20
+ trix.css ships with the action_text-trix gem and is on the Propshaft load path when ActionText is used.
21
+ NOTE: trix.css is light-only for now — dark styling for the Trix toolbar/editor is deferred. %>
22
+ <% if defined?(ActionText::Engine) %>
23
+ <%= stylesheet_link_tag "trix", media: :all, data: {'turbo-track': 'reload'} %>
24
+ <% end %>
25
+ <%# Zero-build delivery: browser-native ESM pinned by the engine-owned importmap, served by Propshaft.
26
+ The importmap-rails helpers thread the request CSP nonce onto every inline <script> they emit. %>
27
+ <%= javascript_inline_importmap_tag(RailsAdminNext::Engine.importmap.to_json(resolver: self)) %>
28
+ <%= javascript_importmap_module_preload_tags(RailsAdminNext::Engine.importmap) %>
29
+ <%= javascript_import_module_tag 'rails_admin' %>
@@ -0,0 +1,18 @@
1
+ <%# locals: () -%>
2
+ <div class="container-fluid" data-controller="collapse">
3
+ <a class="navbar-brand" href="<%= dashboard_path %>">
4
+ Rails
5
+ <small>
6
+ AdminNext
7
+ </small>
8
+ </a>
9
+ <button class="navbar-toggler" type="button" data-collapse-target="trigger" data-action="collapse#toggle" aria-expanded="false" aria-controls="secondary-navigation">
10
+ <span class="sr-only">
11
+ <%= t('admin.toggle_navigation') %>
12
+ </span>
13
+ <span class="navbar-toggler-icon"></span>
14
+ </button>
15
+ <div class="collapse navbar-collapse" id="secondary-navigation" data-collapse-target="region">
16
+ <%= render partial: 'layouts/rails_admin_next/secondary_navigation' %>
17
+ </div>
18
+ </div>
@@ -0,0 +1,27 @@
1
+ <%# locals: () -%>
2
+ <ul class="navbar-nav ms-auto root_links">
3
+ <% actions(:root).select(&:show_in_navigation).each do |action| %>
4
+ <li class="nav-item <%= action.action_name %>_root_link">
5
+ <%= link_to wording_for(:menu, action), { action: action.action_name, controller: 'rails_admin_next/main' }, {class: ['nav-link']}.merge(action.turbo? ? {} : {data: {turbo: 'false'}}) %>
6
+ </li>
7
+ <% end %>
8
+ <% if main_app_root_path = (main_app.root_path rescue false) %>
9
+ <li class="nav-item">
10
+ <%= link_to t('admin.home.name'), main_app_root_path, class: 'nav-link' %>
11
+ </li>
12
+ <% end %>
13
+ <% if _current_user %>
14
+ <% if user_link = edit_user_link %>
15
+ <li class="nav-item edit_user_root_link">
16
+ <%= user_link %>
17
+ </li>
18
+ <% end %>
19
+ <% if logout_path.present? %>
20
+ <li class="nav-item">
21
+ <%= link_to logout_path, class: 'nav-link', data: {turbo_method: logout_method} do %>
22
+ <span class="badge bg-danger"><%= t('admin.misc.log_out') %></span>
23
+ <% end %>
24
+ </li>
25
+ <% end %>
26
+ <% end %>
27
+ </ul>
@@ -0,0 +1,6 @@
1
+ <%# locals: () -%>
2
+ <ul class="sidebar col-sm-3 col-md-2 nav btn-toggle-nav flex-column flex-nowrap bg-light">
3
+ <%= main_navigation %>
4
+ <%= root_navigation %>
5
+ <%= static_navigation %>
6
+ </ul>
@@ -0,0 +1,42 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <%= render "layouts/rails_admin_next/head" %>
5
+ </head>
6
+ <body class="rails_admin">
7
+ <div data-i18n-options="<%= I18n.t("admin.js").to_json %>" id="admin-js"></div>
8
+ <div class="badge bg-warning" id="loading" style="display:none; position:fixed; right:20px; bottom:20px; z-index:100000">
9
+ <%= t('admin.loading') %>
10
+ </div>
11
+ <nav class="navbar navbar-expand-md fixed-top <%= RailsAdminNext::Config.navbar_css_classes.join(' ') %>">
12
+ <%= render "layouts/rails_admin_next/navigation" %>
13
+ </nav>
14
+ <div class="container-fluid">
15
+ <div class="row">
16
+ <div class="col-sm-3 col-md-2 flex-wrap p-0">
17
+ <%= render "layouts/rails_admin_next/sidebar_navigation" %>
18
+ </div>
19
+ <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
20
+ <div class="container-fluid">
21
+ <%= render template: 'layouts/rails_admin_next/content' %>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ <%# Native dialog: the platform owns the top layer, focus trap, Escape and
27
+ (via closedby="any") backdrop light-dismissal. The title is seeded with
28
+ the loading string and the body flagged busy so a just-opened dialog
29
+ has an accessible name and state before the async form arrives. %>
30
+ <dialog id="modal" closedby="any" aria-labelledby="modal-title" data-controller="modal">
31
+ <div class="modal-header">
32
+ <h3 class="modal-header-title" id="modal-title" data-modal-target="title"><%= t('admin.loading') %></h3>
33
+ <button type="button" class="btn-close" data-action="modal#close" aria-label="<%= t('admin.form.cancel') %>"></button>
34
+ </div>
35
+ <div class="modal-body" aria-busy="true" data-modal-target="body"></div>
36
+ <div class="modal-footer">
37
+ <a href="#" class="btn cancel-action" data-modal-target="cancelButton" data-action="modal#close"></a>
38
+ <a href="#" class="btn btn-primary save-action" data-modal-target="saveButton" data-action="modal#save"></a>
39
+ </div>
40
+ </dialog>
41
+ </body>
42
+ </html>
@@ -0,0 +1,22 @@
1
+ <title>
2
+ <%= "#{@abstract_model.try(:pretty_name) || @page_name} | Rails AdminNext" %>
3
+ </title>
4
+ <header class="py-2 m-2 border-bottom" data-model="<%= @abstract_model.to_param %>">
5
+ <h1>
6
+ <%= @page_name %>
7
+ </h1>
8
+ </header>
9
+ <% flash && flash.each do |key, value| %>
10
+ <div class="<%= flash_alert_class(key) %> alert alert-dismissible" data-controller="alert">
11
+ <%= value %>
12
+ <button type="button" class="btn-close" data-action="alert#dismiss" aria-label="Close"></button>
13
+ </div>
14
+ <% end %>
15
+ <nav aria-label="breadcrumb">
16
+ <%= breadcrumb %>
17
+ </nav>
18
+ <ul class="nav nav-tabs mb-3">
19
+ <%= menu_for((@abstract_model ? (@object.try(:persisted?) ? :member : :collection) : :root), @abstract_model, @object) %>
20
+ <%= content_for :contextual_tabs %>
21
+ </ul>
22
+ <%= yield %>
@@ -0,0 +1,7 @@
1
+ <% flash && flash.each do |key, value| %>
2
+ <div class="<%= flash_alert_class(key) %> alert alert-dismissible" data-controller="alert">
3
+ <%= value %>
4
+ <button type="button" class="btn-close" data-action="alert#dismiss" aria-label="Close"></button>
5
+ </div>
6
+ <% end %>
7
+ <%= yield %>