rails_admin_next 1.2.0 → 1.3.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -4
  3. data/app/assets/stylesheets/rails_admin/skin.css +4 -0
  4. data/app/controllers/rails_admin_next/main_controller.rb +64 -7
  5. data/app/helpers/rails_admin_next/form_builder.rb +23 -4
  6. data/app/helpers/rails_admin_next/main_helper.rb +12 -1
  7. data/app/views/layouts/rails_admin_next/_head.html.erb +1 -1
  8. data/app/views/layouts/rails_admin_next/_navigation.html.erb +1 -1
  9. data/app/views/rails_admin_next/main/_form_nested_many.html.erb +9 -4
  10. data/app/views/rails_admin_next/main/_form_nested_one.html.erb +8 -3
  11. data/app/views/rails_admin_next/main/bulk_delete.html.erb +1 -1
  12. data/app/views/rails_admin_next/main/dashboard.html.erb +52 -50
  13. data/app/views/rails_admin_next/main/index.html.erb +3 -2
  14. data/docs/UPGRADING.md +160 -2
  15. data/docs/security.md +5 -0
  16. data/docs/upstream-parity.md +47 -3
  17. data/lib/rails_admin_next/abstract_model.rb +12 -4
  18. data/lib/rails_admin_next/adapters/active_record.rb +47 -3
  19. data/lib/rails_admin_next/config/actions/bulk_delete.rb +4 -4
  20. data/lib/rails_admin_next/config/actions/export.rb +2 -1
  21. data/lib/rails_admin_next/config/actions/index.rb +5 -21
  22. data/lib/rails_admin_next/config/actions/show.rb +1 -1
  23. data/lib/rails_admin_next/config/actions.rb +5 -12
  24. data/lib/rails_admin_next/config/fields/association.rb +1 -1
  25. data/lib/rails_admin_next/config/fields/types/enum.rb +8 -2
  26. data/lib/rails_admin_next/config/fields/types/serialized.rb +17 -2
  27. data/lib/rails_admin_next/config/fields/types/timestamp.rb +1 -0
  28. data/lib/rails_admin_next/config/has_fields.rb +1 -1
  29. data/lib/rails_admin_next/config.rb +4 -4
  30. data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +1 -1
  31. data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +4 -2
  32. data/lib/rails_admin_next/support/composite_keys_serializer.rb +8 -2
  33. data/lib/rails_admin_next/support/csv_converter.rb +45 -5
  34. data/lib/rails_admin_next/support/serialization_schema.rb +74 -0
  35. data/lib/rails_admin_next/version.rb +1 -1
  36. data/lib/rails_admin_next.rb +5 -16
  37. data/src/rails_admin/controllers/nested_form_controller.js +1 -1
  38. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbd1b1c7b37f7d5d89eb5f9ef44dc086bedf1ecbd47f908c545afbb35e8f8bc1
4
- data.tar.gz: 9358676100cad7935f07c91a757e27a761fe463e1a2fc6b291f8d8a0dc63df0d
3
+ metadata.gz: 79af82d2ebf188875574650be98cd21e394a1a324b887cc45518d49f9fa3ec41
4
+ data.tar.gz: 3a1d0d3b539034bc59e0019b1d56ff5dd3420285ea91a73ab193533de5f26c53
5
5
  SHA512:
