rails_admin_next 1.0.1 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2beeee63d5a457910c59511f4f4f28062df8c2349fb0e7bba5ee36cb8625ef2
4
- data.tar.gz: e9c500e67f698ac56486353df1e0c20e956d9cec9a63fa65fa036ac35e3fb834
3
+ metadata.gz: a1e669bb86719b972a161917a8f2b88747a6be2e2233ada17c3a437e6dfdeaaf
4
+ data.tar.gz: 66a69ecedbd7e4fdca9991e2044bede57d842f5bc7e1d29159a2fb4ef41a361f
5
5
  SHA512:
6
- metadata.gz: 65db352b31758be6c51c44be449b327cde1cc64e3de279ad826c86faf68baacf317d957e524d313b0c859d0c43fcbf3519e7b17025d418cc2e00157c0be0323d
7
- data.tar.gz: 01d302227dcb922055eac4c33f06860225d895e8a4c237f92c6e9b48bf78abf631d99b71d5857b271e3be0e6482e1ad7b84b76244165bb07baa5350eb1650f57
6
+ metadata.gz: 85c4a2ad4d178fee59d393020db78d735c114c8b0bb41cf42f14e70017363edd78bb49e0c461db95e8098901f30bf974a2a93683f70faf3cbb935978468cd24f
7
+ data.tar.gz: 0a60be77736c263a0dabea8abb46f573948b56ee672b0ca75473012feeb5a8889a3d5bac60668a3057602f7c5b51db097f9ff39171e210671a8d16ef3856db99
data/Gemfile CHANGED
@@ -7,40 +7,39 @@ gem "rails"
7
7
  gem "turbo-rails"
8
8
 
9
9
  group :development, :test do
10
- gem "pry", ">= 0.9"
10
+ gem "pry"
11
11
  end
12
12
 
13
13
  group :test do
14
14
  gem "brakeman", require: false
15
- gem "bundler-audit", require: false
16
15
  gem "cancancan", "~> 3.0"
17
- gem "cuprite", "!= 0.15.1"
18
- gem "database_cleaner-active_record", ">= 2.0", require: false
19
- gem "factory_bot", ">= 4.2", "!= 6.4.5"
20
- gem "generator_spec", ">= 0.8"
21
- gem "image_processing", ">= 1.2"
16
+ gem "cuprite"
17
+ gem "database_cleaner-active_record", require: false
18
+ gem "factory_bot"
19
+ gem "generator_spec"
20
+ gem "image_processing"
22
21
  gem "pundit"
23
- gem "rspec-expectations", "!= 3.8.3"
24
- gem "rspec-rails", ">= 4.0.0.beta2"
22
+ gem "rspec-rails"
25
23
  gem "rspec-retry"
26
- gem "ruby-vips", ">= 2.1"
27
- gem "simplecov", ">= 0.9", require: false
28
- gem "simplecov-lcov", require: false
29
- gem "standard", ">= 1.35.1", require: false
30
-
31
- # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
32
- gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
24
+ gem "ruby-vips"
25
+ gem "standard", require: false
33
26
  gem "warden"
27
+ # Capybara's own default server is puma, which is not in this bundle; spec_helper selects
28
+ # webrick instead. It arrives as a transitive of ferrum, so declare what the suite runs on
29
+ # rather than borrowing someone else's dependency.
30
+ gem "webrick"
31
+
32
+ # Windows does not ship zoneinfo files.
33
+ gem "tzinfo-data", platforms: %i[windows]
34
34
  end
35
35
 
36
+ # The dummy app boots with `Bundler.require(*Rails.groups, :active_record)`, so this group is
37
+ # loaded in every environment — that is what puts the adapter on the load path outside :test.
36
38
  group :active_record do
