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,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/strip"
4
+
5
+ module RailsAdminNext
6
+ module Extensions
7
+ module PaperTrail
8
+ class VersionProxy
9
+ def initialize(version, user_class = User)
10
+ @version = version
11
+ @user_class = user_class
12
+ end
13
+
14
+ def message
15
+ @message = @version.event
16
+ (@version.respond_to?(:changeset) && @version.changeset.present?) ? @message + " [" + @version.changeset.to_a.collect { |c| "#{c[0]} = #{c[1][1]}" }.join(", ") + "]" : @message
17
+ end
18
+
19
+ def created_at
20
+ @version.created_at
21
+ end
22
+
23
+ def table
24
+ @version.item_type
25
+ end
26
+
27
+ def username
28
+ begin
29
+ @user_class.find(@version.whodunnit).try(:email)
30
+ rescue
31
+ nil
32
+ end || @version.whodunnit
33
+ end
34
+
35
+ def item
36
+ @version.item_id
37
+ end
38
+ end
39
+
40
+ module ControllerExtension
41
+ def user_for_paper_trail
42
+ _current_user.try(:id) || _current_user
43
+ end
44
+ end
45
+
46
+ class AuditingAdapter
47
+ COLUMN_MAPPING = {
48
+ table: :item_type,
49
+ username: :whodunnit,
50
+ item: :item_id,
51
+ created_at: :created_at,
52
+ message: :event
53
+ }.freeze
54
+ E_USER_CLASS_NOT_SET = <<~ERROR
55
+ Please set up PaperTrail's user class explicitly.
56
+
57
+ config.audit_with :paper_trail do
58
+ user_class { User }
59
+ end
60
+ ERROR
61
+ E_VERSION_MODEL_NOT_SET = <<~ERROR
62
+ Please set up PaperTrail's version model explicitly.
63
+
64
+ config.audit_with :paper_trail do
65
+ version_class { PaperTrail::Version }
66
+ end
67
+
68
+ If you have configured a model to use a custom version class
69
+ (https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes)
70
+ that configuration will take precedence over what you specify in `audit_with`.
71
+ ERROR
72
+
73
+ include RailsAdminNext::Config::Configurable
74
+
75
+ def self.setup
76
+ raise "PaperTrail not found" unless defined?(::PaperTrail)
77
+
78
+ RailsAdminNext::Extensions::ControllerExtension.include ControllerExtension
79
+ end
80
+
81
+ def initialize(controller, user_class_name = nil, version_class_name = nil, &block)
82
+ @controller = controller
83
+ @controller&.send(:set_paper_trail_whodunnit)
84
+
85
+ user_class { user_class_name.to_s.constantize } if user_class_name
86
+ version_class { version_class_name.to_s.constantize } if version_class_name
87
+
88
+ instance_eval(&block) if block
89
+ end
90
+
91
+ register_instance_option :user_class do
92
+ User
93
+ rescue NameError
94
+ raise E_USER_CLASS_NOT_SET
95
+ end
96
+
97
+ register_instance_option :version_class do
98
+ PaperTrail::Version
99
+ rescue NameError
100
+ raise E_VERSION_MODEL_NOT_SET
101
+ end
102
+
103
+ register_instance_option :sort_by do
104
+ {id: :desc}
105
+ end
106
+
107
+ def latest(count = 100)
108
+ version_class
109
+ .order(sort_by).includes(:item).limit(count)
110
+ .collect { |version| VersionProxy.new(version, user_class) }
111
+ end
112
+
113
+ def delete_object(_object, _model, _user)
114
+ # do nothing
115
+ end
116
+
117
+ def update_object(_object, _model, _user, _changes)
118
+ # do nothing
119
+ end
120
+
121
+ def create_object(_object, _abstract_model, _user)
122
+ # do nothing
123
+ end
124
+
125
+ def listing_for_model(model, query, sort, sort_reverse, all, page, per_page = RailsAdminNext::Config.default_items_per_page || 20)
126
+ listing_for_model_or_object(model, nil, query, sort, sort_reverse, all, page, per_page)
127
+ end
128
+
129
+ def listing_for_object(model, object, query, sort, sort_reverse, all, page, per_page = RailsAdminNext::Config.default_items_per_page || 20)
130
+ listing_for_model_or_object(model, object, query, sort, sort_reverse, all, page, per_page)
131
+ end
132
+
133
+ protected
134
+
135
+ # - model - a RailsAdminNext::AbstractModel
136
+ def listing_for_model_or_object(model, object, query, sort, sort_reverse, all, page, per_page)
137
+ sort =
138
+ if sort.present?
139
+ {COLUMN_MAPPING[sort.to_sym] => sort_reverse ? :desc : :asc}
140
+ else
141
+ sort_by
142
+ end
143
+
144
+ versions = object.nil? ? versions_for_model(model) : object.public_send(model.model.versions_association_name)
145
+ versions = versions.where("event LIKE ?", "%#{query}%") if query.present?
146
+ versions = versions.order(sort)
147
+
148
+ paginate_versions(versions, all, (page.presence || 1).to_i, per_page.to_i)
149
+ end
150
+
151
+ # Maps a page of PaperTrail versions onto VersionProxy objects and wraps
152
+ # them in the shared view-facing pagination collection. The `all` mode
153
+ # skips offset pagination and exposes every version on a single page.
154
+ def paginate_versions(versions, all, current_page, per_page)
155
+ if all
156
+ proxies = versions.map { |version| VersionProxy.new(version, user_class) }
157
+ return RailsAdminNext::PaginatedCollection.new(proxies, current_page: 1, per_page: [proxies.size, 1].max, total_count: proxies.size, total_pages: 1)
158
+ end
159
+
160
+ recordset = GearedPagination::Recordset.new(versions, per_page:)
161
+ page = recordset.page(current_page)
162
+ proxies = page.records.map { |version| VersionProxy.new(version, user_class) }
163
+ RailsAdminNext::PaginatedCollection.new(proxies, current_page: page.number, per_page:, total_count: recordset.records_count)
164
+ end
165
+
166
+ def versions_for_model(model)
167
+ model_name = model.model.name
168
+ base_class_name = model.model.base_class.name
169
+
170
+ options =
171
+ if base_class_name == model_name
172
+ {item_type: model_name}
173
+ else
174
+ {item_type: base_class_name, item_id: model.model.all}
175
+ end
176
+
177
+ version_class_for(model.model).where(options)
178
+ end
179
+
180
+ # PT can be configured to use [custom version
181
+ # classes](https://github.com/paper-trail-gem/paper_trail#6a-custom-version-classes)
182
+ #
183
+ # ```ruby
184
+ # has_paper_trail versions: { class_name: 'MyVersion' }
185
+ # ```
186
+ def version_class_for(model)
187
+ model.paper_trail.version_class
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_admin_next/extensions/paper_trail/auditing_adapter"
4
+
5
+ RailsAdminNext.add_extension(:paper_trail, RailsAdminNext::Extensions::PaperTrail, auditing: true)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ module Extensions
5
+ module Pundit
6
+ # This adapter is for the Pundit[https://github.com/elabs/pundit] authorization library.
7
+ # You can create another adapter for different authorization behavior, just be certain it
8
+ # responds to each of the public methods here.
9
+ class AuthorizationAdapter
10
+ # This method is called first time only and used for setup
11
+ def self.setup
12
+ RailsAdminNext::Extensions::ControllerExtension.include defined?(::Pundit::Authorization) ? ::Pundit::Authorization : ::Pundit
13
+ end
14
+
15
+ # See the +authorize_with+ config method for where the initialization happens.
16
+ def initialize(controller)
17
+ @controller = controller
18
+ end
19
+
20
+ # This method is called in every controller action and should raise an exception
21
+ # when the authorization fails. The first argument is the name of the controller
22
+ # action as a symbol (:create, :bulk_delete, etc.). The second argument is the
23
+ # AbstractModel instance that applies. The third argument is the actual model
24
+ # instance if it is available.
25
+ def authorize(action, abstract_model = nil, model_object = nil)
26
+ record = model_object || abstract_model&.model
27
+ raise ::Pundit::NotAuthorizedError.new("not allowed to #{action} this #{record}") if action && !policy(record).send(action_for_pundit(action))
28
+
29
+ @controller.instance_variable_set(:@_pundit_policy_authorized, true)
30
+ end
31
+
32
+ # This method is called primarily from the view to determine whether the given user
33
+ # has access to perform the action on a given model. It should return true when authorized.
34
+ # This takes the same arguments as +authorize+. The difference is that this will
35
+ # return a boolean whereas +authorize+ will raise an exception when not authorized.
36
+ def authorized?(action, abstract_model = nil, model_object = nil)
37
+ record = model_object || abstract_model&.model
38
+ policy(record).send(action_for_pundit(action)) if action
39
+ end
40
+
41
+ # This is called when needing to scope a database query. It is called within the list
42
+ # and bulk_delete/destroy actions and should return a scope which limits the records
43
+ # to those which the user can perform the given action on.
44
+ def query(_action, abstract_model)
45
+ @controller.send(:policy_scope, abstract_model.model.all)
46
+ rescue ::Pundit::NotDefinedError
47
+ abstract_model.model.all
48
+ end
49
+
50
+ # This is called in the new/create actions to determine the initial attributes for new
51
+ # records. It should return a hash of attributes which match what the user
52
+ # is authorized to create.
53
+ def attributes_for(action, abstract_model)
54
+ record = abstract_model&.model
55
+ policy(record).try(:attributes_for, action) || {}
56
+ end
57
+
58
+ private
59
+
60
+ def policy(record)
61
+ @controller.send(:policy, record)
62
+ rescue ::Pundit::NotDefinedError
63
+ ::ApplicationPolicy.new(@controller.send(:pundit_user), record)
64
+ end
65
+
66
+ def action_for_pundit(action)
67
+ (action[-1, 1] == "?") ? action : "#{action}?"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_admin_next/extensions/pundit/authorization_adapter"
4
+
5
+ RailsAdminNext.add_extension(:pundit, RailsAdminNext::Extensions::Pundit, authorization: true)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ module Extensions
5
+ module UrlForExtension
6
+ def url_for(options, *)
7
+ case options[:id]
8
+ when Array
9
+ options[:id] = RailsAdminNext.config.composite_keys_serializer.serialize(options[:id])
10
+ end
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module RailsAdminNext
6
+ # Inline-SVG icon registry. RailsAdminNext renders its icons as inline `<svg>`
7
+ # (see IconsHelper#rails_admin_icon) instead of a Font Awesome icon font,
8
+ # so the engine ships no web fonts and no icon CSS framework.
9
+ #
10
+ # Icons are referenced by a **logical name** (e.g. `:edit`, `:delete`, `:list`).
11
+ # The glyph path data below matches Font Awesome Free 6.1.1 (solid), so the
12
+ # rendered icons stay visually familiar.
13
+ #
14
+ # `Action#link_icon` and `Model#navigation_icon` take a logical name. A legacy
15
+ # Font Awesome class string is also resolved best-effort and warns via
16
+ # RailsAdminNext.deprecator.
17
+ module Icons
18
+ # Logical name => { view_box:, path: } (Font Awesome Free 6.1.1 solid).
19
+ GLYPHS = {
20
+ dashboard: {view_box: "0 0 576 512", path: "M575.8 255.5C575.8 273.5 560.8 287.6 543.8 287.6H511.8L512.5 447.7C512.5 450.5 512.3 453.1 512 455.8V472C512 494.1 494.1 512 472 512H456C454.9 512 453.8 511.1 452.7 511.9C451.3 511.1 449.9 512 448.5 512H392C369.9 512 352 494.1 352 472V384C352 366.3 337.7 352 320 352H256C238.3 352 224 366.3 224 384V472C224 494.1 206.1 512 184 512H128.1C126.6 512 125.1 511.9 123.6 511.8C122.4 511.9 121.2 512 120 512H104C81.91 512 64 494.1 64 472V360C64 359.1 64.03 358.1 64.09 357.2V287.6H32.05C14.02 287.6 0 273.5 0 255.5C0 246.5 3.004 238.5 10.01 231.5L266.4 8.016C273.4 1.002 281.4 0 288.4 0C295.4 0 303.4 2.004 309.5 7.014L564.8 231.5C572.8 238.5 576.9 246.5 575.8 255.5L575.8 255.5z"},
21
+ list: {view_box: "0 0 512 512", path: "M0 96C0 60.65 28.65 32 64 32H448C483.3 32 512 60.65 512 96V416C512 451.3 483.3 480 448 480H64C28.65 480 0 451.3 0 416V96zM64 160H128V96H64V160zM448 96H192V160H448V96zM64 288H128V224H64V288zM448 224H192V288H448V224zM64 416H128V352H64V416zM448 352H192V416H448V352z"},
22
+ show: {view_box: "0 0 512 512", path: "M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 128c17.67 0 32 14.33 32 32c0 17.67-14.33 32-32 32S224 177.7 224 160C224 142.3 238.3 128 256 128zM296 384h-80C202.8 384 192 373.3 192 360s10.75-24 24-24h16v-64H224c-13.25 0-24-10.75-24-24S210.8 224 224 224h32c13.25 0 24 10.75 24 24v88h16c13.25 0 24 10.75 24 24S309.3 384 296 384z"},
23
+ new: {view_box: "0 0 448 512", path: "M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z"},
24
+ edit: {view_box: "0 0 512 512", path: "M421.7 220.3L188.5 453.4L154.6 419.5L158.1 416H112C103.2 416 96 408.8 96 400V353.9L92.51 357.4C87.78 362.2 84.31 368 82.42 374.4L59.44 452.6L137.6 429.6C143.1 427.7 149.8 424.2 154.6 419.5L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3zM492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75z"},
25
+ delete: {view_box: "0 0 320 512", path: "M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"},
26
+ export: {view_box: "0 0 576 512", path: "M192 312C192 298.8 202.8 288 216 288H384V160H256c-17.67 0-32-14.33-32-32L224 0H48C21.49 0 0 21.49 0 48v416C0 490.5 21.49 512 48 512h288c26.51 0 48-21.49 48-48v-128H216C202.8 336 192 325.3 192 312zM256 0v128h128L256 0zM568.1 295l-80-80c-9.375-9.375-24.56-9.375-33.94 0s-9.375 24.56 0 33.94L494.1 288H384v48h110.1l-39.03 39.03C450.3 379.7 448 385.8 448 392s2.344 12.28 7.031 16.97c9.375 9.375 24.56 9.375 33.94 0l80-80C578.3 319.6 578.3 304.4 568.1 295z"},
27
+ history: {view_box: "0 0 448 512", path: "M448 336v-288C448 21.49 426.5 0 400 0H96C42.98 0 0 42.98 0 96v320c0 53.02 42.98 96 96 96h320c17.67 0 32-14.33 32-31.1c0-11.72-6.607-21.52-16-27.1v-81.36C441.8 362.8 448 350.2 448 336zM143.1 128h192C344.8 128 352 135.2 352 144C352 152.8 344.8 160 336 160H143.1C135.2 160 128 152.8 128 144C128 135.2 135.2 128 143.1 128zM143.1 192h192C344.8 192 352 199.2 352 208C352 216.8 344.8 224 336 224H143.1C135.2 224 128 216.8 128 208C128 199.2 135.2 192 143.1 192zM384 448H96c-17.67 0-32-14.33-32-32c0-17.67 14.33-32 32-32h288V448z"},
28
+ show_in_app: {view_box: "0 0 576 512", path: "M279.6 160.4C282.4 160.1 285.2 160 288 160C341 160 384 202.1 384 256C384 309 341 352 288 352C234.1 352 192 309 192 256C192 253.2 192.1 250.4 192.4 247.6C201.7 252.1 212.5 256 224 256C259.3 256 288 227.3 288 192C288 180.5 284.1 169.7 279.6 160.4zM480.6 112.6C527.4 156 558.7 207.1 573.5 243.7C576.8 251.6 576.8 260.4 573.5 268.3C558.7 304 527.4 355.1 480.6 399.4C433.5 443.2 368.8 480 288 480C207.2 480 142.5 443.2 95.42 399.4C48.62 355.1 17.34 304 2.461 268.3C-.8205 260.4-.8205 251.6 2.461 243.7C17.34 207.1 48.62 156 95.42 112.6C142.5 68.84 207.2 32 288 32C368.8 32 433.5 68.84 480.6 112.6V112.6zM288 112C208.5 112 144 176.5 144 256C144 335.5 208.5 400 288 400C367.5 400 432 335.5 432 256C432 176.5 367.5 112 288 112z"},
29
+ bulk_delete: {view_box: "0 0 320 512", path: "M204.3 32.01H96c-52.94 0-96 43.06-96 96c0 17.67 14.31 31.1 32 31.1s32-14.32 32-31.1c0-17.64 14.34-32 32-32h108.3C232.8 96.01 256 119.2 256 147.8c0 19.72-10.97 37.47-30.5 47.33L127.8 252.4C117.1 258.2 112 268.7 112 280v40c0 17.67 14.31 31.99 32 31.99s32-14.32 32-31.99V298.3L256 251.3c39.47-19.75 64-59.42 64-103.5C320 83.95 268.1 32.01 204.3 32.01zM144 400c-22.09 0-40 17.91-40 40s17.91 39.1 40 39.1s40-17.9 40-39.1S166.1 400 144 400z"},
30
+ collapse: {view_box: "0 0 448 512", path: "M224 416c-8.188 0-16.38-3.125-22.62-9.375l-192-192c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L224 338.8l169.4-169.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-192 192C240.4 412.9 232.2 416 224 416z"},
31
+ expand: {view_box: "0 0 320 512", path: "M96 480c-8.188 0-16.38-3.125-22.62-9.375c-12.5-12.5-12.5-32.75 0-45.25L242.8 256L73.38 86.63c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25l-192 192C112.4 476.9 104.2 480 96 480z"},
32
+ calendar: {view_box: "0 0 448 512", path: "M96 32C96 14.33 110.3 0 128 0C145.7 0 160 14.33 160 32V64H288V32C288 14.33 302.3 0 320 0C337.7 0 352 14.33 352 32V64H400C426.5 64 448 85.49 448 112V160H0V112C0 85.49 21.49 64 48 64H96V32zM448 464C448 490.5 426.5 512 400 512H48C21.49 512 0 490.5 0 464V192H448V464z"},
33
+ check: {view_box: "0 0 448 512", path: "M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"},
34
+ minus: {view_box: "0 0 448 512", path: "M400 288h-352c-17.69 0-32-14.32-32-32.01s14.31-31.99 32-31.99h352c17.69 0 32 14.3 32 31.99S417.7 288 400 288z"},
35
+ refresh: {view_box: "0 0 512 512", path: "M464 16c-17.67 0-32 14.31-32 32v74.09C392.1 66.52 327.4 32 256 32C161.5 32 78.59 92.34 49.58 182.2c-5.438 16.81 3.797 34.88 20.61 40.28c16.89 5.5 34.88-3.812 40.3-20.59C130.9 138.5 189.4 96 256 96c50.5 0 96.26 24.55 124.4 64H336c-17.67 0-32 14.31-32 32s14.33 32 32 32h128c17.67 0 32-14.31 32-32V48C496 30.31 481.7 16 464 16zM441.8 289.6c-16.92-5.438-34.88 3.812-40.3 20.59C381.1 373.5 322.6 416 256 416c-50.5 0-96.25-24.55-124.4-64H176c17.67 0 32-14.31 32-32s-14.33-32-32-32h-128c-17.67 0-32 14.31-32 32v144c0 17.69 14.33 32 32 32s32-14.31 32-32v-74.09C119.9 445.5 184.6 480 255.1 480c94.45 0 177.4-60.34 206.4-150.2C467.9 313 458.6 294.1 441.8 289.6z"},
36
+ trash: {view_box: "0 0 448 512", path: "M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM394.8 466.1C393.2 492.3 372.3 512 346.9 512H101.1C75.75 512 54.77 492.3 53.19 466.1L31.1 128H416L394.8 466.1z"},
37
+ move_right: {view_box: "0 0 512 512", path: "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM358.6 278.6l-112 112c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25L290.8 256L201.4 166.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l112 112C364.9 239.6 368 247.8 368 256S364.9 272.4 358.6 278.6z"},
38
+ move_left: {view_box: "0 0 512 512", path: "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM310.6 345.4c12.5 12.5 12.5 32.75 0 45.25s-32.75 12.5-45.25 0l-112-112C147.1 272.4 144 264.2 144 256s3.125-16.38 9.375-22.62l112-112c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L221.3 256L310.6 345.4z"},
39
+ move_up: {view_box: "0 0 512 512", path: "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM390.6 310.6c-12.5 12.5-32.75 12.5-45.25 0L256 221.3L166.6 310.6c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l112-112C239.6 147.1 247.8 144 256 144s16.38 3.125 22.62 9.375l112 112C403.1 277.9 403.1 298.1 390.6 310.6z"},
40
+ move_down: {view_box: "0 0 512 512", path: "M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM390.6 246.6l-112 112C272.4 364.9 264.2 368 256 368s-16.38-3.125-22.62-9.375l-112-112c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L256 290.8l89.38-89.38c12.5-12.5 32.75-12.5 45.25 0S403.1 234.1 390.6 246.6z"}
41
+ }.freeze
42
+
43
+ # Synonyms => canonical glyph name. Lets host config and call sites use the
44
+ # name that reads best (e.g. `:cancel` for the X glyph on a cancel button).
45
+ ALIASES = {
46
+ add: :new,
47
+ book: :history,
48
+ cancel: :delete,
49
+ chevron_circle_down: :move_down,
50
+ chevron_circle_left: :move_left,
51
+ chevron_circle_right: :move_right,
52
+ chevron_circle_up: :move_up,
53
+ chevron_down: :collapse,
54
+ chevron_right: :expand,
55
+ close: :delete,
56
+ eye: :show_in_app,
57
+ help: :bulk_delete,
58
+ home: :dashboard,
59
+ info: :show,
60
+ info_circle: :show,
61
+ pencil: :edit,
62
+ plus: :new,
63
+ question: :bulk_delete,
64
+ reload: :refresh,
65
+ sort_asc: :move_up,
66
+ sort_desc: :move_down,
67
+ sync: :refresh,
68
+ th_list: :list,
69
+ times: :delete,
70
+ view: :show_in_app,
71
+ xmark: :delete
72
+ }.freeze
73
+
74
+ # Font Awesome glyph token (v5 and v6 names) => canonical logical name, used
75
+ # to gracefully resolve legacy `'fas fa-…'` values a host may still pass.
76
+ LEGACY_FA = {
77
+ "arrows-rotate" => :refresh, "book" => :history, "calendar" => :calendar,
78
+ "check" => :check, "chevron-circle-down" => :move_down,
79
+ "chevron-circle-left" => :move_left, "chevron-circle-right" => :move_right,
80
+ "chevron-circle-up" => :move_up, "chevron-down" => :collapse,
81
+ "chevron-right" => :expand, "circle-chevron-down" => :move_down,
82
+ "circle-chevron-left" => :move_left, "circle-chevron-right" => :move_right,
83
+ "circle-chevron-up" => :move_up, "circle-info" => :show, "close" => :delete,
84
+ "edit" => :edit, "eye" => :show_in_app, "file-export" => :export,
85
+ "home" => :dashboard, "house" => :dashboard, "info-circle" => :show,
86
+ "list" => :list, "minus" => :minus, "pencil" => :edit,
87
+ "pencil-alt" => :edit, "plus" => :new, "question" => :bulk_delete,
88
+ "sync" => :refresh, "table-list" => :list, "th-list" => :list,
89
+ "times" => :delete, "trash" => :trash, "trash-alt" => :trash,
90
+ "xmark" => :delete
91
+ }.freeze
92
+
93
+ # Build an inline `<svg>` markup string for a logical icon name (see GLYPHS),
94
+ # or nil if the name is unknown. A legacy Font Awesome class string is resolved
95
+ # best-effort and warns via RailsAdminNext.deprecator. The single source of
96
+ # truth for icon markup, shared by IconsHelper (views) and field types (lib).
97
+ def self.svg(name, **html_options)
98
+ return if name.nil?
99
+
100
+ if fa_class?(name)
101
+ RailsAdminNext.deprecator.warn(
102
+ "RailsAdminNext icons now use logical names; #{name.inspect} is a Font Awesome class string. " \
103
+ "Font Awesome is no longer shipped — pass a logical name (see RailsAdminNext::Icons::GLYPHS)."
104
+ )
105
+ name = from_fa_class(name)
106
+ end
107
+
108
+ key = name && canonical(name)
109
+ glyph = key && GLYPHS[key]
110
+ return unless glyph
111
+
112
+ classes = ["rails-admin-icon", html_options.delete(:class)].compact.join(" ")
113
+ attributes = {
114
+ "viewBox" => glyph[:view_box],
115
+ "aria-hidden" => "true",
116
+ "focusable" => "false",
117
+ "data-icon" => key.to_s,
118
+ "class" => classes
119
+ }.merge(html_options.transform_keys(&:to_s))
120
+ rendered = attributes.map { |attr, value| %(#{CGI.escapeHTML(attr.to_s)}="#{CGI.escapeHTML(value.to_s)}") }.join(" ")
121
+ %(<svg #{rendered}><path d="#{CGI.escapeHTML(glyph[:path])}"/></svg>)
122
+ end
123
+
124
+ # Resolve a logical name (or alias) to its canonical glyph key, or nil.
125
+ def self.canonical(name)
126
+ key = name.to_sym
127
+ return key if GLYPHS.key?(key)
128
+
129
+ ALIASES[key]
130
+ end
131
+
132
+ # Does this value look like a (legacy) Font Awesome class string?
133
+ def self.fa_class?(value)
134
+ value.is_a?(String) && value.match?(/\bfa[a-z]?\b|\bfa-[a-z]/)
135
+ end
136
+
137
+ # Best-effort canonical name for a legacy `'fas fa-…'` string, or nil. Scans
138
+ # every `fa-…` token so sizing modifiers (e.g. `fa-fw`) don't mask the glyph.
139
+ def self.from_fa_class(value)
140
+ value.to_s.scan(/fa-([a-z0-9-]+)/).flatten.lazy.filter_map { |token| LEGACY_FA[token] }.first
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ module Support
5
+ module CompositeKeysSerializer
6
+ def self.serialize(keys)
7
+ keys.map { |key| key&.to_s&.gsub("_", "__") }.join("_")
8
+ end
9
+
10
+ def self.deserialize(string)
11
+ string.split("_").map { |key| key&.gsub("__", "_") }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module RailsAdminNext
6
+ class CSVConverter
7
+ def initialize(objects = [], schema = nil)
8
+ @fields = []
9
+ @associations = []
10
+ schema ||= {}
11
+
12
+ return if (@objects = objects).blank?
13
+
14
+ @model = objects.dup.first.class
15
+ @abstract_model = RailsAdminNext::AbstractModel.new(@model)
16
+ @model_config = @abstract_model.config
17
+ @methods = [(schema[:only] || []) + (schema[:methods] || [])].flatten.compact
18
+ @fields = @methods.collect { |m| export_field_for(m) }.compact
19
+ @empty = ::I18n.t("admin.export.empty_value_for_associated_objects")
20
+ schema_include = schema.delete(:include) || {}
21
+
22
+ @associations = schema_include.each_with_object({}) do |(key, values), hash|
23
+ association = export_field_for(key)
24
+ next unless association&.association?
25
+
26
+ model_config = association.associated_model_config
27
+ abstract_model = model_config.abstract_model
28
+ methods = [(values[:only] || []) + (values[:methods] || [])].flatten.compact
29
+
30
+ hash[key] = {
31
+ association: association,
32
+ model: abstract_model.model,
33
+ abstract_model: abstract_model,
34
+ model_config: model_config,
35
+ fields: methods.collect { |m| export_field_for(m, model_config) }.compact
36
+ }
37
+ hash
38
+ end
39
+ end
40
+
41
+ def to_csv(options = {})
42
+ options = HashWithIndifferentAccess.new(options)
43
+ encoding_to = Encoding.find(options[:encoding_to]) if options[:encoding_to].present?
44
+
45
+ csv_string = generate_csv_string(options)
46
+ csv_string = csv_string.encode(encoding_to, invalid: :replace, undef: :replace, replace: "?") if encoding_to
47
+
48
+ # Add a BOM for utf8 encodings, helps with utf8 auto-detect for some versions of Excel.
49
+ # Don't add if utf8 but user don't want to touch input encoding:
50
+ # If user chooses utf8, they will open it in utf8 and BOM will disappear at reading.
51
+ # But that way "English" users who don't bother and chooses to let utf8 by default won't get BOM added
52
+ # and will not see it if Excel opens the file with a different encoding.
53
+ csv_string = "\xEF\xBB\xBF#{csv_string}" if encoding_to == Encoding::UTF_8
54
+
55
+ [!options[:skip_header], (encoding_to || csv_string.encoding).to_s, csv_string]
56
+ end
57
+
58
+ private
59
+
60
+ def export_field_for(method, model_config = @model_config)
61
+ model_config.export.fields.detect { |f| f.name == method }
62
+ end
63
+
64
+ def generate_csv_string(options)
65
+ generator_options = (options[:generator] || {}).symbolize_keys.delete_if { |_, value| value.blank? }
66
+ method = @objects.respond_to?(:find_each) ? :find_each : :each
67
+
68
+ CSV.generate(**generator_options) do |csv|
69
+ csv << generate_csv_header unless options[:skip_header]
70
+
71
+ @objects.send(method) do |object|
72
+ csv << generate_csv_row(object)
73
+ end
74
+ end
75
+ end
76
+
77
+ def generate_csv_header
78
+ @fields.collect do |field|
79
+ ::I18n.t("admin.export.csv.header_for_root_methods", name: field.label, model: @abstract_model.pretty_name)
80
+ end +
81
+ @associations.flat_map do |_association_name, option_hash|
82
+ option_hash[:fields].collect do |field|
83
+ ::I18n.t("admin.export.csv.header_for_association_methods", name: field.label, association: option_hash[:association].label)
84
+ end
85
+ end
86
+ end
87
+
88
+ def generate_csv_row(object)
89
+ @fields.collect do |field|
90
+ field.with(object: object).export_value
91
+ end +
92
+ @associations.flat_map do |association_name, option_hash|
93
+ associated_objects = [object.send(association_name)].flatten.compact
94
+ option_hash[:fields].collect do |field|
95
+ associated_objects.collect { |ao| field.with(object: ao).export_value.presence || @empty }.join(",")
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ class HashHelper
5
+ def self.symbolize(obj)
6
+ case obj
7
+ when Array
8
+ obj.each_with_object([]) do |val, res|
9
+ res << case val
10
+ when Hash, Array then symbolize(val)
11
+ when String then val.to_sym
12
+ else val
13
+ end
14
+ end
15
+ when Hash
16
+ obj.each_with_object({}) do |(key, val), res|
17
+ nkey = key.is_a?(String) ? key.to_sym : key
18
+ nval =
19
+ case val
20
+ when Hash, Array then symbolize(val)
21
+ when String then val.to_sym
22
+ else val
23
+ end
24
+ res[nkey] = nval
25
+ end
26
+ else
27
+ obj
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdminNext
4
+ # View-facing adapter over a single page of records.
5
+ #
6
+ # Wraps either a GearedPagination::Page (ActiveRecord lists, via
7
+ # .from_recordset) or a manually built Array (the PaperTrail audit log, whose
8
+ # VersionProxy objects are not an ActiveRecord::Relation) and exposes only the
9
+ # pagination contract the index/history views and the shared pager partial
10
+ # consume. Counting is lazy so the count-free "limited" pager can render
11
+ # prev/next without triggering a COUNT query.
12
+ class PaginatedCollection
13
+ include Enumerable
14
+
15
+ attr_reader :current_page, :per_page
16
+
17
+ def self.from_recordset(recordset, page_number, per_page)
18
+ page = recordset.page(page_number)
19
+ new page.records,
20
+ current_page: page.number,
21
+ per_page:,
22
+ total_count: -> { recordset.records_count }
23
+ end
24
+
25
+ # total_count / total_pages may each be an Integer, nil, or a callable
26
+ # returning one; callables are evaluated (and memoized) lazily on first read
27
+ # so the count-free pager never triggers a COUNT query. total_pages defaults
28
+ # to a value derived from total_count (see #total_pages).
29
+ def initialize(records, current_page:, per_page:, total_count:, total_pages: nil)
30
+ @records = records.to_a
31
+ @current_page = current_page.to_i
32
+ @per_page = per_page.to_i
33
+ @total_count = total_count
34
+ @total_pages = total_pages
35
+ end
36
+
37
+ def each(&)
38
+ @records.each(&)
39
+ end
40
+
41
+ delegate :size, :length, :empty?, :first, :last, :to_a, :to_json, :as_json, to: :@records
42
+
43
+ # A grouped relation makes the underlying COUNT return a Hash (one entry per
44
+ # group); treat its length as the count so grouped queries paginate correctly
45
+ # instead of letting Hash arithmetic raise.
46
+ def total_count
47
+ @total_count = @total_count.call if @total_count.respond_to?(:call)
48
+ @total_count = @total_count.size if @total_count.is_a?(Hash)
49
+ @total_count
50
+ end
51
+
52
+ # Derived from total_count when not supplied, so a grouped relation never
53
+ # reaches GearedPagination's Hash-unaware page_count arithmetic.
54
+ def total_pages
55
+ @total_pages = @total_pages.call if @total_pages.respond_to?(:call)
56
+ @total_pages = @total_pages.size if @total_pages.is_a?(Hash)
57
+ @total_pages ||= [(total_count.to_f / per_page).ceil, 1].max
58
+ @total_pages
59
+ end
60
+
61
+ def first_page?
62
+ current_page <= 1
63
+ end
64
+
65
+ def last_page?
66
+ current_page >= total_pages
67
+ end
68
+
69
+ def next_page
70
+ current_page + 1 unless last_page?
71
+ end
72
+
73
+ # This page holds a full slice, so (without a COUNT) more records may
74
+ # follow — drives the count-free "limited" pager's next link.
75
+ def full_page?
76
+ size >= per_page
77
+ end
78
+ end
79
+ end