6
- metadata.gz: 8b999764cff3b8b068b54f5bba5ac2107d9f4545584a76be7764c63b24cfc7135560ce7372fc953bae2aab8a5ef5e3e09735e3e592125b93d21ee70f6f5662a8
7
- data.tar.gz: 477419b70e7056875a32b0790422a1310423d93cd43d91bc68007e918dace9b30acd3a6ad55c38eff654df59fbbe11bb87e5a8141c669807849897519a2ad8d0
6
+ metadata.gz: c20bd5d419f98867c7f2528ccf8139e5e09618ee13d4d982457fc983905c41d79b60d04c46971021531ccbc9607ceaace4ddd99072242c6003187c8e656a09f5
7
+ data.tar.gz: 3c06806088cbb19f949301c5150eba329392eccde2ee96128c3fb9a8ac23b3639418d358d4074ea59f1995aa186a9a399e17b86374c280953fb526b2ff1deb48
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![Build](https://github.com/6temes/rails_admin_next/actions/workflows/test.yml/badge.svg)](https://github.com/6temes/rails_admin_next/actions/workflows/test.yml)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md)
11
- [![Ruby](https://img.shields.io/badge/Ruby-4.0.5%2B-CC342D.svg)](https://www.ruby-lang.org)
11
+ [![Ruby](https://img.shields.io/badge/Ruby-4.0.9%2B-CC342D.svg)](https://www.ruby-lang.org)
12
12
  [![Rails](https://img.shields.io/badge/Rails-8.1-CC0000.svg)](https://rubyonrails.org)
13
13
 
14
14
  [Why RailsAdminNext?](#why-railsadminnext) ◆ [Quick Start](#quick-start) ◆ [Installation](#installation) ◆ [Architecture](#architecture) ◆ [Security](#security) ◆ [Upgrading](#upgrading-from-railsadmin)
@@ -32,7 +32,7 @@ multi-bundler, jQuery-era machinery. RailsAdminNext keeps the admin and drops th
32
32
  - **⚡ Zero build step** — browser-native ESM over **importmap + Propshaft**; no Webpack/Vite/Sprockets, no `yarn build`, no generated pins.
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
- - **🎯 One modern target** — Ruby ≥ 4.0.5, Rails ~> 8.1, ActiveRecord only, latest Chrome + Safari. No Mongoid, no multi-bundler / multi-Rails matrix, no legacy-browser CSS.
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
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.
37
37
 
38
38
  ## Quick Start
@@ -50,17 +50,21 @@ bin/rails s # then open http://localhost:3000/admin
50
50
 
51
51
  That's the whole setup — no `yarn add`, no asset build, no generated pins. The engine ships its own importmap and CSS.
52
52
 
53
- > **Prerequisites:** Ruby ≥ 4.0.5 · Rails ~> 8.1 · ActiveRecord · a [Propshaft][propshaft] + [importmap-rails][importmap] host (both are pulled in as dependencies). See [Installation](#installation) for the full walkthrough.
53
+ > **Prerequisites:** Ruby ≥ 4.0.9 · Rails ~> 8.1 · ActiveRecord · a [Propshaft][propshaft] + [importmap-rails][importmap] host (both are pulled in as dependencies). See [Installation](#installation) for the full walkthrough.
54
54
 
55
55
  ## Requirements
56
56
 
57
- - Ruby >= 4.0.5
57
+ - Ruby >= 4.0.9
58
58
  - Rails ~> 8.1
59
59
  - ActiveRecord (Mongoid is not supported)
60
60
  - [Propshaft][propshaft] and [importmap-rails][importmap] (both are pulled in as
61
61
  dependencies; RailsAdminNext serves its frontend over them, with no bundler or build step)
62
62
  - Latest stable Chrome or Safari for the admin's users (see [Browser support](#browser-support))
63
63
 
64
+ RailsAdminNext expects **evergreen** hosts. The Ruby floor follows the latest stable Ruby
65
+ patch release, and a release can raise it without a major version bump. Keep your app on
66
+ current Ruby and Rails to keep receiving updates.
67
+
64
68
  [propshaft]: https://github.com/rails/propshaft
65
69
  [importmap]: https://github.com/rails/importmap-rails
66
70
 
@@ -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;
@@ -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]
@@ -108,6 +128,17 @@ module RailsAdminNext
108
128
  model_config.send(action).with(controller: self, view: view_context, object: @object).visible_fields
109
129
  end
110
130
 
131
+ # Memoized, as export renders through index, which switches @action
132
+ def serialization_schema
133
+ @serialization_schema ||= begin
134
+ section = %i[export show].include?(@action.key) ? @model_config.send(@action.key) : @model_config.list
135
+ SerializationSchema.new(
136
+ section.with(controller: self, view: view_context, object: @object || @abstract_model.model.new),
137
+ include_associations: @action.key == :export
138
+ )
139
+ end
140
+ end
141
+
111
142
  def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key], nested_in = nil)
112
143
  return unless target_params.present?
113
144
 
@@ -167,7 +198,8 @@ module RailsAdminNext
167
198
 
168
199
  def get_collection(model_config, scope, pagination)
169
200
  section = (@action.key == :export) ? model_config.export : model_config.list
170
- 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
171
203
  options = {}
172
204
  options = options.merge(page: (params[:page] || 1).to_i, per: params[:per] || model_config.list.items_per_page) if pagination
173
205
  options = options.merge(include: eager_loads) unless eager_loads.blank?
@@ -178,14 +210,39 @@ module RailsAdminNext
178
210
  model_config.abstract_model.all(options, scope)
179
211
  end
180
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
+
181
233
  def get_association_scope_from_params
182
234
  return nil unless params[:associated_collection].present?
183
235
 
184
236
  source_abstract_model = RailsAdminNext::AbstractModel.new(to_model_name(params[:source_abstract_model]))
185
237
  source_model_config = source_abstract_model.config
186
- source_object = source_abstract_model.get(params[:source_object_id])
187
238
  action = params[:current_action].in?(%w[create update]) ? params[:current_action] : "edit"
188
- @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)
189
246
  @association.associated_collection_scope
190
247
  end
191
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>
@@ -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>