37
- gem "paper_trail", ">= 12.0"
38
-
39
- platforms :ruby, :mswin, :mingw, :x64_mingw do
40
- gem "mysql2", ">= 0.3.14"
41
- gem "pg", ">= 1.0.0"
42
- gem "sqlite3", ">= 1.3.0"
43
- end
39
+ gem "mysql2"
40
+ gem "paper_trail"
41
+ gem "pg"
42
+ gem "sqlite3"
44
43
  end
45
44
 
46
45
  gemspec
@@ -108,10 +108,17 @@ module RailsAdminNext
108
108
  model_config.send(action).with(controller: self, view: view_context, object: @object).visible_fields
109
109
  end
110
110
 
111
- def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key])
111
+ def sanitize_params_for!(action, model_config = @model_config, target_params = params[@abstract_model.param_key], nested_in = nil)
112
112
  return unless target_params.present?
113
113
 
114
- fields = visible_fields(action, model_config)
114
+ # A subform never draws the child's link back to the record being edited — the parent
115
+ # association names it, and FormBuilder#nested_field_association? skips it on that name.
116
+ # Deriving the allowlist from visible_fields alone would keep permitting keys no form of
117
+ # ours submits, so drop that one field here too. Only at nested depth: editing a Comment
118
+ # on its own is a legitimate way to set its commentable.
119
+ fields = visible_fields(action, model_config).reject do |field|
120
+ nested_in && field.is_a?(Config::Fields::Association) && field.name == nested_in.inverse_of
121
+ end
115
122
  allowed_methods = fields.collect(&:allowed_methods).flatten.uniq.collect(&:to_s) << "id" << "_destroy"
116
123
  fields.each { |field| field.parse_input(target_params) }
117
124
  target_params.slice!(*allowed_methods)
@@ -132,7 +139,7 @@ module RailsAdminNext
132
139
  []
133
140
  end
134
141
  children_params.each do |children_param|
135
- sanitize_params_for!(:nested, association.associated_model_config, children_param) if children_param.respond_to?(:values)
142
+ sanitize_params_for!(:nested, association.associated_model_config, children_param, association) if children_param.respond_to?(:values)
136
143
  end
137
144
  end
138
145
  end
@@ -144,7 +144,7 @@ module RailsAdminNext
144
144
  content_tag(:li, class: ["breadcrumb-item", current_action?(a, am, o) && "active"]) do
145
145
  if current_action?(a, am, o)
146
146
  wording_for(:breadcrumb, a, am, o)
147
- elsif a.http_methods.include?(:get)
147
+ elsif a.linkable?
148
148
  link_to rails_admin_next.url_for(action: a.action_name, controller: "rails_admin_next/main", model_name: am.try(:to_param), id: o.try(:persisted?) && o.try(:id) || nil) do
149
149
  wording_for(:breadcrumb, a, am, o)
150
150
  end
@@ -159,7 +159,7 @@ module RailsAdminNext
159
159
  # parent => :root, :collection, :member
160
160
  # perf matters here (no action view trickery)
161
161
  def menu_for(parent, abstract_model = nil, object = nil, only_icon = false)
162
- actions = actions(parent, abstract_model, object).select { |a| a.http_methods.include?(:get) && a.show_in_menu }
162
+ actions = actions(parent, abstract_model, object).select { |a| a.linkable? && a.show_in_menu }
163
163
  actions.collect do |action|
164
164
  wording = wording_for(:menu, action)
165
165
  li_class = ["nav-item", "icon", "#{action.key}_#{parent}_link"]
@@ -164,10 +164,17 @@ module RailsAdminNext
164
164
 
165
165
  private
166
166
 
167
+ # A field the parent association names as its inverse is the child's link back to
168
+ # the record being edited, so the user is never asked to fill it. That name match
169
+ # stands on its own: a polymorphic child such as Comment#commentable declares no
170
+ # inverse_of, so gating it on the child's own inverse_of would never let it fire.
171
+ # It stays limited to association fields because a polymorphic `as:` is never checked
172
+ # against the child class, and a plain column could otherwise match it by name.
167
173
  def nested_field_association?(field, nested_in)
