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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +32 -20
- data/app/assets/stylesheets/rails_admin/skin.css +12 -0
- data/app/controllers/rails_admin_next/main_controller.rb +53 -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/layouts/rails_admin_next/application.html.erb +3 -0
- 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 +263 -9
- data/docs/security.md +4 -0
- data/lib/generators/rails_admin_next/templates/initializer.erb +7 -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/base.rb +0 -1
- data/lib/rails_admin_next/config/actions/bulk_delete.rb +4 -4
- data/lib/rails_admin_next/config/actions/index.rb +3 -19
- 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 +29 -9
- data/lib/rails_admin_next/extensions/action_policy/authorization_adapter.rb +60 -0
- data/lib/rails_admin_next/extensions/action_policy.rb +5 -0
- data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +1 -1
- data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +17 -1
- 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/version.rb +3 -3
- data/lib/rails_admin_next.rb +5 -17
- data/skills/rails-admin-next/SKILL.md +5 -4
- data/src/rails_admin/controllers/nested_form_controller.js +1 -1
- metadata +3 -3
- data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +0 -90
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85cf357bfb48a568e28ee9d031d96f9d9d96546564579a17273d06ddd1643a45
|
|
4
|
+
data.tar.gz: a3850dcf1e9f5fb4bf6797a1175be2112d6f5ecc1d9f4588dfc561a412ce9072
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 950489886f285808c1d8688a3099361d63bc0f2bead9b15b3b752c3de41e696a48232f63bd6bdce8ec6835a9266b35f3b13a1faebc10572a0de1daf05b87a61b
|
|
7
|
+
data.tar.gz: 6dcc9aa9063071f2008888dff5c1c4e43042c6fd3026ce6c52bdd5844080f0276cbb719241427c84254d95d2b05160cf066aaf699ddd14ac03c5c9204a5d791d
|
data/Gemfile
CHANGED
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
|
|
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 [
|
|
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], [
|
|
147
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
`:
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
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>
|
|
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 =
|
|
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]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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>
|
|
@@ -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 =
|
|
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>
|