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
@@ -0,0 +1,85 @@
1
+ module ActiveScaffold::DataStructures
2
+ class Filter
3
+ include Enumerable
4
+
5
+ attr_reader :name, :default_option
6
+ attr_writer :label, :description
7
+ attr_accessor :type, :weight, :css_class, :security_method
8
+
9
+ def initialize(name, type)
10
+ raise ArgumentError, 'Filter name must use only word characters (a-zA-Z0-9_)' unless name.match?(/\A\w+\z/)
11
+
12
+ @label = @name = name.to_sym
13
+ @type = type
14
+ @options = []
15
+ @weight = 0
16
+ end
17
+
18
+ # adds a FilterOption, creating one from the arguments if need be
19
+ def add(name, options = {})
20
+ if name.is_a?(ActiveScaffold::DataStructures::FilterOption)
21
+ option = name
22
+ name = option.name
23
+ end
24
+ existing = self[name]
25
+ raise ArgumentError, "there is a filter option with '#{name}' name" if existing
26
+
27
+ option ||= ActiveScaffold::DataStructures::FilterOption.new(@name, name, options)
28
+ @default_option ||= option.name
29
+ @options << option
30
+ self
31
+ end
32
+ alias << add
33
+
34
+ def default_option=(name)
35
+ option = self[name]
36
+ raise ArgumentError, "'#{name}' option not found" unless option
37
+
38
+ @default_option = option.name
39
+ end
40
+
41
+ # finds a FilterOption by matching the name
42
+ def [](option_name)
43
+ @options.find { |option| option.name.to_s == option_name.to_s }
44
+ end
45
+
46
+ def delete(option_name)
47
+ @options.delete self[option_name]
48
+ end
49
+
50
+ # iterates over the links, possibly by type
51
+ def each(&)
52
+ @options.each(&)
53
+ end
54
+
55
+ def empty?
56
+ @options.empty?
57
+ end
58
+
59
+ def label(*)
60
+ case @label
61
+ when Symbol
62
+ ActiveScaffold::Registry.cache(:translations, @label) { as_(@label) }
63
+ else
64
+ @label
65
+ end
66
+ end
67
+
68
+ def description
69
+ case @description
70
+ when Symbol
71
+ ActiveScaffold::Registry.cache(:translations, @description) { as_(@description) }
72
+ else
73
+ @description
74
+ end
75
+ end
76
+
77
+ protected
78
+
79
+ # called during clone or dup. makes the clone/dup deeper.
80
+ def initialize_copy(from)
81
+ @options = []
82
+ from.each { |option| @options << option.clone }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveScaffold::DataStructures
2
+ class FilterOption < ActionLink
3
+ attr_reader :name, :filter_name
4
+ attr_writer :description
5
+ attr_accessor :conditions
6
+
7
+ def initialize(filter_name, name, options = {})
8
+ @filter_name = filter_name
9
+ @label = @name = name.to_sym
10
+ super(
11
+ :index,
12
+ options.merge(
13
+ action: :index,
14
+ type: :collection,
15
+ method: :get,
16
+ position: false,
17
+ toggle: true
18
+ )
19
+ )
20
+ parameters.merge!(filter_name => name)
21
+ end
22
+
23
+ def description
24
+ case @description
25
+ when Symbol
26
+ ActiveScaffold::Registry.cache(:translations, @description) { as_(@description) }
27
+ else
28
+ @description
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ module ActiveScaffold::DataStructures
2
+ class Filters
3
+ include Enumerable
4
+
5
+ def initialize
6
+ @set = []
7
+ @default_type = self.class.default_type
8
+ end
9
+
10
+ # adds a FilterOption, creating one from the arguments if need be
11
+ def add(name, &block)
12
+ if name.is_a?(ActiveScaffold::DataStructures::Filter)
13
+ filter = name
14
+ name = filter.name
15
+ end
16
+ existing = self[name]
17
+ raise ArgumentError, "there is a filter with '#{name}' name" if existing
18
+
19
+ filter ||= ActiveScaffold::DataStructures::Filter.new(name, default_type)
20
+ @set << filter
21
+ block&.call filter
22
+ self
23
+ end
24
+ alias << add
25
+
26
+ # finds a Filter by matching the name
27
+ def [](name)
28
+ @set.find { |filter| filter.name.to_s == name.to_s }
29
+ end
30
+
31
+ def delete(name)
32
+ @set.delete self[name]
33
+ end
34
+
35
+ # iterates over the links, possibly by type
36
+ def each(&)
37
+ @set.each(&)
38
+ end
39
+
40
+ def empty?
41
+ @set.empty?
42
+ end
43
+
44
+ # default filter type for all app filters, can be :links or :select
45
+ cattr_accessor :default_type
46
+ @@default_type = :links
47
+
48
+ # default filter type for all filters in this set, can be :links or :select
49
+ attr_accessor :default_type
50
+ end
51
+ end
@@ -47,8 +47,8 @@ module ActiveScaffold::DataStructures
47
47
  end