168
- field.inverse_of.presence && nested_in.presence && field.inverse_of == nested_in.name &&
169
- (@template.instance_variable_get(:@model_config).abstract_model == field.abstract_model ||
170
- field.name == nested_in.inverse_of)
174
+ nested_in.presence &&
175
+ ((field.is_a?(Config::Fields::Association) && field.name == nested_in.inverse_of) ||
176
+ (field.inverse_of.presence && field.inverse_of == nested_in.name &&
177
+ @template.instance_variable_get(:@model_config).abstract_model == field.abstract_model))
171
178
  end
172
179
  end
173
180
  end
@@ -10,17 +10,19 @@
10
10
  CSP out of the picture and lets :light/:dark pins override the OS preference. %>
11
11
  <meta content="<%= {auto: 'light dark', light: 'light', dark: 'dark'}.fetch(RailsAdminNext.config.color_scheme) %>" name="color-scheme">
12
12
  <%# The entry stylesheet pulls tokens/framework/skin via @import, which the browser only discovers
13
- after fetching and parsing the entry file. Preload the three targets so all four fetch in parallel. %>
14
- <link rel="preload" as="style" href="<%= asset_path('rails_admin/tokens.css') %>">
15
- <link rel="preload" as="style" href="<%= asset_path('rails_admin/framework.css') %>">
16
- <link rel="preload" as="style" href="<%= asset_path('rails_admin/skin.css') %>">
13
+ after fetching and parsing the entry file. Preload the three targets so all four fetch in parallel.
14
+ A nonce-based style-src blocks a preload the same as a stylesheet, so these carry the nonce too;
15
+ `tag` drops the attribute entirely when no policy is configured and the nonce is nil. %>
16
+ <% %w[tokens framework skin].each do |sheet| %>
17
+ <%= tag.link rel: "preload", as: "style", href: asset_path("rails_admin/#{sheet}.css"), nonce: content_security_policy_nonce %>
18
+ <% end %>
17
19
  <%# Date/time fields use native HTML5 inputs; the picker UI is the browser's. Icons are inline SVG (no icon font). %>
18
- <%= stylesheet_link_tag "rails_admin", media: :all, data: {'turbo-track': 'reload'} %>
20
+ <%= stylesheet_link_tag "rails_admin", media: :all, nonce: true, data: {'turbo-track': 'reload'} %>
19
21
  <%# Rich-text (ActionText) fields render a Trix editor; load its stylesheet so the toolbar is styled.
20
22
  trix.css ships with the action_text-trix gem and is on the Propshaft load path when ActionText is used.
21
23
  NOTE: trix.css is light-only for now — dark styling for the Trix toolbar/editor is deferred. %>
22
24
  <% if defined?(ActionText::Engine) %>
23
- <%= stylesheet_link_tag "trix", media: :all, data: {'turbo-track': 'reload'} %>
25
+ <%= stylesheet_link_tag "trix", media: :all, nonce: true, data: {'turbo-track': 'reload'} %>
24
26
  <% end %>
25
27
  <%# Zero-build delivery: browser-native ESM pinned by the engine-owned importmap, served by Propshaft.
26
28
  The importmap-rails helpers thread the request CSP nonce onto every inline <script> they emit. %>
@@ -53,6 +53,7 @@ en:
53
53
  flash:
54
54
  successful: "%{name} successfully %{action}"
55
55
  error: "%{name} failed to be %{action}"
56
+ delete_restricted: "%{name} failed to be deleted: %{reason}"
56
57
  noaction: "No actions were taken"
57
58
  model_not_found: "Model '%{model}' could not be found"
58
59
  object_not_found: "%{model} with id '%{id}' could not be found"
@@ -131,6 +131,17 @@ module RailsAdminNext
131
131
  end
132
132
 
133
133
  def to_statement
