active_scaffold 4.0.13 → 4.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +19 -0
  3. data/README.md +11 -5
  4. data/app/assets/javascripts/active_scaffold.js.erb +1 -1
  5. data/app/assets/javascripts/jquery/active_scaffold.js +67 -20
  6. data/app/assets/javascripts/jquery/date_picker_bridge.js.erb +1 -1
  7. data/app/assets/javascripts/jquery/draggable_lists.js +1 -1
  8. data/app/assets/javascripts/jquery/tiny_mce_bridge.js +1 -0
  9. data/app/assets/stylesheets/active_scaffold.scss +415 -6
  10. data/app/assets/stylesheets/active_scaffold_layout.css +11 -1
  11. data/app/views/active_scaffold_overrides/_form.html.erb +1 -1
  12. data/app/views/active_scaffold_overrides/_form_association_footer.html.erb +2 -1
  13. data/app/views/active_scaffold_overrides/_form_association_record.html.erb +1 -1
  14. data/app/views/active_scaffold_overrides/_human_filters.html.erb +1 -0
  15. data/app/views/active_scaffold_overrides/_list_header.html.erb +2 -0
  16. data/app/views/active_scaffold_overrides/_list_messages.html.erb +11 -0
  17. data/app/views/active_scaffold_overrides/_render_field.js.erb +1 -1
  18. data/app/views/active_scaffold_overrides/_show_association.html.erb +1 -0
  19. data/app/views/active_scaffold_overrides/_show_columns.html.erb +17 -2
  20. data/app/views/active_scaffold_overrides/_update_field_on_create.js.erb +20 -0
  21. data/app/views/active_scaffold_overrides/add_tab.js.erb +3 -3
  22. data/app/views/active_scaffold_overrides/edit_associated.js.erb +1 -1
  23. data/app/views/active_scaffold_overrides/on_create.js.erb +21 -17
  24. data/app/views/active_scaffold_overrides/on_update.js.erb +1 -1
  25. data/lib/active_scaffold/actions/core.rb +34 -16
  26. data/lib/active_scaffold/actions/field_search.rb +21 -8
  27. data/lib/active_scaffold/actions/list.rb +40 -5
  28. data/lib/active_scaffold/actions/nested.rb +1 -1
  29. data/lib/active_scaffold/actions/subform.rb +2 -1
  30. data/lib/active_scaffold/attribute_params.rb +12 -2
  31. data/lib/active_scaffold/bridges/cancan/cancan_bridge.rb +1 -1
  32. data/lib/active_scaffold/bridges/paper_trail/helper.rb +1 -1
  33. data/lib/active_scaffold/bridges/record_select/helpers.rb +1 -1
  34. data/lib/active_scaffold/config/core.rb +5 -1
  35. data/lib/active_scaffold/config/list.rb +35 -1
  36. data/lib/active_scaffold/constraints.rb +4 -2
  37. data/lib/active_scaffold/data_structures/action_columns.rb +2 -2
  38. data/lib/active_scaffold/data_structures/action_link.rb +16 -11
  39. data/lib/active_scaffold/data_structures/action_links.rb +3 -3
  40. data/lib/active_scaffold/data_structures/actions.rb +2 -2
  41. data/lib/active_scaffold/data_structures/column.rb +21 -0
  42. data/lib/active_scaffold/data_structures/columns.rb +2 -2
  43. data/lib/active_scaffold/data_structures/filter.rb +85 -0
  44. data/lib/active_scaffold/data_structures/filter_option.rb +32 -0
  45. data/lib/active_scaffold/data_structures/filters.rb +51 -0
  46. data/lib/active_scaffold/data_structures/set.rb +2 -2
  47. data/lib/active_scaffold/data_structures/sorting.rb +2 -2
  48. data/lib/active_scaffold/extensions/action_controller_rendering.rb +2 -2
  49. data/lib/active_scaffold/extensions/action_view_rendering.rb +3 -3
  50. data/lib/active_scaffold/finder.rb +16 -6
  51. data/lib/active_scaffold/helpers/action_link_helpers.rb +21 -17
  52. data/lib/active_scaffold/helpers/filter_helpers.rb +36 -0
  53. data/lib/active_scaffold/helpers/form_column_helpers.rb +116 -26
  54. data/lib/active_scaffold/helpers/human_condition_helpers.rb +4 -0
  55. data/lib/active_scaffold/helpers/list_column_helpers.rb +26 -12
  56. data/lib/active_scaffold/helpers/search_column_helpers.rb +4 -2
  57. data/lib/active_scaffold/helpers/show_column_helpers.rb +4 -3
  58. data/lib/active_scaffold/helpers/tabs_helpers.rb +4 -3
  59. data/lib/active_scaffold/helpers/view_helpers.rb +7 -1
  60. data/lib/active_scaffold/registry.rb +1 -1
  61. data/lib/active_scaffold/responds_to_parent.rb +3 -3
  62. data/lib/active_scaffold/tableless.rb +2 -2
  63. data/lib/active_scaffold/version.rb +2 -2
  64. data/lib/active_scaffold.rb +3 -7
  65. metadata +22 -17
  66. data/app/assets/stylesheets/active_scaffold_colors.scss +0 -414