48
48
  alias [] find_by_name
49
49
 
50
- def each(&block)
51
- @set.each(&block)
50
+ def each(&)
51
+ @set.each(&)
52
52
  end
53
53
 
54
54
  # returns the number of items in the set
@@ -106,8 +106,8 @@ module ActiveScaffold::DataStructures
106
106
  end
107
107
 
108
108
  # iterate over the clauses
109
- def each(&block)
110
- @clauses.each(&block)
109
+ def each(&)
110
+ @clauses.each(&)
111
111
  end
112
112
 
113
113
  def each_column
@@ -1,14 +1,14 @@
1
1
  # wrap the action rendering for ActiveScaffold controllers
2
2
  module ActiveScaffold
3
3
  module ActionController # :nodoc:
4
- def render(*args, &block)
4
+ def render(*args, &)
5
5
  if self.class.uses_active_scaffold? && params[:adapter] && @rendering_adapter.nil? && request.xhr?
6
6
  @rendering_adapter = true # recursion control
7
7
  # if we need an adapter, then we render the actual stuff to a string and insert it into the adapter template
8
8
  opts = args.any? ? args.first : {}
9
9
 
10
10
  render partial: params[:adapter][1..],
11
- locals: {payload: render_to_string(opts.merge(layout: false), &block).html_safe}, # rubocop:disable Rails/OutputSafety
11
+ locals: {payload: render_to_string(opts.merge(layout: false), &).html_safe}, # rubocop:disable Rails/OutputSafety
12
12
  use_full_path: true, layout: false, content_type: :html
13
13
  @rendering_adapter = nil # recursion control
14
14
  else
@@ -35,7 +35,7 @@ module ActiveScaffold # :nodoc:
35
35
  #
36
36
  # options[:xhr] force to load embedded scaffold with AJAX even when render_component gem is installed.
37
37
  #
38
- def render(*args, &block)
38
+ def render(*args, &)
39
39
  if args.first.is_a?(Hash) && args.first[:active_scaffold]
40
40
  render_embedded args.first
41
41
  elsif args.first == :super
@@ -103,9 +103,9 @@ module ActiveScaffold # :nodoc:
103
103
  def update_view_paths
104
104
  last_view_path =
105
105
  if @lookup_context # rails 6
106
- File.expand_path(File.dirname(File.dirname(@lookup_context.last_template.short_identifier.to_s)), Rails.root)
106
+ File.expand_path(File.dirname(@lookup_context.last_template.short_identifier.to_s, 2), Rails.root)
107
107
  else
108
- File.expand_path(File.dirname(File.dirname(lookup_context.last_template.inspect)), Rails.root)
108
+ File.expand_path(File.dirname(lookup_context.last_template.inspect, 2), Rails.root)
109
109
  end
110
110
  new_view_paths = view_paths.drop(view_paths.find_index { |path| path.to_s == last_view_path } + 1)
111
111
  if @lookup_context # rails 6
