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.
- checksums.yaml +4 -4
- data/README.md +8 -4
- data/app/assets/stylesheets/rails_admin/skin.css +4 -0
- data/app/controllers/rails_admin_next/main_controller.rb +64 -7
- data/app/helpers/rails_admin_next/form_builder.rb +23 -4
- data/app/helpers/rails_admin_next/main_helper.rb +12 -1
- data/app/views/layouts/rails_admin_next/_head.html.erb +1 -1
- data/app/views/layouts/rails_admin_next/_navigation.html.erb +1 -1
- data/app/views/rails_admin_next/main/_form_nested_many.html.erb +9 -4
- data/app/views/rails_admin_next/main/_form_nested_one.html.erb +8 -3
- data/app/views/rails_admin_next/main/bulk_delete.html.erb +1 -1
- data/app/views/rails_admin_next/main/dashboard.html.erb +52 -50
- data/app/views/rails_admin_next/main/index.html.erb +3 -2
- data/docs/UPGRADING.md +160 -2
- data/docs/security.md +5 -0
- data/docs/upstream-parity.md +47 -3
- data/lib/rails_admin_next/abstract_model.rb +12 -4
- data/lib/rails_admin_next/adapters/active_record.rb +47 -3
- data/lib/rails_admin_next/config/actions/bulk_delete.rb +4 -4
- data/lib/rails_admin_next/config/actions/export.rb +2 -1
- data/lib/rails_admin_next/config/actions/index.rb +5 -21
- data/lib/rails_admin_next/config/actions/show.rb +1 -1
- data/lib/rails_admin_next/config/actions.rb +5 -12
- data/lib/rails_admin_next/config/fields/association.rb +1 -1
- data/lib/rails_admin_next/config/fields/types/enum.rb +8 -2
- data/lib/rails_admin_next/config/fields/types/serialized.rb +17 -2
- data/lib/rails_admin_next/config/fields/types/timestamp.rb +1 -0
- data/lib/rails_admin_next/config/has_fields.rb +1 -1
- data/lib/rails_admin_next/config.rb +4 -4
- data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +1 -1
- data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +4 -2
- data/lib/rails_admin_next/support/composite_keys_serializer.rb +8 -2
- data/lib/rails_admin_next/support/csv_converter.rb +45 -5
- data/lib/rails_admin_next/support/serialization_schema.rb +74 -0
- data/lib/rails_admin_next/version.rb +1 -1
- data/lib/rails_admin_next.rb +5 -16
- data/src/rails_admin/controllers/nested_form_controller.js +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79af82d2ebf188875574650be98cd21e394a1a324b887cc45518d49f9fa3ec41
|
|
4
|
+
data.tar.gz: 3a1d0d3b539034bc59e0019b1d56ff5dd3420285ea91a73ab193533de5f26c53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c20bd5d419f98867c7f2528ccf8139e5e09618ee13d4d982457fc983905c41d79b60d04c46971021531ccbc9607ceaace4ddd99072242c6003187c8e656a09f5
|
|
7
|
+
data.tar.gz: 3c06806088cbb19f949301c5150eba329392eccde2ee96128c3fb9a8ac23b3639418d358d4074ea59f1995aa186a9a399e17b86374c280953fb526b2ff1deb48
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/6temes/rails_admin_next/actions/workflows/test.yml)
|
|
10
10
|
[](LICENSE.md)
|
|
11
|
-
[](https://www.ruby-lang.org)
|
|
12
12
|
[](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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
82
|
-
#
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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="
|
|
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 =
|
|
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
|
-
|
|
11
|
-
<%= form.
|
|
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
|
-
|
|
10
|
-
<%= form.
|
|
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
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<%
|
|
20
|
-
<%
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<div class="
|
|
40
|
-
<%=
|
|
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
|
-
</
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</
|
|
48
|
-
</
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
</
|
|
53
|
-
</
|
|
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
|
|
85
|
+
<% unless list_scopes.empty? %>
|
|
86
86
|
<ul class="nav nav-tabs" id="scope_selector">
|
|
87
|
-
<%
|
|
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>
|