@@ -88,14 +88,13 @@ module ActiveScaffold::Actions
88
88
  @columns += [@column.name] if @column.options[:refresh_link] && @columns.exclude?(@column.name)
89
89
  process_render_field_params
90
90
 
91
- @record =
91
+ @record = find_from_scope(setup_parent, @scope) if main_form_controller && @scope
92
+ @record ||=
92
93
  if @column.send_form_on_update_column
93
94
  updated_record_with_form(@main_columns, params[:record] || params[:search], @scope)
94
95
  else
95
96
  updated_record_with_column(@column, params.delete(:value), @scope)
96
97
  end
97
- # if @scope has more than 2 ] then it's subform inside subform, and assign parent would fail (found associotion may be through association)
98
- setup_parent(@record) if main_form_controller && @scope && @scope.scan(']').size == 2
99
98
  after_render_field(@record, @column)
100
99
  end
101
100
 
@@ -146,21 +145,15 @@ module ActiveScaffold::Actions
146
145
  "#{params[:parent_controller].camelize}Controller"
147
146
  end
148
147
 
149
- def setup_parent(record)
148
+ def setup_parent
150
149
  cfg = main_form_controller.active_scaffold_config
151
- association = cfg.columns[subform_child_association]&.association&.reverse_association
152
- return if association.nil?
153
-
154
150
  parent_model = cfg.model
155
151
  parent = parent_model.new
156
152
  copy_attributes(find_if_allowed(params[:parent_id], :read, parent_model), parent) if params[:parent_id]
157
153
  parent.id = params[:parent_id]
158
154
  apply_constraints_to_record(parent) unless params[:parent_id]
159
- parent = update_record_from_params(parent, cfg.send(params[:parent_id] ? :update : :create).columns, params[:record], true) if @column.send_form_on_update_column
160
- if association.collection?
161
- record.send(association.name) << parent
162
- else
163
- record.send(:"#{association.name}=", parent)
155
+ if @column.send_form_on_update_column
156
+ parent = update_record_from_params(parent, cfg.send(params[:parent_id] ? :update : :create).columns, params[:record], true)
164
157
  end
165
158
 
166
159
  if params[:nested] # form in nested scaffold, set nested parent_record to parent
@@ -172,6 +165,27 @@ module ActiveScaffold::Actions
172
165
  parent
173
166
  end
174
167
 
168
+ def find_from_scope(parent, scope)
169
+ parts = scope[1..-2].split('][')
170
+ record = parent
171
+
172
+ while parts.present?
173
+ part = parts.shift
174
+ return unless record.respond_to?(part)
175
+
176
+ association = record.class.reflect_on_association(part)
177
+ id = parts.shift.to_i if association&.collection?
178
+ record = record.send(part)
179
+ if id
180
+ record = record.find { |child| child.id == id }
181
+ record ||= @new_records&.dig(association.klass, id.to_s)
182
+ end
183
+ return if record.nil?
184
+ end
185
+
186
+ record
187
+ end
188
+
175
189
  def copy_attributes(orig, dst = nil)