134
+ # A multi-select filter submits its values as an array, so the sentinel has to
135
+ # be stripped out of one rather than compared against it. An array emptied by
136
+ # that strip discards the filter, but one that arrived empty does not: a date
137
+ # filter carries its meaning in the operator alone ("today", "this_week").
138
+ #
139
+ # "between" is excluded because it alone reads the array by index, so removing
140
+ # an element would shift the range ends rather than discard anything.
141
+ if @value.is_a?(Array) && @operator != "between" && @value.include?("_discard")
142
+ @value -= ["_discard"]
143
+ return if @value.empty?
144
+ end
134
145
  return if [@operator, @value].any? { |v| v == "_discard" }
135
146
 
136
147
  unary_operators[@operator] || unary_operators[@value] ||
@@ -104,8 +104,31 @@ module RailsAdminNext
104
104
  options[:polymorphic] || false
105
105
  end
106
106
 
107
+ # A polymorphic `as:` names the belongs_to on the other side, and Rails resolves
108
+ # an inverse for conventional pairs that declare nothing — both are the
109
+ # association's inverse for form-nesting purposes. `inverse_of: false` opts out
110
+ # of all of it, the way it does in Rails.
111
+ #
112
+ # The declared option is read first because it has to be: asking Rails for the
113
+ # inverse of a polymorphic belongs_to that declares one raises, since there is no
114
+ # single class to look the name up on.
107
115
  def inverse_of
108
- options[:inverse_of].try :to_sym
116
+ return nil if options[:inverse_of] == false
117
+
118
+ options[:inverse_of].try(:to_sym) || association.inverse_of&.name || as
119
+ end
120
+
121
+ # Rails guards the presence validator it adds for a required belongs_to with
122
+ # an `if:` condition, and the validator scan in Fields::Base#required? skips
123
+ # conditional validators, so required-ness has to come from the reflection.
124
+ def required?
125
+ return false unless type == :belongs_to
126
+ # A `default:` association is filled by a before_validation callback, so the
127
+ # record validates without the user choosing anything.
128
+ return false if options[:default]
129
+
130
+ optional = options[:optional]
131
+ optional.nil? ? model.belongs_to_required_by_default : !optional
109
132
  end
110
133
 
111
134
  def read_only?
@@ -147,6 +147,13 @@ module RailsAdminNext
147
147
 
148
148
  # Off API.
149
149
 
150
+ # Whether visiting the action's URL performs it, so it can be rendered as a
151
+ # plain link rather than a button. Verbs are host-supplied, so they may be
152
+ # written as :GET or "get".
153
+ def linkable?
154
+ http_methods.include?(:get) || http_methods.any? { |verb| verb.to_s.casecmp?("get") }
155
+ end
156
+
150
157
  def key
151
158
  self.class.key
152
159
  end
@@ -34,9 +34,14 @@ module RailsAdminNext
34
34
  unless params[:bulk_ids].blank?
35
35
  @objects = list_entries(@model_config, :destroy)
36
36
  unless @objects.blank?
37
- processed_objects = @abstract_model.destroy(@objects)
38
- destroyed = processed_objects.select(&:destroyed?)
39
- not_destroyed = processed_objects - destroyed
37
+ # Rescue per record: dependent: :restrict_with_exception raises, and an
38
+ # escaping raise would abandon every record behind it in the batch.
39
+ destroyed, not_destroyed = Array.wrap(@objects).partition do |object|
40
+ object.destroy
41
+ object.destroyed?
42
+ rescue ActiveRecord::DeleteRestrictionError
43
+ false
44
+ end
40
45
  destroyed.each do |object|
41
46
  @auditing_adapter&.delete_object(object, @abstract_model, _current_user)
42
47
  end
@@ -33,12 +33,20 @@ module RailsAdminNext
33
33
 
34
34
  elsif request.delete? # DESTROY
35
35
 
