rails_admin_next 1.2.1 → 2.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +32 -20
  4. data/app/assets/stylesheets/rails_admin/skin.css +12 -0
  5. data/app/controllers/rails_admin_next/main_controller.rb +53 -7
  6. data/app/helpers/rails_admin_next/form_builder.rb +23 -4
  7. data/app/helpers/rails_admin_next/main_helper.rb +12 -1
  8. data/app/views/layouts/rails_admin_next/_head.html.erb +1 -1
  9. data/app/views/layouts/rails_admin_next/_navigation.html.erb +1 -1
  10. data/app/views/layouts/rails_admin_next/application.html.erb +3 -0
  11. data/app/views/rails_admin_next/main/_form_nested_many.html.erb +9 -4
  12. data/app/views/rails_admin_next/main/_form_nested_one.html.erb +8 -3
  13. data/app/views/rails_admin_next/main/bulk_delete.html.erb +1 -1
  14. data/app/views/rails_admin_next/main/dashboard.html.erb +52 -50
  15. data/app/views/rails_admin_next/main/index.html.erb +3 -2
  16. data/docs/UPGRADING.md +263 -9
  17. data/docs/security.md +4 -0
  18. data/lib/generators/rails_admin_next/templates/initializer.erb +7 -3
  19. data/lib/rails_admin_next/abstract_model.rb +12 -4
  20. data/lib/rails_admin_next/adapters/active_record.rb +47 -3
  21. data/lib/rails_admin_next/config/actions/base.rb +0 -1
  22. data/lib/rails_admin_next/config/actions/bulk_delete.rb +4 -4
  23. data/lib/rails_admin_next/config/actions/index.rb +3 -19
  24. data/lib/rails_admin_next/config/actions.rb +5 -12
  25. data/lib/rails_admin_next/config/fields/association.rb +1 -1
  26. data/lib/rails_admin_next/config/fields/types/enum.rb +8 -2
  27. data/lib/rails_admin_next/config/fields/types/serialized.rb +17 -2
  28. data/lib/rails_admin_next/config/fields/types/timestamp.rb +1 -0
  29. data/lib/rails_admin_next/config/has_fields.rb +1 -1
  30. data/lib/rails_admin_next/config.rb +29 -9
  31. data/lib/rails_admin_next/extensions/action_policy/authorization_adapter.rb +60 -0
  32. data/lib/rails_admin_next/extensions/action_policy.rb +5 -0
  33. data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +1 -1
  34. data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +17 -1
  35. data/lib/rails_admin_next/support/composite_keys_serializer.rb +8 -2
  36. data/lib/rails_admin_next/support/csv_converter.rb +45 -5
  37. data/lib/rails_admin_next/version.rb +3 -3
  38. data/lib/rails_admin_next.rb +5 -17
  39. data/skills/rails-admin-next/SKILL.md +5 -4
  40. data/src/rails_admin/controllers/nested_form_controller.js +1 -1
  41. metadata +3 -3
  42. data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +0 -90
  43. data/lib/rails_admin_next/extensions/cancancan.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 957743cf70d6fa24bb0cb1eaf74ca15b4a3b2996e0dca4f5dcee2d6428985d6b
4
- data.tar.gz: a205770a7170c2b1ce7e04a85cd6422f6cda1d150da237df81795387af8cc18d
3
+ metadata.gz: 85cf357bfb48a568e28ee9d031d96f9d9d96546564579a17273d06ddd1643a45
4
+ data.tar.gz: a3850dcf1e9f5fb4bf6797a1175be2112d6f5ecc1d9f4588dfc561a412ce9072
5
5
  SHA512:
6
- metadata.gz: ad848e8080f75c1e3d6b12c180dcb745bb4a3caea7791ee08d4fec1d97567836effacf01ea340a764b0febb4a52c345e793b3d1708d7496e9d1c2e3f6a167f6d
7
- data.tar.gz: 37ec4a516104eb2ecc4d8924c758ad862b8fa091f32a0d460b0dfda2f8bc566a233fa61ea81d6cdbbfe49695b1d71eb0aed55f75c281001e9d86a73821fbe8a8
6
+ metadata.gz: 950489886f285808c1d8688a3099361d63bc0f2bead9b15b3b752c3de41e696a48232f63bd6bdce8ec6835a9266b35f3b13a1faebc10572a0de1daf05b87a61b
7
+ data.tar.gz: 6dcc9aa9063071f2008888dff5c1c4e43042c6fd3026ce6c52bdd5844080f0276cbb719241427c84254d95d2b05160cf066aaf699ddd14ac03c5c9204a5d791d
data/Gemfile CHANGED
@@ -11,8 +11,8 @@ group :development, :test do
11
11
  end