176
190
  dst ||= orig.class.new
177
191
  orig.attributes.each { |attr, value| dst.send :write_attribute, attr, value }
@@ -203,8 +217,8 @@ module ActiveScaffold::Actions
203
217
  cookies[params[:_dl_cookie]] = {value: Time.now.to_i, expires: 1.day.since} if params[:_dl_cookie]
204
218
  end
205
219
 
206
- def each_marked_record(&block)
207
- active_scaffold_config.model.as_marked.each(&block)
220
+ def each_marked_record(&)
221
+ active_scaffold_config.model.as_marked.each(&)
208
222
  end
209
223
 
210
224
  def marked_records
@@ -276,6 +290,10 @@ module ActiveScaffold::Actions
276
290
  redirect_to options.is_a?(Hash) ? url_for(options) : options
277
291
  end
278
292
 
293
+ def filtered_query
294
+ beginning_of_chain
295
+ end
296
+
279
297
  # Overide this method on your controller to provide model with named scopes
280
298
  def beginning_of_chain
281
299
  active_scaffold_config.model
@@ -352,7 +370,7 @@ module ActiveScaffold::Actions
352
370
  end
353
371
 
354
372
  def get_row(crud_type_or_security_options = :read)
355
- klass = beginning_of_chain
373
+ klass = filtered_query
356
374
  klass = klass.preload(active_scaffold_preload) unless active_scaffold_config.mongoid?
357
375
  @record = find_if_allowed(params[:id], crud_type_or_security_options, klass)
358
376
  end
@@ -369,7 +387,7 @@ module ActiveScaffold::Actions
369
387
  end
370
388
 
371
389
  def set_vary_accept_header
372
- response.headers['Vary'] = 'Accept'
390
+ response.set_header 'vary', 'Accept'
373
391
  end
374
392
 
375
393
  def check_input_device
@@ -65,16 +65,22 @@ module ActiveScaffold::Actions
65
65
 
66
66
  def grouped_search_finder_options
67
67
  select_query = grouped_search_select
68
+ group_by = calculation_for_group_by(search_group_column&.group_by || [search_group_name], search_group_function)
69
+
68
70
  if search_group_function
69
- group_sql = calculation_for_group_by(search_group_column&.field || search_group_name, search_group_function)
70
- group_by = group_sql.to_sql
71
- select_query += [group_sql.as(search_group_column.name.to_s)]
71
+ select_query += [group_by.as(search_group_name)]
72
+ group_by = group_by.to_sql
72
73
  order = grouped_sorting(group_by)
73
74
  else
74
- group_by = quoted_select_columns(search_group_column&.select_columns || [search_group_name])
75
- select_query += group_by
75
+ if search_group_column&.group_by
76
+ sql_with_names = search_group_column.group_by.map.with_index { |part, i| [part, "#{search_group_name}_#{i}"] }
77
+ select_query += quoted_select_columns(sql_with_names)
78
+ else
79
+ select_query += group_by
80
+ end
76
81
  order = grouped_sorting
77
82
  end
83
+
78
84
  {group: group_by, select: select_query, reorder: order}
79
85
  end
80
86
 
@@ -103,11 +109,13 @@ module ActiveScaffold::Actions
103
109
  end
104
110
 
105
111
  def calculation_for_group_search(column)
106
- sql_function column.calculate.to_s, column.active_record_class.arel_table[column.name]
112
+ sql_function column.calculate.to_s, column.grouped_select
107
113
  end
108
114
 
109
115
  def calculation_for_group_by(group_sql, group_function)
110
- group_sql = Arel::Nodes::SqlLiteral.new(group_sql)
116
+ return quoted_select_columns(group_sql) unless group_function
117
+
118
+ group_sql = Arel.sql(group_sql.join(', '))
111
119
  case group_function
112
120
  when 'year', 'month', 'quarter'
113
121
  extract_sql_fn(group_function, group_sql)
@@ -121,7 +129,7 @@ module ActiveScaffold::Actions
121
129
  end
122
130
 