@@ -502,7 +502,7 @@ module ActiveScaffold
502
502
 
503
503
  protected
504
504
 
505
- attr_writer :active_scaffold_conditions, :active_scaffold_preload, :active_scaffold_habtm_joins, :active_scaffold_outer_joins, :active_scaffold_references
505
+ attr_writer :active_scaffold_conditions, :active_scaffold_preload, :active_scaffold_joins, :active_scaffold_outer_joins, :active_scaffold_references
506
506
 
507
507
  def active_scaffold_conditions
508
508
  @active_scaffold_conditions ||= []
@@ -512,8 +512,18 @@ module ActiveScaffold
512
512
  @active_scaffold_preload ||= []
513
513
  end
514
514
 
515
+ def active_scaffold_joins
516
+ @active_scaffold_joins ||= []
517
+ end
518
+
515
519
  def active_scaffold_habtm_joins
516
- @active_scaffold_habtm_joins ||= []
520
+ ActiveScaffold.deprecator.warn 'use active_scaffold_joins'
521
+ active_scaffold_joins
522
+ end
523
+
524
+ def active_scaffold_habtm_joins=(value)
525
+ ActiveScaffold.deprecator.warn 'use active_scaffold_joins='
526
+ self.active_scaffold_joins = value
517
527
  end
518
528
 
519
529
  def active_scaffold_outer_joins
@@ -559,7 +569,7 @@ module ActiveScaffold
559
569
  # returns a single record (the given id) but only if it's allowed for the specified security options.
560
570
  # security options can be a hash for authorized_for? method or a value to check as a :crud_type
561
571
  # accomplishes this by checking model.#{action}_authorized?
562
- def find_if_allowed(id, security_options, klass = beginning_of_chain)
572
+ def find_if_allowed(id, security_options, klass = filtered_query)
563
573
  record = klass.find(id)
564
574
  security_options = {crud_type: security_options.to_sym} unless security_options.is_a? Hash
565
575
  raise ActiveScaffold::RecordNotAllowed, "#{klass} with id = #{id}" unless record.authorized_for? security_options
@@ -623,7 +633,7 @@ module ActiveScaffold
623
633
  options[:page] ||= 1
624
634
 
625
635
  find_options = finder_options(options)
626
- query = beginning_of_chain
636
+ query = filtered_query
627
637
  query = query.where(nil) if active_scaffold_config.active_record? # where(nil) is needed because we need a relation
628
638
 
629
639
  # NOTE: we must use :include in the count query, because some conditions may reference other tables
@@ -663,7 +673,7 @@ module ActiveScaffold
663
673
  left_joins = active_scaffold_outer_joins
664
674
  left_joins += includes if includes
665
675
  primary_key = active_scaffold_config.primary_key
666
- subquery = append_to_query(beginning_of_chain, conditions: conditions, joins: joins_for_finder, left_joins: left_joins, select: active_scaffold_config.columns[primary_key].field)
676
+ subquery = append_to_query(filtered_query, conditions: conditions, joins: joins_for_finder, left_joins: left_joins, select: active_scaffold_config.columns[primary_key].field)
667
677
  subquery.unscope(:order)
668
678
  end
669
679
 
@@ -690,7 +700,7 @@ module ActiveScaffold
690
700
  joins_for_collection
691
701
  else
692
702
  []
693
- end + active_scaffold_habtm_joins
703
+ end + active_scaffold_joins
694
704
  end
695
705
 
696
706
  def apply_conditions(relation, *conditions)
@@ -1,7 +1,6 @@
1
1
  module ActiveScaffold
2
2
  module Helpers
3
- # All extra helpers that should be included in the View.
4
- # Also a dumping ground for uncategorized helpers.
3
+ # Helpers rendering action links
5
4
  module ActionLinkHelpers
6
5
  # params which mustn't be copying to nested links
7
6
  NESTED_PARAMS = %i[eid embedded association parent_scaffold].freeze
@@ -62,9 +61,9 @@ module ActiveScaffold
62
61
  output