36
- @auditing_adapter&.delete_object(@object, @abstract_model, _current_user)
37
- if @object.destroy
38
- flash[:success] = t("admin.flash.successful", name: @model_config.label, action: t("admin.actions.delete.done"))
39
- redirect_to index_path
40
- else
41
- handle_save_error :delete
36
+ begin
37
+ if @object.destroy
38
+ @auditing_adapter&.delete_object(@object, @abstract_model, _current_user)
39
+ flash[:success] = t("admin.flash.successful", name: @model_config.label, action: t("admin.actions.delete.done"))
40
+ redirect_to index_path
41
+ else
42
+ handle_save_error :delete
43
+ end
44
+ rescue ActiveRecord::DeleteRestrictionError => e
45
+ # dependent: :restrict_with_exception raises instead of adding an error to the
46
+ # record, so there is nothing for handle_save_error to render — report the
47
+ # association the exception names and send the admin back where they came from.
48
+ flash[:error] = t("admin.flash.delete_restricted", name: @model_config.label, reason: e.message)
49
+ redirect_to back_or_index, status: :see_other
42
50
  end
43
51
 
44
52
  end
@@ -20,9 +20,11 @@ module RailsAdminNext
20
20
 
21
21
  register_instance_option :controller do
22
22
  proc do
23
+ # HTML first: an Accept-less request arrives as */*, which matches every
24
+ # registered type, so Rails falls back to whichever is declared first.
23
25
  respond_to do |format|
24
- format.json { render json: @object }
25
26
  format.html { render @action.template_name }
27
+ format.json { render json: @object }
26
28
  end
27
29
  end
28
30
  end
@@ -55,9 +55,13 @@ module RailsAdminNext
55
55
  option_name = option_name.to_s
56
56
  options[option_name] = nil
57
57
 
58
- # If it's a boolean create an alias for it and remove question mark
58
+ # If it's a boolean create an alias for it and remove question mark. The
59
+ # rebinding is what the rest of the method reads: the getter, the setter, the
60
+ # @<name>_registered variable and the recursion key all want the chopped name,
61
+ # while @config_options keeps the un-chopped one it was registered under.
59
62
  if option_name.end_with?("?")
60
- scope.send(:define_method, "#{option_name.chop!}?") do
63
+ option_name = option_name.chop
64
+ scope.send(:define_method, "#{option_name}?") do
61
65
  send(option_name)
62
66
  end
63
67
  end
@@ -16,6 +16,9 @@ module RailsAdminNext
16
16
  include RailsAdminNext::Config::Groupable
17
17
  include RailsAdminNext::Config::Inspectable
18
18
 
19
+ # The length-validation options that are compared against the column limit.
20
+ LENGTH_BOUNDS = %i[is maximum minimum].freeze
21
+
19
22
  attr_reader :name, :properties, :abstract_model, :parent, :root
20
23
  attr_accessor :defined, :order, :section
21
24
 
@@ -196,8 +199,14 @@ module RailsAdminNext
196
199
 
197
200
  # Accessor for field's length restrictions per validations
198
201
  #
202
+ # ActiveModel also accepts a bound as a Proc or a Symbol, which it resolves
203
+ # against the record at validation time. Neither can be compared against the
204
+ # column limit here, so both are dropped. Devise's password length validation
205
+ # declares its bounds as Procs.
199
206
  register_instance_option :valid_length do
200
- @valid_length ||= abstract_model.model.validators_on(name).detect { |v| v.kind == :length }.try(&:options) || {}
207
+ @valid_length ||=
208
+ (abstract_model.model.validators_on(name).detect { |v| v.kind == :length }.try(&:options) || {})
209
+ .reject { |option, bound| LENGTH_BOUNDS.include?(option) && (bound.is_a?(Proc) || bound.is_a?(Symbol)) }
201
210
  end
202
211
 
203
212
  register_instance_option :partial do
@@ -215,7 +224,9 @@ module RailsAdminNext
215
224
  :nil
216
225
  end
217
226
 