123
131
  def extract_sql_fn(part, column)
124
- sql_function('extract', sql_operator(Arel::Nodes::SqlLiteral.new(part), 'FROM', column))
132
+ sql_function('extract', sql_operator(Arel.sql(part), 'FROM', column))
125
133
  end
126
134
 
127
135
  def sql_function(function, *args)
@@ -206,11 +214,16 @@ module ActiveScaffold::Actions
206
214
  def setup_joins_for_filtered_columns(filtered_columns)
207
215
  if grouped_search? || active_scaffold_config.list.user.count_includes.present?
208
216
  active_scaffold_outer_joins.concat filtered_columns.map(&:search_joins).flatten.uniq.compact
217
+ active_scaffold_joins << search_group_column.search_joins if grouped_column_needs_joins?(filtered_columns)
209
218
  else
210
219
  set_outer_joins_for_search filtered_columns
211
220
  end
212
221
  end
213
222
 
223
+ def grouped_column_needs_joins?(filtered_columns)
224
+ grouped_search? && search_group_column&.search_joins.present? && filtered_columns.exclude?(search_group_column)
225
+ end
226
+
214
227
  def field_search_formats
215
228
  (default_formats + active_scaffold_config.formats + active_scaffold_config.field_search.formats).uniq
216
229
  end
@@ -215,7 +215,10 @@ module ActiveScaffold::Actions
215
215
  end
216
216
 
217
217
  def quoted_select_columns(columns)
218
- columns&.map { |c| active_scaffold_config.columns[c]&.field || c }
218
+ columns&.map do |col, name|
219
+ sql_column = active_scaffold_config.columns[col]&.field || col
220
+ name ? Arel.sql(sql_column).as(name) : sql_column
221
+ end
219
222
  end
220
223
 
221
224
  def do_refresh_list
@@ -227,12 +230,12 @@ module ActiveScaffold::Actions
227
230
  do_list
228
231
  end
229
232
 
230
- def each_record_in_page(&block)
231
- page_items.each(&block)
233
+ def each_record_in_page(&)
234
+ page_items.each(&)
232
235
  end
233
236
 
234
- def each_record_in_scope(&block)
235
- scoped_query.each(&block)
237
+ def each_record_in_scope(&)
238
+ scoped_query.each(&)
236
239
  end
237
240
 
238
241
  def page_items
@@ -245,6 +248,38 @@ module ActiveScaffold::Actions
245
248
  end
246
249
  end
247
250
 
251
+ def filtered_query
252
+ apply_filters beginning_of_chain
253
+ end
254
+
255
+ def filters_enabled?
256
+ active_scaffold_config.list.filters.present? && params[:id].nil?
257
+ end
258
+
259
+ def apply_filters(query)
260
+ return query unless filters_enabled?
261
+
262
+ active_scaffold_config.list.refresh_with_header = true
263
+
264
+ active_scaffold_config.list.filters.inject(query) do |q, filter|
265
+ next q unless filter.security_method.nil? || send(filter.security_method)
266
+
267
+ default_option = filter[filter.default_option]
268
+ apply_filter q, params[filter.name] ? filter[params[filter.name]] : default_option, default_option
269
+ end
270
+ end
271
+
272
+ def apply_filter(query, filter_option, default_option)
273
+ return query if filter_option.nil? || (filter_option.security_method_set? && !send(filter_option.security_method))
274
+
275
+ @applied_filters ||= []
276
+ @applied_filters << filter_option unless filter_option == default_option
277
+ case filter_option.conditions
278
+ when Proc then instance_exec query, &filter_option.conditions
279
+ else query.where(filter_option.conditions)
280
+ end
281
+ end
282
+
248
283
  def scoped_query
249
284
  @scoped_query ||= begin
250
285
  do_search if respond_to? :do_search, true
@@ -147,7 +147,7 @@ module ActiveScaffold::Actions::Nested
147
147
  security_method: :delete_existing_authorized?,
148
148
  ignore_method: :delete_existing_ignore?)
149
149
  end