63
62
  end
64
63
 
65
- def display_action_link_group(link, record, options, &block)
64
+ def display_action_link_group(link, record, options, &)
66
65
  options[:level] += 1
67
- content = display_action_links(link, record, options, &block)
66
+ content = display_action_links(link, record, options, &)
68
67
  options[:level] -= 1
69
68
  display_action_link(link, content, record, options).tap { options[:first_action] = false } if content.present?
70
69
  end
@@ -85,7 +84,7 @@ module ActiveScaffold
85
84
  group_tag = :li
86
85
  end
87
86
  content = content_tag(group_tag, class: html_classes.presence, onclick: ('' if hover_via_click?)) do
88
- content_tag(:div, as_(link.label(record)), class: link.css_class) << content_tag(:ul, content)
87
+ content_tag(:div, link.label(record), class: link.css_class, title: options[:title]) << content_tag(:ul, content)
89
88
  end
90
89
  else
91
90
  content = render_action_link(link, record, options)
@@ -341,14 +340,23 @@ module ActiveScaffold
341
340
  end
342
341
 
343
342
  def action_link_selected?(link, record)
344
- missing_options, url_options = replaced_action_link_url_options(link, record)
345
- safe_params = params.to_unsafe_h
346
- (url_options - safe_params.to_a).blank? && missing_options.all? { |k, _| params[k].nil? }
343
+ if link.respond_to?(:filter_name)
344
+ if params[link.filter_name]
345
+ params[link.filter_name].to_s == link.name.to_s
346
+ else
347
+ active_scaffold_config.list.filters[link.filter_name].default_option == link.name
348
+ end
349
+ else
350
+ missing_options, url_options = replaced_action_link_url_options(link, record)
351
+ safe_params = params.to_unsafe_h
352
+ (url_options - safe_params.to_a).blank? && missing_options.all? { |k, _| params[k].nil? }
353
+ end
347
354
  end
348
355
 
349
356
  def action_link_html_options(link, record, options)
350
357
  link_id = get_action_link_id(link, record)
351
- html_options = link.html_options.merge(class: [link.html_options[:class], link.action.to_s].compact.join(' '))
358
+ html_options = options[:html_options] || link.html_options
359
+ html_options = html_options.merge(class: [html_options[:class], link.action.to_s].compact.join(' '))
352
360
  html_options[:link] = action_link_text(link, record, options)
353
361
 
354
362
  # Needs to be in html_options to as the adding _method to the url is no longer supported by Rails
@@ -363,6 +371,10 @@ module ActiveScaffold
363
371
  html_options[:data][:action] = link.action
364
372
  html_options[:data][:cancel_refresh] = true if link.refresh_on_close
365
373
  html_options[:data][:keep_open] = true if link.keep_open?
374
+ if link.prompt?
375
+ html_options[:data][:prompt] = link.prompt(h(record&.to_label))
376
+ html_options[:data][:prompt_required] = true if link.prompt_required?
377
+ end
366
378
  html_options[:remote] = true
367
379
  end
368
380
 
@@ -376,14 +388,6 @@ module ActiveScaffold
376
388
  html_options[:rel] = [html_options[:rel], 'noopener noreferrer'].compact.join(' ')
377
389
  end
378
390
  html_options[:id] = link_id
379
- if link.dhtml_confirm?
380
- unless link.inline?
381
- html_options[:class] << ' as_action'
382
- html_options[:page_link] = 'true'
383
- end
384
- html_options[:dhtml_confirm] = link.dhtml_confirm.value
385
- html_options[:onclick] = link.dhtml_confirm.onclick_function(controller, link_id)
386
- end
387
391
  html_options
388
392
  end
389
393
 