12
12
 
13
13
  group :test do
14
+ gem "action_policy"
14
15
  gem "brakeman", require: false
15
- gem "cancancan", "~> 3.0"
16
16
  gem "cuprite"
17
17
  gem "database_cleaner-active_record", require: false
18
18
  gem "factory_bot"
data/README.md CHANGED
@@ -33,7 +33,7 @@ multi-bundler, jQuery-era machinery. RailsAdminNext keeps the admin and drops th
33
33
  - **🧩 Turbo + Stimulus** — the jQuery / jQuery-UI / Bootstrap-JS / `@rails/ujs` stack is gone, replaced by engine-shipped Stimulus controllers; styling is hand-maintained vanilla CSS.
34
34
  - **🔒 Hardened & CDN-free** — a documented CSRF / clickjacking / mass-assignment / CSP posture ([SECURITY.md](SECURITY.md)) and **zero external CDN**, default or opt-in — every asset is self-hosted off a gem.
35
35
  - **đŸŽ¯ One modern target** — Ruby â‰Ĩ 4.0.9, Rails ~> 8.1, ActiveRecord only, latest Chrome + Safari. No Mongoid, no multi-bundler / multi-Rails matrix, no legacy-browser CSS.
36
- - **â†Šī¸ Familiar** — `RailsAdminNext.config`, per-model `rails_admin_next do â€Ļ end` blocks, and CanCanCan / Pundit authorization all work as before; upgrading from RailsAdmin is a mechanical rename.
36
+ - **â†Šī¸ Familiar** — `RailsAdminNext.config`, per-model `rails_admin_next do â€Ļ end` blocks, and Pundit authorization all work as before; upgrading from RailsAdmin is a mechanical rename.
37
37
 
38
38
  ## Quick Start
39
39
 
@@ -113,12 +113,12 @@ always match the installed version.
113
113
  - File uploads (ActiveStorage)
114
114
  - Rich text fields (ActionText / Trix)
115
115
  - Authentication (via [Devise][devise] or any other scheme)
116
- - Authorization (via [CanCanCan][cancancan] or [Pundit][pundit])
116
+ - Authorization (via [Pundit][pundit], [ActionPolicy][action_policy] or a custom adapter)
117
117
  - User action history (via [PaperTrail][papertrail])
118
118
 
119
119
  [devise]: https://github.com/heartcombo/devise
120
- [cancancan]: https://github.com/CanCanCommunity/cancancan
121
120
  [pundit]: https://github.com/varvet/pundit
121
+ [action_policy]: https://github.com/palkan/action_policy
122
122
  [papertrail]: https://github.com/paper-trail-gem/paper_trail
123
123
 
124
124
  ## Configuration
@@ -143,8 +143,15 @@ RailsAdminNext.config do |config|
143
143
  end