150
- config.action_links['destroy'].ignore_method = :habtm_delete_ignore? if config.actions.include?(:delete)
150
+ config.action_links['destroy']&.ignore_method = :habtm_delete_ignore? if config.actions.include?(:delete)
151
151
  end
152
152
 
153
153
  def new_existing
@@ -26,7 +26,8 @@ module ActiveScaffold::Actions
26
26
  @record = (find_associated_record if params[:associated_id]) ||
27
27
  build_associated(@column.association, @parent_record) do |blank_record|
28
28
  if params[:tabbed_by] && params[:value]
29
- assign_tabbed_by(blank_record, @column, params[:tabbed_by], params[:value], params[:value_type])
29
+ @tab_id = params.delete(:value)
30
+ assign_tabbed_by(blank_record, @column, params.delete(:tabbed_by), @tab_id, params.delete(:value_type))
30
31
  end
31
32
  end
32
33
  end
@@ -195,13 +195,23 @@ module ActiveScaffold
195
195
  manage_nested_record_from_params(parent_record, column, value, avoid_changes)
196
196
  elsif column.association&.collection?
197
197
  # HACK: to be able to delete all associated records, hash will include "0" => ""
198
- value.values.compact_blank
199
- .filter_map { |val| manage_nested_record_from_params(parent_record, column, val, avoid_changes) }
198
+ value.compact_blank.filter_map do |id, attributes|
199
+ manage_nested_record_from_params(parent_record, column, attributes, avoid_changes).tap do |record|
200
+ track_new_record(record, id)
201
+ end
202
+ end
200
203
  else
201
204
  value
202
205
  end
203
206
  end
204
207
 
208
+ def track_new_record(record, id)
209
+ return unless record&.new_record?
210
+
211
+ @new_records ||= Hash.new { |h, k| h[k] = {} }
212
+ @new_records[record.class][id] = record
213
+ end
214
+
205
215
  def manage_nested_record_from_params(parent_record, column, attributes, avoid_changes = false)
206
216
  return nil unless build_record_from_params?(attributes, column, parent_record)
207
217
 
@@ -23,7 +23,7 @@ module ActiveScaffold::Bridges
23
23
  # As already has callbacks to ensure authorization at controller method via "authorization_method"
24
24
  # but let's include this too, just in case, no sure how performance is affected tough :TODO benchmark
25
25
  module ClassMethods
26
- def active_scaffold(model_id = nil, &block)
26
+ def active_scaffold(model_id = nil, &)
27
27
  super