@@ -0,0 +1,36 @@
1
+ module ActiveScaffold
2
+ module Helpers
3
+ # Helpers rendering filters
4
+ module FilterHelpers
5
+ def clear_filters_params
6
+ active_scaffold_config.list.filters.each_with_object({}) do |filter, url_options|
7
+ url_options[filter.name] = nil
8
+ end
9
+ end
10
+
11
+ def display_filters(filters)
12
+ content = filters.sort_by(&:weight).map { |filter| display_filter(filter) }
13
+ content_tag :div, safe_join(content), class: 'filters' if content.present?
14
+ end
15
+
16
+ def display_filter(filter)
17
+ return if filter.security_method && !controller.send(filter.security_method)
18
+
19
+ options = filter.reject { |option| option.security_method_set? && !controller.send(option.security_method) }
20
+ send :"display_filter_as_#{filter.type}", filter, options if options.present?
21
+ end
22
+
23
+ def display_filter_as_links(filter, options)
24
+ content = options.map { |option| display_action_link(option, nil, nil, authorized: true, level: 1) }
25
+ display_action_link(filter, safe_join(content), nil, level: 0, title: filter.description) if content.present?
26
+ end
27
+
28
+ def display_filter_as_select(filter, options)
29
+ content = options.map do |option|
30
+ content_tag :option, option.label(nil), data: {url: action_link_url(option, nil)}, selected: action_link_selected?(option, nil), title: option.description
31
+ end
32
+ select_tag nil, safe_join(content), class: "action_group #{link.css_class}", title: filter.description || filter.label, data: {remote: :url} if content.present?
33
+ end
34
+ end
35
+ end
36
+ end
@@ -129,7 +129,7 @@ module ActiveScaffold
129
129
  form_columns ||= @main_columns.visible_columns_names
130
130
  end
131
131
  form_columns ||= current_form_columns(record, scope, subform_controller)
132
- if force || (form_columns && column.update_columns && (column.update_columns & form_columns).present?)
132
+ if force || (form_columns && column.update_columns&.intersect?(form_columns))
133
133
  url_params.reverse_merge! params_for(action: 'render_field', column: column.name, id: record.to_param)
134
134
  if nested? && scope
135
135
  url_params[:nested] = url_params.slice(:parent_scaffold, :association, nested.param_name)
@@ -146,6 +146,7 @@ module ActiveScaffold
146
146
  options['data-update_url'] = url_for(url_params)
147
147
  options['data-update_send_form'] = column.send_form_on_update_column
148
148
  options['data-update_send_form_selector'] = column.options[:send_form_selector] if column.options[:send_form_selector]
149
+ options['data-skip-disable-form'] = !column.disable_on_update_column
149
150
  end
150
151
  options
151
152
  end
@@ -332,8 +333,9 @@ module ActiveScaffold
332
333
  record = html_options.delete(:object)
333
334
  associated = html_options.include?(:associated) ? html_options.delete(:associated) : record.send(column.association.name)
334
335
 
335
- select_options = sorted_association_options_find(column.association, nil, record)
336
- select_options.unshift(associated) unless associated.nil? || select_options.include?(associated)
336
+ helper_method = association_helper_method(column.association, :sorted_association_options_find)
337
+ select_options = send(helper_method, column.association, nil, record)
338
+ select_options.unshift(associated) if associated&.persisted? && select_options.exclude?(associated)
337
339
 
338
340
  method = column.name
339
341
  options.merge! selected: associated&.id, include_blank: as_(:_select_), object: record
@@ -350,21 +352,80 @@ module ActiveScaffold
350
352
  collection_select(:record, method, select_options, :id, ui_options[:label_method] || :to_label, options, html_options)
351
353
  end
352
354
  html << active_scaffold_refresh_link(column, html_options, record, ui_options) if ui_options[:refresh_link]
353
- html << active_scaffold_new_record_subform(column, record, html_options, ui_options: ui_options) if ui_options[:add_new]
355
+ html << active_scaffold_add_new(column, record, html_options, ui_options: ui_options) if ui_options[:add_new]
354
356
  html
355
357
  end
356
358
 