144
144
  ```
145
145
 
146
- This is also where you wire up [Devise][devise], [CanCanCan][cancancan]/[Pundit][pundit],
147
- and [PaperTrail][papertrail].
146
+ This is also where you wire up [Devise][devise], [Pundit][pundit], and [PaperTrail][papertrail].
147
+
148
+ Every admin page ends with a small "Made in Tokyo with â¤ī¸ and 🤖" footer. To hide it:
149
+
150
+ ```ruby
151
+ RailsAdminNext.config do |config|
152
+ config.show_footer = false
153
+ end
154
+ ```
148
155
 
149
156
  ### Per model
150
157
 
@@ -170,19 +177,24 @@ The configuration DSL is unchanged from upstream RailsAdmin, so the upstream
170
177
 
171
178
  ### Authorization keys are stable
172
179
 
173
- The authorization contract is held stable, so existing CanCanCan
174
- abilities and Pundit policies keep working **without edits**: the access subject is
175
- `:rails_admin` and every action's
176
- authorization key (`:dashboard`, `:index`, `:show`, `:new`, `:edit`, `:export`,
177
- `:destroy`, `:history`, `:show_in_app`) is unchanged.
178
-
179
- ```ruby
180
- # Still grants access to the admin:
181
- can :access, :rails_admin
182
- ```
180
+ The authorization contract is held stable, so existing Pundit policies keep working
181
+ **without edits**: every action's authorization key (`:dashboard`, `:index`, `:show`,
182
+ `:new`, `:edit`, `:export`, `:destroy`, `:history`, `:show_in_app`) is unchanged, and the
183
+ Pundit adapter calls it with a `?` suffix (`index?`, `edit?`). Pundit and ActionPolicy are
184
+ the built-in adapters; any other authorization library plugs in as a custom adapter, which by convention
185
+ checks the `:access, :rails_admin` subject. See [docs/UPGRADING.md](docs/UPGRADING.md).
183
186
 
184
187
  The `admin.*` I18n namespace is likewise unchanged.
185
188
 
189
+ ### ActionPolicy
190
+
191
+ `config.authorize_with :action_policy` checks each action against the record's policy with
192
+ the same `<key>?` rules the Pundit adapter calls (`index?`, `show?`, `new?`, `edit?`,
193
+ `destroy?`, `export?`, `history?`, `show_in_app?`, `dashboard?`), and scopes lists, export
194
+ and bulk delete through the policy's default `relation_scope`. A model with no policy, or a
195
+ policy with no default relation scope, lists nothing. Bundle `action_policy` yourself; see
196
+ the "ActionPolicy adapter" section of [docs/UPGRADING.md](docs/UPGRADING.md).
197
+
186
198
  ## Frontend and assets
187
199
 
188
200
  RailsAdminNext ships a single, zero-build asset path:
@@ -294,16 +306,16 @@ action methods**, where each admin operation is an object.
294
306
  - **One controller, many action objects** — to change how e.g. _edit_ behaves, edit `lib/rails_admin_next/config/actions/edit.rb`, not the controller.
295
307
  - **ORM facade** — nothing outside `AbstractModel` references ActiveRecord directly; the adapter seam is intact even though Mongoid was removed.
296
308
  - **Lazy, layered config** — `config/initializers` blocks run before in-model `rails_admin_next do â€Ļ end` blocks, so per-model config overrides the initializer defaults.
297
- - **A deliberately frozen `rails_admin` surface** — the Ruby layer is `RailsAdminNext`, but the `:access, :rails_admin` authorization subject, the `admin.*` I18n namespace, and the `src/rails_admin/` asset tree stay `rails_admin` on purpose, so host abilities, translations, and assets keep working across the rename.
309
+ - **A deliberately frozen `rails_admin` surface** — the Ruby layer is `RailsAdminNext`, but the action authorization keys, the `admin.*` I18n namespace, and the `src/rails_admin/` asset tree stay `rails_admin`-era on purpose, so host policies, translations, and assets keep working across the rename.
298
310
 
299
311
  ## Upgrading from RailsAdmin
300
312
 
301
313
  See [docs/UPGRADING.md](docs/UPGRADING.md) for the one-time changes a host app needs:
302
314
  the Gemfile gem name, the importmap/Propshaft prerequisites, and a mechanical rename of the
303
315
  `RailsAdmin` constant, initializer, mount, model macro, and route helpers to `RailsAdminNext`
304
- (`rails_admin_next`). The `:access, :rails_admin` authorization subject and the `admin.*`
305
- I18n namespace are deliberately left unchanged, so CanCanCan/Pundit rules and translations
306
- keep working without edits.
316
+ (`rails_admin_next`). The action authorization keys and the `admin.*` I18n namespace are
317
+ deliberately left unchanged, so Pundit policies and translations keep working without edits.
318
+ Its 2.0 section covers hosts whose authorization library is no longer a built-in adapter.
307
319
 
308
320
  ## Security
309
321
 
@@ -333,5 +345,5 @@ RailsAdminNext is released under the [MIT License](LICENSE.md).
333
345
  ---
334
346
 
335
347
  <div align="center">
336
- <sub>Built in Tokyo with â¤ī¸ and 🤖</sub>
348
+ <sub>Made in Tokyo with â¤ī¸ and 🤖</sub>
337
349
  </div>
@@ -430,6 +430,10 @@ body.rails_admin #filters_box {
430
430
  display: none;
431
431
  }
432
432
  }
433
+ body.rails_admin .dashboard-statistics {
434
+ overflow-x: auto;
435
+ overscroll-behavior: contain;
436
+ }
433
437
  body.rails_admin #sidescroll {
434
438
  margin-bottom: 20px;
435
439
  overflow-x: auto;
@@ -456,6 +460,14 @@ body.rails_admin .pagination .page-item {
456
460
  white-space: nowrap;
457
461
  }
458
462
 
463
+ /* === Admin skin: footer ================================================= */
464
+ body.rails_admin .admin-footer {
465
+ padding: 2rem 0 1rem;
466
+ font-size: 0.875em;
467
+ color: var(--ra-gray-600);
468
+ text-align: center;
469
+ }
470
+
459
471
  /* === Admin skin: responsive adjustments ================================= */
460
472
  @media screen and (min-width: 576px) {
461
473
  body.rails_admin .sidebar.btn-toggle-nav {
@@ -8,19 +8,35 @@ module RailsAdminNext
8
8
 
9
9
  before_action :check_for_cancel
10
10
 
11
+ helper_method :list_scopes
12
+
11
13
  def bulk_action
12
14
  get_model
13
15
  process(params[:bulk_action]) if params[:bulk_action].in?(RailsAdminNext::Config::Actions.all(:bulkable, controller: self, abstract_model: @abstract_model).collect(&:route_fragment))
14
16
  end
15
17
 
16
- def list_entries(model_config = @model_config, auth_scope_key = :index, additional_scope = get_association_scope_from_params, pagination = !(params[:associated_collection] || params[:all] || params[:bulk_ids]))
18
+ def list_entries(model_config = @model_config, auth_scope_key = :index, additional_scope = nil, pagination = !(params[:all] || params[:bulk_ids]))
17
19
  scope = model_config.scope
18
20
  auth_scope = @authorization_adapter&.query(auth_scope_key, model_config.abstract_model)
19
21
  scope = scope.merge(auth_scope) if auth_scope
22
+ tab = list_scope if model_config == @model_config
23
+ if tab
24
+ scope = scope.public_send(tab)
25
+ scope = within_authorized(scope, auth_scope) if auth_scope
26
+ end
20
27
  scope = scope.instance_eval(&additional_scope) if additional_scope
21
28
  get_collection(model_config, scope, pagination)
22
29
  end
23
30
 
31
+ # The association scope chains after get_collection, which only works on an unpaginated relation.
32
+ def association_entries(model_config, collection_scope)
33
+ scope = model_config.scope
34
+ auth_scope = @authorization_adapter&.query(:index, model_config.abstract_model)
35
+ scope = scope.merge(auth_scope) if auth_scope
36
+ scope = get_collection(model_config, scope, false)
37
+ collection_scope ? scope.instance_eval(&collection_scope) : scope
38
+ end
39
+
24
40
  private
25
41
 
26
42
  def action_missing(name, *_args)
@@ -78,9 +94,7 @@ module RailsAdminNext
78
94
  end
79
95
 
80
96
  def get_sort_hash(model_config)
81
- field = model_config.list.fields.detect { |f| f.name.to_s == params[:sort] }
82
- # If no sort param, default to the `sort_by` specified in the list config
83
- field ||= model_config.list.possible_fields.detect { |f| f.name == model_config.list.sort_by.try(:to_sym) }
97
+ field = sort_field(model_config)
84
98
 
85
99
  column =
86
100
  if field.nil? || field.sortable == false # use default sort, asked field does not exist or is not sortable
@@ -93,6 +107,12 @@ module RailsAdminNext
93
107
  {sort: column, sort_reverse: (params[:sort_reverse] == (field&.sort_reverse&.to_s || "true"))}
94
108
  end
95
109
 
110
+ def sort_field(model_config)
111
+ field = model_config.list.fields.detect { |f| f.name.to_s == params[:sort] }
112
+ # If no sort param, default to the `sort_by` specified in the list config
113
+ field || model_config.list.possible_fields.detect { |f| f.name == model_config.list.sort_by.try(:to_sym) }
114
+ end
115
+
96
116
  def redirect_to_on_success
97
117
  notice = I18n.t("admin.flash.successful", name: @model_config.label, action: I18n.t("admin.actions.#{@action.key}.done"))
98
118
  if params[:_add_another]
@@ -178,7 +198,8 @@ module RailsAdminNext
178
198
 
179
199
  def get_collection(model_config, scope, pagination)
180
200
  section = (@action.key == :export) ? model_config.export : model_config.list
181
- eager_loads = section.fields.flat_map(&:eager_load_values)
201
+ # The sort comes from the list, so export must also load the association a list sort orders by.
202
+ eager_loads = (section.fields + Array(sort_field(model_config))).flat_map(&:eager_load_values).uniq
182
203
  options = {}
183
204
  options = options.merge(page: (params[:page] || 1).to_i, per: params[:per] || model_config.list.items_per_page) if pagination
184
205
  options = options.merge(include: eager_loads) unless eager_loads.blank?
@@ -189,14 +210,39 @@ module RailsAdminNext
189
210
  model_config.abstract_model.all(options, scope)
190
211
  end
191
212
 
213
+ def list_scopes
214
+ @_list_scopes ||= @model_config.list.with(controller: self, view: view_context, object: @abstract_model.model.new).scopes
215
+ end
216
+
217
+ # A row-value IN: `where(primary_key => relation)` renders an empty condition for composite keys.
218
+ def within_authorized(scope, auth_scope)
219
+ key = Array(scope.primary_key).map { scope.arel_table[it] }
220
+ scope.where(Arel::Nodes::In.new(Arel::Nodes::Grouping.new(key), auth_scope.reselect(*key).arel.ast))
221
+ end
222
+
223
+ def list_scope
224
+ return if params[:compact]
225
+
226
+ if list_scopes.empty? || params[:scope].blank?
227
+ list_scopes.first
228
+ elsif list_scopes.collect(&:to_s).include?(params[:scope])
229
+ params[:scope].to_sym
230
+ end
231
+ end
232
+
192
233
  def get_association_scope_from_params
193
234
  return nil unless params[:associated_collection].present?
194
235
 
195
236
  source_abstract_model = RailsAdminNext::AbstractModel.new(to_model_name(params[:source_abstract_model]))
196
237
  source_model_config = source_abstract_model.config
197
- source_object = source_abstract_model.get(params[:source_object_id])
198
238
  action = params[:current_action].in?(%w[create update]) ? params[:current_action] : "edit"
199
- @association = source_model_config.send(action).fields.detect { |f| f.name == params[:associated_collection].to_sym }.with(controller: self, object: source_object)
239
+ field = source_model_config.send(action).fields.detect { |f| f.name == params[:associated_collection].to_sym }
240
+ # The picker's params are client-supplied; a field that doesn't target this model would run its scope and labels on the wrong records.
241
+ # Polymorphic pickers never send associated_collection, and their collection ignores the listed records.
242
+ return nil unless field&.association? && !field.polymorphic? && field.associated_model_config == @model_config
243
+
244
+ source_object = source_abstract_model.get(params[:source_object_id])
245
+ @association = field.with(controller: self, object: source_object)
200
246
  @association.associated_collection_scope
201
247
  end
202
248
  end
@@ -4,6 +4,10 @@ module RailsAdminNext
4
4
  class FormBuilder < ::ActionView::Helpers::FormBuilder
5
5
  include ::RailsAdminNext::ApplicationHelper
6
6
 
7
+ # Bounds the nested form of an association that can reach itself. Counting repeats of
8
+ # one association, rather than depth, leaves chains of distinct models uncapped.
9
+ MAX_NESTED_FORM_RECURSION = 3
10
+
7
11
  def generate(options = {})
8
12
  without_field_error_proc_added_div do
9
13
  options.reverse_merge!(
@@ -78,8 +82,10 @@ module RailsAdminNext
78
82
 
79
83
  # Wraps one nested-attributes subform (rendered by native `fields_for`) for
80
84
  # 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.
85
+ # `_destroy` hidden field (gated on `allow_destroy`, always present in the blank
86
+ # <template>), a remove button, then the recursively generated fields. A saved
87
+ # record is removable only under `allow_destroy`; an unsaved one always is, so a
88
+ # row added before a failed save can still be taken back.
83
89
  def nested_field_set(nested_form, field, allow_destroy:)
84
90
  @template.content_tag(:div, class: "fields tab-pane active", data: {"nested-form-target": "subform"}) do
85
91
  contents = []
@@ -88,8 +94,8 @@ module RailsAdminNext
88
94
  # persisted subform from a brand-new one. Suppress the auto-id in the
89
95
  # partials with `include_id: false` so it is not rendered twice.
90
96
  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"})
97
+ contents << nested_form.hidden_field(:_destroy, data: {"nested-form-target": "destroy"}) if allow_destroy
98
+ if allow_destroy || nested_form.object.new_record?
93
99
  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
100
  end
95
101
  contents << nested_form.generate(action: :nested, model_config: field.associated_model_config, nested_in: field)
@@ -97,6 +103,19 @@ module RailsAdminNext
97
103
  end
98
104
  end
99
105
 
106
+ # Whether this association's subforms, its blank <template> and its add control render
107
+ # here. Saved records can form a cycle too, so the bound covers them as well.
108
+ def nested_within_bound?(field)
109
+ @object_name.to_s.scan("[#{field.name}_attributes]").size < MAX_NESTED_FORM_RECURSION
110
+ end
111
+
112
+ # The child index the <template> carries until a row is added. Tagged by depth, not by
113
+ # association name alone: the add control replaces every occurrence in the template,
114
+ # nested templates included, and a name-tagged token can be a prefix of a nested one's.
115
+ def nested_placeholder(field)
116
+ "new_#{@object_name.to_s.scan("_attributes]").size + 1}_#{field.name}"
117
+ end
118
+
100
119
  def object_infos
101
120
  model_config = RailsAdminNext.config(object)
102
121
  model_label = model_config.label
@@ -29,7 +29,7 @@ module RailsAdminNext
29
29
  return @ordered_filters if @ordered_filters.present?
30
30
 
31
31
  @index = 0
32
- @ordered_filters = (params[:f].try(:permit!).try(:to_h) || @model_config.list.filters).each_with_object({}) do |filter, memo|
32
+ @ordered_filters = (filters_from_params || @model_config.list.filters).each_with_object({}) do |filter, memo|
33
33
  field_name = filter.is_a?(Array) ? filter.first : filter
34
34
  (filter.is_a?(Array) ? filter.last : {(@index += 1) => {"v" => ""}}).each do |index, filter_hash|
35
35
  if filter_hash["disabled"].blank?
@@ -41,6 +41,17 @@ module RailsAdminNext
41
41
  end.to_a.sort_by(&:first)
42
42
  end
43
43
 
44
+ # Only the field => index => filter shape the filter box submits, for fields that
45
+ # are filterable; hand-written or stale params are dropped rather than raising.
46
+ def filters_from_params
47
+ return unless params[:f].respond_to?(:permit!)
48
+
49
+ names = filterable_fields.map { it.name.to_s }
50
+ params[:f].permit!.to_h.filter_map do |name, filters_dump|
51
+ [name, filters_dump.select { |_index, filter| filter.is_a?(Hash) }] if names.include?(name) && filters_dump.is_a?(Hash)
52
+ end.to_h
53
+ end
54
+
44
55
  def ordered_filter_options
45
56
  if ordered_filters
46
57
  @ordered_filter_options ||= ordered_filters.map do |duplet|
@@ -1,7 +1,7 @@
1
1
  <%# locals: () -%>
2
2
  <meta content="IE=edge" http-equiv="X-UA-Compatible">
3
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">
4
+ <meta content="width=device-width, initial-scale=1" name="viewport">
5
5
  <meta content="NONE,NOARCHIVE" name="robots">
6
6
  <meta content="false" name="turbo-prefetch">
7
7
  <%= csrf_meta_tag %>
@@ -7,7 +7,7 @@
7
7
  </small>
8
8
  </a>
9
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">
10
+ <span class="visually-hidden">
11
11
  <%= t('admin.toggle_navigation') %>
12
12
  </span>
13
13
  <span class="navbar-toggler-icon"></span>
@@ -20,6 +20,9 @@
20
20
  <div class="container-fluid">
21
21
  <%= render template: 'layouts/rails_admin_next/content' %>
22
22
  </div>
23
+ <% if RailsAdminNext::Config.show_footer %>
24
+ <footer class="admin-footer">Made in Tokyo with â¤ī¸ and 🤖</footer>
25
+ <% end %>
23
26
  </div>
24
27
  </div>
25
28
  </div>
@@ -3,18 +3,23 @@
3
3
  associated_model = field.associated_model_config.abstract_model
4
4
  add_label = rails_admin_icon(:new) + " " + wording_for(:link, :new, associated_model)
5
5
  allow_destroy = field.nested_form && field.nested_form[:allow_destroy]
6
- placeholder = "new_#{field.name}"
6
+ placeholder = form.nested_placeholder(field)
7
+ template = form.nested_within_bound?(field)
7
8
  -%>
8
9
  <div class="controls col-sm-10" data-controller="nested-form" data-nested-form-placeholder-value="<%= placeholder %>">
9
10
  <div class="tab-content" data-nested-form-target="container">
10
- <%= form.fields_for field.name, nil, include_id: false do |nested_form| %>
11
- <%= form.nested_field_set nested_form, field, allow_destroy: allow_destroy %>
11
+ <% if template %>
12
+ <%= form.fields_for field.name, nil, include_id: false do |nested_form| %>
13
+ <%= form.nested_field_set nested_form, field, allow_destroy: allow_destroy %>
14
+ <% end %>
12
15
  <% end %>
13
16
  </div>
14
- <% if field.inline_add %>
17
+ <% if field.inline_add && template %>
15
18
  <button type="button" class="btn btn-info add_nested_fields" data-action="nested-form#add"><%= add_label %></button>
16
19
  <% end %>
17
20
  <%= form.errors_for(field) %>
18
21
  <%= form.help_for(field) %>
22
+ <% if template %>
19
23
  <template data-nested-form-target="template"><%= form.fields_for field.name, associated_model.model.new, child_index: placeholder, include_id: false do |nested_form| %><%= form.nested_field_set nested_form, field, allow_destroy: true %><% end %></template>
24
+ <% end %>
20
25
  </div>
@@ -3,17 +3,22 @@
3
3
  associated_model = field.associated_model_config.abstract_model
4
4
  add_label = rails_admin_icon(:new) + " " + wording_for(:link, :new, associated_model)
5
5
  allow_destroy = field.nested_form && field.nested_form[:allow_destroy]
6
+ template = form.nested_within_bound?(field)
6
7
  -%>
7
8
  <div class="controls col-sm-10" data-controller="nested-form" data-nested-form-singular-value="true">
8
9
  <div class="tab-content" data-nested-form-target="container">
9
- <%= form.fields_for field.name, nil, include_id: false do |nested_form| %>
10
- <%= form.nested_field_set nested_form, field, allow_destroy: allow_destroy %>
10
+ <% if template %>
11
+ <%= form.fields_for field.name, nil, include_id: false do |nested_form| %>
12
+ <%= form.nested_field_set nested_form, field, allow_destroy: allow_destroy %>
13
+ <% end %>
11
14
  <% end %>
12
15
  </div>
13
- <% if field.inline_add %>
16
+ <% if field.inline_add && template %>
14
17
  <button type="button" class="btn btn-info add_nested_fields" data-action="nested-form#add"><%= add_label %></button>
15
18
  <% end %>
16
19
  <%= form.errors_for(field) %>
17
20
  <%= form.help_for(field) %>
21
+ <% if template %>
18
22
  <template data-nested-form-target="template"><%= form.fields_for field.name, associated_model.model.new, include_id: false do |nested_form| %><%= form.nested_field_set nested_form, field, allow_destroy: true %><% end %></template>
23
+ <% end %>
19
24
  </div>
@@ -4,7 +4,7 @@
4
4
  <ul>
5
5
  <%= render partial: "delete_notice", collection: @objects %>
6
6
  </ul>
7
- <%= form_tag bulk_delete_path(model_name: @abstract_model.to_param, bulk_ids: @objects.map(&:id)), method: :delete do %>
7
+ <%= form_tag bulk_delete_path(model_name: @abstract_model.to_param, bulk_ids: @objects.map(&:id), scope: params[:scope].presence), method: :delete do %>
8
8
  <div class="form-actions">
9
9
  <input name="return_to" type="<%= :hidden %>" value="<%= (params[:return_to].presence || request.referer) %>" />
10
10
  <button class="btn btn-danger" data-disable-with="<%= t("admin.form.confirmation") %>" data-turbo-confirm="<%= t("admin.form.bulk_delete") %>" type="submit">
@@ -1,56 +1,58 @@
1
1
  <% if @abstract_models %>
2
- <table class="table table-condensed table-striped table-hover">
3
- <thead>
4
- <tr>
5
- <th class="shrink model-name">
6
- <%= t "admin.table_headers.model_name" %>
7
- </th>
8
- <th class="shrink last-created">
9
- <%= t "admin.table_headers.last_created" %>
10
- </th>
11
- <th class="records">
12
- <%= t "admin.table_headers.records" %>
13
- </th>
14
- <th class="shrink controls"></th>
15
- </tr>
16
- </thead>
17
- <tbody class="table-group-divider">
18
- <% @abstract_models.each do |abstract_model| %>
19
- <% if authorized? :index, abstract_model %>
20
- <% index_path = index_path(model_name: abstract_model.to_param) %>
21
- <% row_class = "#{cycle("odd", "even")}#{" link" if index_path} #{abstract_model.param_key}_links" %>
22
- <tr class="<%= row_class %>" data-link="<%= index_path %>">
23
- <% last_created = @most_recent_created[abstract_model.model.name] %>
24
- <% active = last_created.try(:today?) %>
25
- <td>
26
- <span class="show">
27
- <%= link_to abstract_model.config.label_plural, index_path %>
28
- </span>
29
- </td>
30
- <td>
31
- <% if last_created %>
32
- <%= t "admin.misc.time_ago", time: time_ago_in_words(last_created), default: "#{time_ago_in_words(last_created)} #{t("admin.misc.ago")}" %>
33
- <% end %>
34
- </td>
35
- <td>
36
- <% count = @count[abstract_model.model.name] %>
37
- <% percent = count > 0 ? (@max <= 1 ? count : ((Math.log(count+1) * 100.0) / Math.log(@max+1)).to_i) : -1 %>
38
- <div class="progress" style="margin-bottom:0px">
39
- <div class="bg-<%= get_indicator(percent) %> progress-bar<%= ' progress-bar-striped progress-bar-animated' if active %>" style="width:<%= [2.0, percent].max.to_i %>%">
40
- <%= @count[abstract_model.model.name] %>
2
+ <div class="dashboard-statistics">
3
+ <table class="table table-condensed table-striped table-hover">
4
+ <thead>
5
+ <tr>
6
+ <th class="shrink model-name">
7
+ <%= t "admin.table_headers.model_name" %>
8
+ </th>
9
+ <th class="shrink last-created">
10
+ <%= t "admin.table_headers.last_created" %>
11
+ </th>
12
+ <th class="records">
13
+ <%= t "admin.table_headers.records" %>
14
+ </th>
15
+ <th class="shrink controls"></th>
16
+ </tr>
17
+ </thead>
18
+ <tbody class="table-group-divider">
19
+ <% @abstract_models.each do |abstract_model| %>
20
+ <% if authorized? :index, abstract_model %>
21
+ <% index_path = index_path(model_name: abstract_model.to_param) %>
22
+ <% row_class = "#{cycle("odd", "even")}#{" link" if index_path} #{abstract_model.param_key}_links" %>
23
+ <tr class="<%= row_class %>" data-link="<%= index_path %>">
24
+ <% last_created = @most_recent_created[abstract_model.model.name] %>
25
+ <% active = last_created.try(:today?) %>
26
+ <td>
27
+ <span class="show">
28
+ <%= link_to abstract_model.config.label_plural, index_path %>
29
+ </span>
30
+ </td>
31
+ <td>
32
+ <% if last_created %>
33
+ <%= t "admin.misc.time_ago", time: time_ago_in_words(last_created), default: "#{time_ago_in_words(last_created)} #{t("admin.misc.ago")}" %>
34
+ <% end %>
35
+ </td>
36
+ <td>
37
+ <% count = @count[abstract_model.model.name] %>
38
+ <% percent = count > 0 ? (@max <= 1 ? count : ((Math.log(count+1) * 100.0) / Math.log(@max+1)).to_i) : -1 %>
39
+ <div class="progress" style="margin-bottom:0px">
40
+ <div class="bg-<%= get_indicator(percent) %> progress-bar<%= ' progress-bar-striped progress-bar-animated' if active %>" style="width:<%= [2.0, percent].max.to_i %>%">
41
+ <%= @count[abstract_model.model.name] %>
42
+ </div>
41
43
  </div>
42
- </div>
43
- </td>
44
- <td class="links">
45
- <ul class="nav d-inline list-inline">
46
- <%= menu_for :collection, abstract_model, nil, true %>
47
- </ul>
48
- </td>
49
- </tr>
44
+ </td>
45
+ <td class="links">
46
+ <ul class="nav d-inline list-inline">
47
+ <%= menu_for :collection, abstract_model, nil, true %>
48
+ </ul>
49
+ </td>
50
+ </tr>
51
+ <% end %>
50
52
  <% end %>
51
- <% end %>
52
- </tbody>
53
- </table>
53
+ </tbody>
54
+ </table>
55
+ </div>
54
56
  <% end %>
55
57
  <% if @history && authorized?(:history_index) %>
56
58
  <div class="block" id="block-tables">
@@ -82,9 +82,9 @@
82
82
  </div>
83
83
  </div>
84
84
  <% end %>
85
- <% unless @model_config.list.scopes.empty? %>
85
+ <% unless list_scopes.empty? %>
86
86
  <ul class="nav nav-tabs" id="scope_selector">
87
- <% @model_config.list.scopes.each_with_index do |scope, index| %>
87
+ <% list_scopes.each_with_index do |scope, index| %>
88
88
  <% scope = '_all' if scope.nil? %>
89
89
  <li class="nav-item">
90
90
  <a href="<%= index_path(params.merge(scope: scope, page: nil)) %>" class="nav-link <%= 'active' if scope.to_s == params[:scope] || (params[:scope].blank? && index == 0) %>">
@@ -96,6 +96,7 @@
96
96
  <% end %>
97
97
  <%= form_tag bulk_action_path(model_name: @abstract_model.to_param), method: :post, id: "bulk_form", class: ["form", "mb-3"] do %>
98
98
  <%= hidden_field_tag :bulk_action %>
99
+ <%= hidden_field_tag :scope, params[:scope] if params[:scope].present? %>
99
100
  <% if description.present? %>
100
101
  <p>
101
102
  <strong>