218
- (@required ||= {})[context] ||= !!([name] + children_fields).uniq.detect do |column_name|
227
+ # An association Rails treats as optional can still be required by a validator on its
228
+ # foreign key, so the reflection answer widens the validator scan instead of replacing it.
229
+ (@required ||= {})[context] ||= (association? && properties.try(:required?)) || !!([name] + children_fields).uniq.detect do |column_name|
219
230
  abstract_model.model.validators_on(column_name).detect do |v|
220
231
  !(v.options[:allow_nil] || v.options[:allow_blank]) &&
221
232
  %i[presence numericality attachment_presence].include?(v.kind) &&
@@ -264,8 +275,6 @@ module RailsAdminNext
264
275
  false
265
276
  end
266
277
 
267
- register_deprecated_instance_option :eager_load?, :eager_load
268
-
269
278
  def eager_load_values
270
279
  case eager_load
271
280
  when true
@@ -63,11 +63,6 @@ module RailsAdminNext
63
63
  :form_datetime
64
64
  end
65
65
 
66
- # Picker-only legacy option; native HTML5 inputs render per the browser locale, so it is ignored.
67
- register_deprecated_instance_option :momentjs_format do
68
- RailsAdminNext.deprecator.warn("The momentjs_format configuration option is deprecated and ignored; date/time fields now use native HTML5 inputs.")
69
- end
70
-
71
66
  # The native <input type="datetime-local" step="1"> control accepts and emits ISO 8601 only.
72
67
  def form_value
73
68
  value&.in_time_zone&.strftime("%Y-%m-%dT%H:%M:%S") || form_default_value
@@ -29,8 +29,6 @@ module RailsAdminNext
29
29
  nil
30
30
  end
31
31
 
32
- register_deprecated_instance_option :delete_key, :delete_value
33
-
34
32
  register_instance_option :pretty_value do
35
33
  if value.presence
36
34
  v = bindings[:view]
@@ -46,17 +46,9 @@ module RailsAdminNext
46
46
  ""
47
47
  end
48
48
 
49
- register_deprecated_instance_option :sidescroll do
50
- RailsAdminNext.deprecator.warn("The sidescroll configuration option was removed, it is always enabled now.")
51
- end
52
-
53
49
  def fields_for_table
54
50
  visible_fields.partition(&:sticky?).flatten
55
51
  end
56
-
57
- register_deprecated_instance_option :sort_reverse do
58
- RailsAdminNext.deprecator.warn("The sort_reverse configuration option is deprecated and has no effect.")
59
- end
60
52
  end
61
53
  end
62
54
  end
@@ -316,14 +316,6 @@ module RailsAdminNext
316
316
  end
317
317
  end
318
318
 
319
- def total_columns_width=(_)
320
- RailsAdminNext.deprecator.warn("The total_columns_width configuration option is deprecated and has no effect.")
321
- end
322
-
323
- def sidescroll=(_)
324
- RailsAdminNext.deprecator.warn("The sidescroll configuration option was removed, it is always enabled now.")
325
- end
326
-
327
319
  # Setup actions to be used.
328
320
  def actions(&block)
329
321
  return unless block
@@ -6,8 +6,8 @@ module RailsAdminNext
6
6
  # the tag's version over these constants at build time, so the in-repo values are only
7
7
  # the floor a local `gem build` produces.
8
8
  MAJOR = 1
9
- MINOR = 0
10
- PATCH = 1
9
+ MINOR = 1
10
+ PATCH = 0
11
11
  PRE = nil
12
12
 
13
13
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_admin_next
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel López Prat
@@ -340,7 +340,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
340
340
  - !ruby/object:Gem::Version
341
341
  version: 1.8.11
342
342
  requirements: []
343
- rubygems_version: 4.0.10
343
+ rubygems_version: 4.0.16
344
344
  specification_version: 4
345
345
  summary: Admin for Rails
346
346
  test_files: []