357
- def active_scaffold_new_record_subform(column, record, html_options, ui_options: column.options, new_record_attributes: nil, locals: {}, skip_link: false)
358
- klass =
359
- if column.association.polymorphic? && column.association.belongs_to?
360
- type = record.send(column.association.foreign_type)
361
- column.association.klass(record) if type.present? && (ui_options[:add_new] == true || type.in?(ui_options[:add_new]))
362
- else
363
- column.association.klass
364
- end
359
+ def active_scaffold_new_record_klass(column, record, **options)
360
+ if column.association.polymorphic? && column.association.belongs_to?
361
+ type = record.send(column.association.foreign_type)
362
+ type_options = options[:types]
363
+ column.association.klass(record) if type.present? && (type_options.nil? || type_options.include?(type))
364
+ else
365
+ column.association.klass
366
+ end
367
+ end
368
+
369
+ def active_scaffold_add_new(column, record, html_options, ui_options: column.options, skip_link: false)
370
+ options = ui_options[:add_new] == true ? {} : ui_options[:add_new]
371
+ if options.is_a?(Array)
372
+ ActiveScaffold.deprecator.warn "use add_new: {types: #{options.inspect}} instead of add_new: #{options.inspect}"
373
+ options = {types: options}
374
+ end
375
+ if ui_options[:hide_subgroups] && !options.key?(:hide_subgroups)
376
+ ActiveScaffold.deprecator.warn "use add_new: {hide_subgroups: #{ui_options[:hide_subgroups]}} instead of hide_subgroups: #{ui_options[:hide_subgroups]}"
377
+ options[:hide_subgroups] = ui_options[:hide_subgroups]
378
+ end
379
+ if ui_options[:layout] && !options.key?(:layout)
380
+ ActiveScaffold.deprecator.warn "use add_new: {layout: #{ui_options[:layout]}} instead of layout: #{ui_options[:layout]}"
381
+ options[:layout] = ui_options[:layout]
382
+ end
383
+
384
+ case options[:mode]
385
+ when nil, :subform
386
+ active_scaffold_new_record_subform(column, record, html_options, options: options, skip_link: skip_link)
387
+ when :popup
388
+ active_scaffold_new_record_popup(column, record, html_options, options: options) unless skip_link
389
+ else
390
+ raise ArgumentError, "unsupported mode for add_new: #{options[:mode].inspect}"
391
+ end
392
+ end
393
+
394
+ def active_scaffold_new_record_url_options(column, record)
395
+ if column.association.reverse
396
+ constraint = [record.id]
397
+ constraint.unshift record.class.name if column.association.reverse_association.polymorphic?
398
+ {embedded: {constraints: {column.association.reverse => constraint}}}
399
+ else
400
+ raise "can't add constraint to create new record with :popup, no reverse association for " \
401
+ "\"#{column.name}\" in #{column.association.klass}, add the reverse association " \
402
+ 'or override active_scaffold_new_record_url_options helper.'
403
+ end
404
+ end
405
+
406
+ def active_scaffold_new_record_popup(column, record, html_options, options: {})
407
+ klass = send(override_helper_per_model(:active_scaffold_new_record_klass, record.class), column, record, **options)
408
+ klass = nil if options[:security_method] && !controller.send(options[:security_method])
409
+ klass = nil if klass && options[:security_method].nil? && !klass.authorized_for?(crud_type: :create)
410
+ return h('') unless klass
411
+
412
+ link_text = active_scaffold_add_new_text(options, :add_new_text, :add)
413
+ url_options_helper = override_helper_per_model(:active_scaffold_new_record_url_options, record.class)
414
+ url_options = send(url_options_helper, column, record)
415
+ url_options[:controller] ||= active_scaffold_controller_for(klass).controller_path
416
+ url_options[:action] ||= :new
417
+ url_options[:from_field] ||= html_options[:id]
418
+ url_options[:parent_model] ||= record.class.name
419
+ url_options[:parent_column] ||= column.name
420
+ url_options.reverse_merge! options[:url_options] if options[:url_options]
421
+ link_to(link_text, url_options, remote: true, data: {position: :popup}, class: 'as_action')
422
+ end
423
+
424
+ def active_scaffold_new_record_subform(column, record, html_options, options: {}, new_record_attributes: nil, locals: {}, skip_link: false)
425
+ klass = send(override_helper_per_model(:active_scaffold_new_record_klass, record.class), column, record, **options)
365
426
  return content_tag(:div, '') unless klass