28
28
  authorize_resource(
29
29
  class: active_scaffold_config.model,
@@ -3,7 +3,7 @@ module ActiveScaffold::Bridges
3
3
  module Helper
4
4
  def filter_action_links_for_deleted(action_links, record, options); end
5
5
 
6
- def display_action_links(action_links, record, options, &block)
6
+ def display_action_links(action_links, record, options, &)
7
7
  if action_name == 'deleted'
8
8
  action_links = filter_action_links_for_deleted(action_links, record, options)
9
9
  return unless action_links
@@ -15,7 +15,7 @@ class ActiveScaffold::Bridges::RecordSelect
15
15
  if column.association&.singular?
16
16
  multiple = ui_options.dig(:html_options, :multiple)
17
17
  html = active_scaffold_record_select(record, column, options, record.send(column.name), multiple, ui_options: ui_options)
18
- html << active_scaffold_new_record_subform(column, record, options, ui_options: ui_options) if ui_options[:add_new]
18
+ html << active_scaffold_add_new(column, record, options, ui_options: ui_options) if ui_options[:add_new]
19
19
  html
20
20
  elsif column.association&.collection?
21
21
  active_scaffold_record_select(record, column, options, record.send(column.name), true, ui_options: ui_options)
@@ -197,9 +197,13 @@ module ActiveScaffold::Config
197
197
  def _cache_lazy_values
198
198
  action_links.collection # ensure the collection group exist although it's empty
199
199
  action_links.member # ensure the collection group exist although it's empty
200
- action_links.each(&:name_to_cache) if cache_action_link_urls
200
+ if cache_action_link_urls
201
+ action_links.each(&:name_to_cache)
202
+ list.filters.each { |filter| filter.each(&:name_to_cache) } if actions.include?(:list)
203
+ end
201
204
  columns.select(&:sortable?).each(&:sort)
202
205
  columns.select(&:searchable?).each(&:search_sql)
206
+ columns.each(&:field)
203
207
  actions.each do |action_name|
204
208
  action = send(action_name)
205
209
  Array(action.class.columns_collections).each { |method| action.send(method) }
@@ -7,6 +7,7 @@ module ActiveScaffold::Config
7
7
  # inherit from global scope
8
8
  # full configuration path is: defaults => global table => local table
9
9
  @per_page = self.class.per_page
10
+ @filters = ActiveScaffold::DataStructures::Filters.new
10
11
  @page_links_inner_window = self.class.page_links_inner_window
11
12
  @page_links_outer_window = self.class.page_links_outer_window
12
13
 
@@ -20,7 +21,10 @@ module ActiveScaffold::Config
20
21
  @pagination = self.class.pagination
21
22
  @auto_pagination = self.class.auto_pagination
22
23
  @show_search_reset = self.class.show_search_reset
24
+ @show_filter_reset = self.class.show_filter_reset
25
+ @filter_human_message = self.class.filter_human_message
23
26
  @reset_link = self.class.reset_link.clone
27
+ @reset_filter_link = self.class.reset_filter_link.clone
24
28
  @wrap_tag = self.class.wrap_tag
25
29
  @always_show_search = self.class.always_show_search
26
30
  @always_show_create = self.class.always_show_create
@@ -77,10 +81,23 @@ module ActiveScaffold::Config
77
81
  cattr_accessor :show_search_reset, instance_accessor: false
78
82
  @@show_search_reset = true
79
83
 
84
+ # show a link to reset the filter next to filter human message
85
+ cattr_accessor :show_filter_reset, instance_accessor: false
86
+ @@show_filter_reset = true
87
+
88
+ # filter human message
89
+ # you may show the user a humanized applied filters, not the default ones
90
+ cattr_accessor :filter_human_message, instance_accessor: false
91
+ @@filter_human_message = false
92
+
80
93
  # the ActionLink to reset search
81
94
  cattr_reader :reset_link, instance_reader: false
82
95
  @@reset_link = ActiveScaffold::DataStructures::ActionLink.new('index', label: :click_to_reset, type: :collection, position: false, parameters: {search: ''})
83
96
 
97
+ # the ActionLink to reset the filters
98
+ cattr_reader :reset_filter_link, instance_reader: false
99
+ @@reset_filter_link = ActiveScaffold::DataStructures::ActionLink.new('index', label: :click_to_reset, type: :collection, position: false, dynamic_parameters: -> { clear_filters_params })
100
+
84
101
  # wrap normal cells (not inplace editable columns or with link) with a tag
85
102
  # it allows for more css styling
86
103
  cattr_accessor :wrap_tag, instance_accessor: false
@@ -103,6 +120,10 @@ module ActiveScaffold::Config
103
120
  cattr_accessor :calculate_etag, instance_accessor: false
104
121
  @@calculate_etag = false
105
122
 
123
+ def self.filters
124
+ ActiveScaffold::DataStructures::Filters
125
+ end
126
+
106
127
  # instance-level configuration
107
128
  # ----------------------------
108
129
 
@@ -143,9 +164,22 @@ module ActiveScaffold::Config
143
164
  # show a link to reset the search next to filtered message
144
165
  attr_accessor :show_search_reset
145
166
 
167
+ # show a link to reset the filter next to filter human message
168
+ attr_accessor :show_filter_reset
169
+
170
+ # filter human message
171
+ # you may show the user a humanized applied filters, not the default ones
172
+ attr_accessor :filter_human_message
173
+
146
174
  # the ActionLink to reset search
147
175
  attr_reader :reset_link
148
176
 
177
+ # the ActionLink to reset the filters
178
+ attr_reader :reset_filter_link
179
+
180
+ # the filters for this controller
181
+ attr_reader :filters
182
+
149
183
  # the default sorting.
150
184
  # should be a hash of {column_name => direction}, e.g. {a: 'desc', b: 'asc'}.
151
185
  # for backwards compatibility, it may be an array of hashes of {column_name => direction}, e.g. [{a: 'desc'}, {b: 'asc'}].
@@ -229,7 +263,7 @@ module ActiveScaffold::Config
229
263
  user_attr :page_links_inner_window, :page_links_outer_window, :refresh_with_header, :empty_field_text,
230
264
  :association_join_text, :messages_above_header, :wrap_tag, :auto_select_columns, :calculate_etag,
231
265
  :no_entries_message, :filtered_message, :show_search_reset, :always_show_create, :always_show_search,
232
- :hide_nested_column, :pagination, :auto_pagination
266
+ :hide_nested_column, :pagination, :auto_pagination, :filter_human_message, :show_filter_reset
233
267
 
234
268
  def initialize(conf, storage, params)
235
269
  super(conf, storage, params, :list)
@@ -87,7 +87,9 @@ module ActiveScaffold
87
87
  # regular column constraints
88
88
  elsif column.searchable? && params[column.name] != v
89
89
  active_scaffold_references.concat column.references if column.includes.present?
90
- conditions << [column.search_sql.collect { |search_sql| "#{search_sql} = ?" }.join(' OR '), *([v] * column.search_sql.size)]
90
+ condition = column.search_sql.collect { |search_sql| "#{search_sql} = ?" }.join(' OR ')
91
+ condition = ([condition] * v.size).join(' OR ') if Array(v).many?
92
+ conditions << [condition, *(Array(v) * column.search_sql.size)]
91
93
  end
92
94
  # unknown-to-activescaffold-but-real-database-column constraint
93
95
  elsif active_scaffold_config._columns_hash[k.to_s] && params[column.name] != v
@@ -101,7 +103,7 @@ module ActiveScaffold
101
103
 
102
104
  def join_from_association_constraint(column)
103
105
  if column.association.habtm?
104
- active_scaffold_habtm_joins.concat column.includes
106
+ active_scaffold_joins.concat column.includes
105
107
  elsif !column.association.polymorphic?
106
108
  if column.association.belongs_to?
107
109
  active_scaffold_preload.concat column.includes
@@ -40,11 +40,11 @@ module ActiveScaffold::DataStructures
40
40
  attr_accessor :collapsed
41
41
 
42
42
  # nests a subgroup in the column set
43
- def add_subgroup(label, &proc)
43
+ def add_subgroup(label, &)
44
44
  columns = ActiveScaffold::DataStructures::ActionColumns.new
45
45
  columns.label = label
46
46
  columns.action = action
47
- columns.configure(&proc)
47
+ columns.configure(&)
48
48
  exclude columns.collect_columns
49
49
  add columns
50
50
  end
@@ -95,10 +95,7 @@ module ActiveScaffold::DataStructures
95
95
  attr_accessor :image
96
96
 
97
97
  # if the action requires confirmation
98
- def confirm=(value)
99
- @dhtml_confirm = nil if value
100
- @confirm = value
101
- end
98
+ attr_writer :confirm
102
99
 
103
100
  def confirm(label = '')
104
101
  return @confirm if !confirm? || @confirm.is_a?(String)
@@ -110,16 +107,24 @@ module ActiveScaffold::DataStructures
110
107
  @confirm.present?
111
108
  end
112
109
 
113
- # if the action uses a DHTML based (i.e. 2-phase) confirmation
114
- attr_reader :dhtml_confirm
110
+ # if the action requires prompting a value, only for inline links
111
+ attr_writer :prompt
112
+
113
+ def prompt(label = '')
114
+ return @prompt if !prompt? || @prompt.is_a?(String)
115
115
 
116
- def dhtml_confirm=(value)
117
- @confirm = nil if value
118
- @dhtml_confirm = value
116
+ ActiveScaffold::Registry.cache(:translations, @prompt) { as_(@prompt) } % {label: label}
119
117
  end
120
118
 
121
- def dhtml_confirm?
122
- @dhtml_confirm.present?
119
+ def prompt?
120
+ @prompt.present?
121
+ end
122
+
123
+ # if the prompt is required, empty value or cancel will prevent running the action
124
+ attr_writer :prompt_required
125
+
126
+ def prompt_required?
127
+ @prompt_required
123
128
  end
124
129
 
125
130
  # what method to call on the controller to see if this action_link should be visible
@@ -160,9 +160,9 @@ module ActiveScaffold::DataStructures
160
160
  end
161
161
  end
162
162
 
163
- def method_missing(name, *args, &block)
163
+ def method_missing(name, *args, &)
164
164
  return super if name.match?(/[=!?]$/)
165
- return subgroup(name.to_sym, args.first, &block) if frozen?
165
+ return subgroup(name.to_sym, args.first, &) if frozen?
166
166
 
167
167
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
168
168
  def #{name}(label = nil) # def group_name(label = nil)
@@ -171,7 +171,7 @@ module ActiveScaffold::DataStructures
171
171
  @#{name} # @group_name
172
172
  end # end
173
173
  METHOD
174
- send(name, args.first, &block)
174
+ send(name, args.first, &)
175
175
  end
176
176
 
177
177
  def respond_to_missing?(name, *)
@@ -16,8 +16,8 @@ class ActiveScaffold::DataStructures::Actions
16
16
  end
17
17
  alias << add
18
18
 
19
- def each(&block)
20
- @set.each(&block)
19
+ def each(&)
20
+ @set.each(&)
21
21
  end
22
22
 
23
23
  def include?(val)
@@ -44,6 +44,9 @@ module ActiveScaffold::DataStructures
44
44
  # send all the form instead of only new value when this column changes
45
45
  attr_accessor :send_form_on_update_column
46
46
 
47
+ # disable the form while the request to refresh other columns is sent
48
+ attr_accessor :disable_on_update_column
49
+
47
50
  # add a custom attr_accessor that can contain a Proc (or boolean or symbol)
48
51
  # that will be called when the column renders, such that we can dynamically
49
52
  # hide or show the column with an element that can be replaced by
@@ -57,6 +60,9 @@ module ActiveScaffold::DataStructures
57
60
  # config.columns[:my_column].hide_form_column_if = :hide_tractor_fields?
58
61
  attr_accessor :hide_form_column_if
59
62
 
63
+ # text to display when the column is empty, defaults nil, so list.empty_field_text is used
64
+ attr_accessor :empty_field_text
65
+
60
66
  # a collection of columns to load from the association when eager loading is disabled, if it's nil all columns will be loaded
61
67
  attr_accessor :select_associated_columns
62
68
 
@@ -480,6 +486,7 @@ module ActiveScaffold::DataStructures
480
486
  if @column.nil? && active_record? && active_record_class._default_attributes.key?(name.to_s)
481
487
  @column = active_record_class._default_attributes[name.to_s]
482
488
  end
489
+ @disable_on_update_column = true
483
490
  @db_default_value = ActiveScaffold::OrmChecks.default_value active_record_class, name if @column
484
491
  @delegated_association = delegated_association
485
492
  @cache_key = [@active_record_class.name, name].compact.map(&:to_s).join('#')
@@ -551,6 +558,20 @@ module ActiveScaffold::DataStructures
551
558
  @field ||= quoted_field(field_name)
552
559
  end
553
560
 
561
+ def group_by=(value)
562
+ @group_by = value ? Array(value) : nil
563
+ end
564
+
565
+ def group_by
566
+ @group_by || select_columns || [field]
567
+ end
568
+
569
+ attr_writer :grouped_select
570
+
571
+ def grouped_select
572
+ Arel.sql(@grouped_select&.to_s || field)
573
+ end
574
+
554
575
  def quoted_foreign_type
555
576
  quoted_field(quoted_field_name(association.foreign_type))
556
577
  end
@@ -78,8 +78,8 @@ module ActiveScaffold::DataStructures
78
78
  end
79
79
  alias [] find_by_name
80
80
 
81
- def each(&block)
82
- @set.each_value(&block)
81
+ def each(&)
82
+ @set.each_value(&)
83
83
  end
84
84
 
85
85
  def _inheritable