366
427
 
367
- subform_attrs = active_scaffold_subform_attributes(column, nil, klass, ui_options: ui_options)
428
+ subform_attrs = active_scaffold_subform_attributes(column, nil, klass, ui_options: options)
368
429
  if record.send(column.name)&.new_record?
369
430
  new_record = record.send(column.name)
370
431
  else
@@ -374,19 +435,28 @@ module ActiveScaffold
374
435
  scope = html_options[:name].scan(/record(.*)\[#{column.name}\]/).dig(0, 0)
375
436
  new_record ||= klass.new(new_record_attributes)
376
437
  locals = locals.reverse_merge(column: column, parent_record: record, associated: [], show_blank_record: new_record, scope: scope)
377
- subform = render(partial: subform_partial_for_column(column, klass, ui_options: ui_options), locals: locals)
378
- if ui_options[:hide_subgroups]
438
+ subform = render(partial: subform_partial_for_column(column, klass, ui_options: options), locals: locals)
439
+ if options[:hide_subgroups]
379
440
  toggable_id = "#{sub_form_id(association: column.name, id: record.id || generated_id(record) || 99_999_999_999)}-div"
380
441
  subform << link_to_visibility_toggle(toggable_id, default_visible: false)
381
442
  end
382
443
  html = content_tag(:div, subform, subform_attrs)
383
444
  return html if skip_link
384
445
 
385
- html << active_scaffold_show_new_subform_link(column, record, html_options[:id], subform_attrs[:id])
446
+ html << active_scaffold_show_new_subform_link(column, record, html_options[:id], subform_attrs[:id], options: options)
447
+ end
448
+
449
+ def active_scaffold_add_new_text(options, key, default)
450
+ text = options[key] unless options == true
451
+ return text if text.is_a? String
452
+
453
+ as_(text || default)
386
454
  end
387
455
 
388
- def active_scaffold_show_new_subform_link(column, record, select_id, subform_id)
389
- data = {select_id: select_id, subform_id: subform_id, subform_text: as_(:add_existing), select_text: as_(:create_new)}
456
+ def active_scaffold_show_new_subform_link(column, record, select_id, subform_id, options: {})
457
+ add_existing = active_scaffold_add_new_text(options, :add_existing_text, :add_existing)
458
+ create_new = active_scaffold_add_new_text(options, :add_new_text, :create_new)
459
+ data = {select_id: select_id, subform_id: subform_id, subform_text: add_existing, select_text: create_new}
390
460
  label = data[record.send(column.name)&.new_record? ? :subform_text : :select_text]
391
461
  link_to(label, '#', data: data, class: 'show-new-subform')
392
462
  end
@@ -439,7 +509,8 @@ module ActiveScaffold
439
509
 
440
510
  def active_scaffold_plural_association_options(column, record = nil)
441
511
  associated_options = record.send(column.association.name)
442
- [associated_options, associated_options | sorted_association_options_find(column.association, nil, record)]
512
+ helper_method = association_helper_method(column.association, :sorted_association_options_find)
513
+ [associated_options, associated_options | send(helper_method, column.association, nil, record)]
443
514
  end
444
515
 
445
516
  def active_scaffold_input_plural_association(column, options, ui_options: column.options)
@@ -557,7 +628,8 @@ module ActiveScaffold
557
628
  html_options.merge!(ui_options[:html_options] || {})
558
629
  options =
559
630
  if column.association
560
- sorted_association_options_find(column.association, nil, record)
631
+ helper_method = association_helper_method(column.association, :sorted_association_options_find)
632
+ send(helper_method, column.association, nil, record)
561
633
  else
562
634
  enum_options_method = override_helper_per_model(:active_scaffold_enum_options, record.class)
563
635
  send(enum_options_method, column, record, ui_options: ui_options)
@@ -579,18 +651,35 @@ module ActiveScaffold
579
651
  if ui_options[:include_blank]
580
652
  label = ui_options[:include_blank]
581
653
  label = as_(ui_options[:include_blank]) if ui_options[:include_blank].is_a?(Symbol)
582
- radios.prepend content_tag(:label, radio_button(:record, column.name, '', html_options.merge(id: nil)) + label)
654
+ radio_id = "#{html_options[:id]}-"
655
+ radios.prepend content_tag(:label, radio_button(:record, column.name, '', html_options.merge(id: radio_id)) + label)
583
656
  end
584
657
  if ui_options[:add_new]
585
- create_new_button = radio_button_tag(html_options[:name], '', selected&.new_record?, html_options.merge(id: nil, class: "#{html_options[:class]} show-new-subform").except(:object))
586
- radios << content_tag(:label, create_new_button << as_(:create_new)) <<
587
- active_scaffold_new_record_subform(column, record, html_options, ui_options: ui_options, skip_link: true)
658
+ if ui_options[:add_new] == true || ui_options[:add_new][:mode].in?([nil, :subform])
659
+ create_new = content_tag(:label) do
660
+ radio_button_tag(html_options[:name], '', selected&.new_record?, html_options.merge(
661
+ id: "#{html_options[:id]}-create_new", class: "#{html_options[:class]} show-new-subform"
662
+ ).except(:object)) <<
663
+ active_scaffold_add_new_text(ui_options[:add_new], :add_new_text, :create_new)
664
+ end
665
+ radios << create_new
666
+ skip_link = true
667
+ else
668
+ ui_options = ui_options.merge(add_new: ui_options[:add_new].merge(
669
+ url_options: {
670
+ parent_scope: html_options[:name].gsub(/^record|\[[^\]]*\]$/, '').presence,
671
+ radio_data: html_options.slice(*html_options.keys.grep(/^data-update_/))
672
+ }
673
+ ))
674
+ radios << content_tag(:span, '', class: 'new-radio-container', id: html_options[:id])
675
+ end
676
+ radios << active_scaffold_add_new(column, record, html_options, ui_options: ui_options, skip_link: skip_link)
588
677
  end
589
678
  safe_join radios
590
679
  else
591
680
  html = content_tag(:span, as_(:no_options), class: "#{html_options[:class]} no-options", id: html_options[:id])
592
681
  html << hidden_field_tag(html_options[:name], '', id: nil)
593
- html << active_scaffold_new_record_subform(column, record, html_options, ui_options: ui_options) if ui_options[:add_new]
682
+ html << active_scaffold_add_new(column, record, html_options, ui_options: ui_options) if ui_options[:add_new]
594
683
  html
595
684
  end
596
685
  end
@@ -778,7 +867,8 @@ module ActiveScaffold
778
867
  options.merge!(active_scaffold_input_text_options)
779
868
  record_select_field(options[:name], nil, options)
780
869
  else
781
- select_options = sorted_association_options_find(nested.association, nil, record)
870
+ helper_method = association_helper_method(column.association, :sorted_association_options_find)
871
+ select_options = send(helper_method, nested.association, nil, record)
782
872
  select_options ||= active_scaffold_config.model.all
783
873
  select_options = options_from_collection_for_select(select_options, :id, :to_label)
784
874
  select_tag 'associated_id', (content_tag(:option, as_(:_select_), value: '') + select_options) unless select_options.empty?
@@ -16,6 +16,10 @@ module ActiveScaffold
16
16
  end
17
17
  end
18
18
 
19
+ def active_scaffold_human_filter_for(filter_option)
20
+ filter_option.label
21
+ end
22
+
19
23
  def active_scaffold_grouped_by_label
20
24
  text, = active_scaffold_config.field_search.group_options.find do |text, value|
21
25
  (value || text).to_s == field_search_params['active_scaffold_group']