active_scaffold 4.3.2 → 4.4.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +19 -0
  3. data/README.md +1 -1
  4. data/app/assets/javascripts/active_scaffold/active_scaffold.js +98 -11
  5. data/app/assets/javascripts/active_scaffold/draggable_lists.js +4 -3
  6. data/app/assets/javascripts/active_scaffold/load.js +20 -5
  7. data/app/assets/stylesheets/active_scaffold/_images.scss +53 -1
  8. data/app/assets/stylesheets/active_scaffold/_layout.scss +1 -66
  9. data/app/views/active_scaffold_overrides/_add_existing_form.html.erb +17 -11
  10. data/app/views/active_scaffold_overrides/_base_form.html.erb +29 -19
  11. data/app/views/active_scaffold_overrides/_field_search.html.erb +9 -9
  12. data/app/views/active_scaffold_overrides/_field_search_columns.html.erb +29 -33
  13. data/app/views/active_scaffold_overrides/_form.html.erb +43 -34
  14. data/app/views/active_scaffold_overrides/_form_association.html.erb +3 -3
  15. data/app/views/active_scaffold_overrides/_form_association_footer.html.erb +20 -16
  16. data/app/views/active_scaffold_overrides/_form_association_record.html.erb +3 -4
  17. data/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb +2 -1
  18. data/app/views/active_scaffold_overrides/_list.html.erb +6 -4
  19. data/app/views/active_scaffold_overrides/_list_calculations.html.erb +1 -1
  20. data/app/views/active_scaffold_overrides/_list_header.html.erb +3 -3
  21. data/app/views/active_scaffold_overrides/_list_messages.html.erb +19 -19
  22. data/app/views/active_scaffold_overrides/_list_messages_content.html.erb +5 -5
  23. data/app/views/active_scaffold_overrides/_list_pagination.html.erb +7 -5
  24. data/app/views/active_scaffold_overrides/_list_pagination_links.html.erb +5 -3
  25. data/app/views/active_scaffold_overrides/_list_record.html.erb +13 -8
  26. data/app/views/active_scaffold_overrides/_list_with_header.html.erb +8 -8
  27. data/app/views/active_scaffold_overrides/_messages.html.erb +3 -3
  28. data/app/views/active_scaffold_overrides/_render_field.js.erb +8 -11
  29. data/app/views/active_scaffold_overrides/_search.html.erb +7 -6
  30. data/app/views/active_scaffold_overrides/_show.html.erb +4 -4
  31. data/app/views/active_scaffold_overrides/_show_actions.html.erb +2 -2
  32. data/app/views/active_scaffold_overrides/_show_columns.html.erb +28 -30
  33. data/app/views/active_scaffold_overrides/add_existing.js.erb +7 -4
  34. data/app/views/active_scaffold_overrides/render_field.js.erb +1 -1
  35. data/config/locales/de.yml +2 -1
  36. data/config/locales/en.yml +33 -32
  37. data/config/locales/es.yml +15 -14
  38. data/config/locales/fr.yml +3 -2
  39. data/config/locales/hu.yml +3 -2
  40. data/config/locales/ja.yml +2 -1
  41. data/config/locales/ro.yml +169 -0
  42. data/config/locales/ru.yml +8 -7
  43. data/config/locales/ta.yml +7 -6
  44. data/config/locales/zh_Hans.yml +14 -13
  45. data/lib/active_scaffold/actions/common_search.rb +1 -14
  46. data/lib/active_scaffold/actions/core.rb +55 -16
  47. data/lib/active_scaffold/actions/field_search.rb +2 -2
  48. data/lib/active_scaffold/actions/list.rb +35 -13
  49. data/lib/active_scaffold/actions/nested.rb +31 -7
  50. data/lib/active_scaffold/actions/show.rb +1 -1
  51. data/lib/active_scaffold/active_record_permissions.rb +1 -1
  52. data/lib/active_scaffold/attribute_params.rb +2 -1
  53. data/lib/active_scaffold/bridges/date_picker/helper.rb +14 -0
  54. data/lib/active_scaffold/bridges/date_picker.rb +10 -0
  55. data/lib/active_scaffold/config/list.rb +10 -1
  56. data/lib/active_scaffold/core.rb +9 -6
  57. data/lib/active_scaffold/data_structures/action_columns.rb +2 -0
  58. data/lib/active_scaffold/data_structures/column.rb +27 -6
  59. data/lib/active_scaffold/data_structures/filter.rb +4 -0
  60. data/lib/active_scaffold/data_structures/nested_info.rb +8 -1
  61. data/lib/active_scaffold/data_structures/sorting.rb +1 -1
  62. data/lib/active_scaffold/extensions/action_view_rendering.rb +1 -1
  63. data/lib/active_scaffold/finder.rb +66 -21
  64. data/lib/active_scaffold/helpers/action_link_helpers.rb +43 -18
  65. data/lib/active_scaffold/helpers/assets_helpers.rb +4 -4
  66. data/lib/active_scaffold/helpers/filter_helpers.rb +3 -3
  67. data/lib/active_scaffold/helpers/form_column_helpers.rb +34 -22
  68. data/lib/active_scaffold/helpers/form_ui_helpers.rb +15 -10
  69. data/lib/active_scaffold/helpers/framework_ui_helpers.rb +22 -0
  70. data/lib/active_scaffold/helpers/human_condition_helpers.rb +1 -0
  71. data/lib/active_scaffold/helpers/list_column_helpers.rb +19 -0
  72. data/lib/active_scaffold/helpers/pagination_helpers.rb +28 -7
  73. data/lib/active_scaffold/helpers/search_column_helpers.rb +32 -5
  74. data/lib/active_scaffold/helpers/tabs_helpers.rb +3 -5
  75. data/lib/active_scaffold/helpers/view_helpers.rb +12 -0
  76. data/lib/active_scaffold/tableless.rb +11 -8
  77. data/lib/active_scaffold/version.rb +2 -2
  78. data/lib/active_scaffold.rb +169 -0
  79. data/lib/tasks/bundle.rake +2 -1
  80. data/lib/tasks/mock_app.rake +43 -0
  81. metadata +20 -7
  82. data/shoulda_macros/macros.rb +0 -145
@@ -74,6 +74,10 @@ module ActiveScaffold::DataStructures
74
74
  end
75
75
  end
76
76
 
77
+ def security_method_set?
78
+ @security_method.present?
79
+ end
80
+
77
81
  protected
78
82
 
79
83
  # called during clone or dup. makes the clone/dup deeper.
@@ -17,6 +17,8 @@ module ActiveScaffold::DataStructures
17
17
  def initialize(model, params)
18
18
  @parent_scaffold = "#{params[:parent_scaffold].to_s.camelize}Controller".constantize
19
19
  @parent_model = @parent_scaffold.active_scaffold_config.model
20
+ rescue NameError => e
21
+ raise ActionController::BadRequest, e.message
20
22
  end
21
23
 
22
24
  def to_params
@@ -82,9 +84,14 @@ module ActiveScaffold::DataStructures
82
84
  def initialize(model, params)
83
85
  super
84
86
  column = parent_scaffold.active_scaffold_config.columns[params[:association].to_sym]
87
+ unless column&.association
88
+ raise ActionController::BadRequest,
89
+ "Invalid nested association #{params[:association].inspect} for #{parent_scaffold.active_scaffold_config.model.name}"
90
+ end
91
+
85
92
  @param_name = column.model.name.foreign_key.to_sym
86
93
  @parent_id = params[@param_name]
87
- @association = column&.association
94
+ @association = column.association
88
95
  @child_association = association.reverse_association(model) if association
89
96
  setup_constrained_fields
90
97
  end
@@ -136,7 +136,7 @@ module ActiveScaffold::DataStructures
136
136
  sql = sql.to_sql if sql.respond_to?(:to_sql)
137
137
 
138
138
  parts = Array(sql).map do |column|
139
- mongoid? ? [column, sort_direction] : "#{column} #{sort_direction}"
139
+ mongoid? ? [column, sort_direction] : {column => sort_direction}
140
140
  end
141
141
  order << parts
142
142
  end
@@ -162,7 +162,7 @@ module ActiveScaffold # :nodoc:
162
162
  content_tag(:div, class: 'active-scaffold-header') do
163
163
  content_tag(:h2) do
164
164
  label = options[:label] || remote_controller_config(remote_controller).list.label
165
- link_to(label, url, remote: true, class: 'load-embedded', data: {error_msg: as_(:error_500)}) <<
165
+ link_to(label, url, remote: true, class: 'load-embedded', data: {error_msg: server_error_msg(as_(:error_500))}) <<
166
166
  loading_indicator_tag(url_options)
167
167
  end
168
168
  end
@@ -2,8 +2,11 @@
2
2
 
3
3
  module ActiveScaffold
4
4
  module Finder
5
- def self.like_operator
6
- @@like_operator ||= ::ActiveRecord::Base.connection.adapter_name.in?(%w[PostgreSQL PostGIS]) ? 'ILIKE' : 'LIKE'
5
+ def self.like_operator(column = nil)
6
+ return column.like_operator if column&.like_operator
7
+
8
+ connection = column&.active_record_class&.connection || ::ActiveRecord::Base.connection
9
+ connection.adapter_name.in?(%w[PostgreSQL PostGIS]) ? 'ILIKE' : 'LIKE'
7
10
  end
8
11
 
9
12
  def self.logical_comparators
@@ -35,13 +38,14 @@ module ActiveScaffold
35
38
 
36
39
  def type_casted_tokens(tokens, columns, like_pattern)
37
40
  tokens.map do |value|
38
- columns.each_with_object({}) do |column, column_tokens|
39
- column_tokens[column.name] =
41
+ columns.to_h do |column|
42
+ casted_value =
40
43
  if column.text?
41
44
  like_pattern.sub('?', column.active_record? ? column.active_record_class.sanitize_sql_like(value) : value)
42
45
  else
43
- ActiveScaffold::Core.column_type_cast(value, column.column)
46
+ ActiveScaffold::Core.column_type_cast(value, column.column, column.model)
44
47
  end
48
+ [column.name, casted_value]
45
49
  end
46
50
  end
47
51
  end
@@ -51,7 +55,7 @@ module ActiveScaffold
51
55
  where_clauses = []
52
56
  columns.each do |column|
53
57
  column.search_sql.each do |search_sql|
54
- where_clauses << "#{search_sql} #{column.text? ? ActiveScaffold::Finder.like_operator : '='} ?"
58
+ where_clauses << "#{search_sql} #{column.text? ? ActiveScaffold::Finder.like_operator(column) : '='} ?"
55
59
  end
56
60
  end
57
61
  phrase = where_clauses.join(' OR ')
@@ -170,7 +174,7 @@ module ActiveScaffold
170
174
  if value == 'null'
171
175
  condition_for_null_type(column, value)
172
176
  else
173
- ['%<search_sql>s = ?', column.column ? ActiveScaffold::Core.column_type_cast(value, column.column) : value]
177
+ ['%<search_sql>s = ?', column.column ? ActiveScaffold::Core.column_type_cast(value, column.column, column.model) : value]
174
178
  end
175
179
  when :integer, :decimal, :float
176
180
  condition_for_numeric(column, value)
@@ -180,7 +184,7 @@ module ActiveScaffold
180
184
  else
181
185
  condition_for_single_value(column, value, like_pattern)
182
186
  end
183
- when :date, :time, :datetime, :timestamp
187
+ when :date, :time, :datetime, :timestamp, :timestamptz
184
188
  condition_for_datetime(column, value)
185
189
  when :select, :select_multiple, :draggable, :multi_select, :country, :usa_state, :chosen, :multi_chosen
186
190
  if value.is_a?(Hash)
@@ -211,9 +215,9 @@ module ActiveScaffold
211
215
  def condition_for_single_value(column, value, like_pattern = nil)
212
216
  if column.text?
213
217
  value = column.active_record_class.sanitize_sql_like(value) if column.active_record?
214
- ["%<search_sql>s #{ActiveScaffold::Finder.like_operator} ?", like_pattern.sub('?', value)]
218
+ ["%<search_sql>s #{ActiveScaffold::Finder.like_operator(column)} ?", like_pattern.sub('?', value)]
215
219
  else
216
- ['%<search_sql>s = ?', ActiveScaffold::Core.column_type_cast(value, column.column)]
220
+ ['%<search_sql>s = ?', ActiveScaffold::Core.column_type_cast(value, column.column, column.model)]
217
221
  end
218
222
  end
219
223
 
@@ -228,7 +232,7 @@ module ActiveScaffold
228
232
  elsif ActiveScaffold::Finder::STRING_COMPARATORS.value?(value[:opt])
229
233
  text = column.active_record? ? column.active_record_class.sanitize_sql_like(value[:from]) : value[:from]
230
234
  [
231
- "%<search_sql>s #{'NOT ' if value[:opt].start_with?('not_')}#{ActiveScaffold::Finder.like_operator} ?",
235
+ "%<search_sql>s #{'NOT ' if value[:opt].start_with?('not_')}#{ActiveScaffold::Finder.like_operator(column)} ?",
232
236
  value[:opt].sub('not_', '').sub('?', text)
233
237
  ]
234
238
  elsif value[:opt] == 'BETWEEN'
@@ -367,7 +371,7 @@ module ActiveScaffold
367
371
  end
368
372
  elsif conversion == :to_date
369
373
  parse_date_with_format(*format_for_date(column, value, column.send(ui_method), ui_options))
370
- elsif value.include?('T')
374
+ elsif value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/) # iso8601 format
371
375
  Time.zone.parse(value)
372
376
  else # datetime
373
377
  time = parse_time_with_format(value, *format_for_datetime(column, value, column.send(ui_method), ui_options))
@@ -623,7 +627,6 @@ module ActiveScaffold
623
627
  search_conditions = all_conditions
624
628
 
625
629
  sorting = options[:sorting]&.clause
626
- sorting = sorting.map { |part| Arel.sql(part) } if sorting && active_scaffold_config.active_record?
627
630
  # create a general-use options array that's compatible with Rails finders
628
631
  finder_options = {
629
632
  reorder: sorting,
@@ -647,6 +650,22 @@ module ActiveScaffold
647
650
  finder_options
648
651
  end
649
652
 
653
+ def add_order_expressions_to_select(query, sorting)
654
+ return query unless query.respond_to?(:distinct_value) && query.distinct_value &&
655
+ active_scaffold_config.model.connection.needs_order_expressions_in_select?
656
+
657
+ order_expressions = order_select_expressions(sorting)
658
+ return query if order_expressions.empty?
659
+
660
+ select_columns = query.select_values
661
+ select_columns = ["#{active_scaffold_config.model.quoted_table_name}.*"] if select_columns.empty?
662
+ query.reselect(*(select_columns + order_expressions).uniq)
663
+ end
664
+
665
+ def order_select_expressions(sorting)
666
+ Array(sorting&.clause).flat_map { |order| order.is_a?(Hash) ? order.keys : [] }
667
+ end
668
+
650
669
  def count_items(query, find_options = {}, count_includes = nil)
651
670
  count_includes ||= find_options[:includes] if find_options[:conditions].present?
652
671
  options = find_options.except(:select, :reorder, :order)
@@ -671,31 +690,54 @@ module ActiveScaffold
671
690
  find_options = finder_options(options)
672
691
  query = filtered_query
673
692
  query = query.where(nil) if active_scaffold_config.active_record? # where(nil) is needed because we need a relation
693
+ count_query = query
674
694
 
675
695
  # NOTE: we must use :include in the count query, because some conditions may reference other tables
676
- if options[:pagination] && options[:pagination] != :infinite
696
+ count_after_first_page = count_after_loading_first_page?(options)
697
+ if options[:pagination] && options[:pagination] != :infinite && !count_after_first_page
677
698
  count = count_items(query, find_options, options[:count_includes])
678
699
  end
679
700
 
680
701
  query = append_to_query(query, find_options)
702
+ query = add_order_expressions_to_select(query, options[:sorting])
703
+ first_page_items = load_page_for_delayed_count(query, options[:per_page]) if count_after_first_page
704
+ if first_page_items
705
+ count = first_page_items.size
706
+ count = count_items(count_query, find_options, options[:count_includes]) if count == options[:per_page]
707
+ end
681
708
  # we build the paginator differently for method- and sql-based sorting
682
709
  pager = if options[:sorting]&.sorts_by_method?
683
710
  ::Paginator.new(count, options[:per_page]) do |offset, per_page|
684
711
  calculate_last_modified(query)
685
712
  sorted_collection = sort_collection_by_column(query.to_a, *options[:sorting].first)
686
- sorted_collection = sorted_collection.slice(offset, per_page) if options[:pagination]
713
+ sorted_collection = sorted_collection.slice(offset, per_page) || [] if options[:pagination]
687
714
  sorted_collection
688
715
  end
689
716
  else
690
717
  ::Paginator.new(count, options[:per_page]) do |offset, per_page|
691
- query = append_to_query(query, offset: offset, limit: per_page) if options[:pagination]
692
- calculate_last_modified(query)
693
- query
718
+ if first_page_items && offset.zero?
719
+ first_page_items
720
+ else
721
+ page_query = options[:pagination] ? append_to_query(query, offset: offset, limit: per_page) : query
722
+ calculate_last_modified(page_query)
723
+ page_query
724
+ end
694
725
  end
695
726
  end
696
727
  pager.page(options[:page])
697
728
  end
698
729
 
730
+ def count_after_loading_first_page?(options)
731
+ active_scaffold_config.active_record? && options[:pagination] && options[:pagination] != :infinite && options[:page].to_i == 1 &&
732
+ !options[:sorting]&.sorts_by_method?
733
+ end
734
+
735
+ def load_page_for_delayed_count(query, per_page)
736
+ page_query = append_to_query(query, offset: 0, limit: per_page)
737
+ calculate_last_modified(page_query)
738
+ page_query.to_a
739
+ end
740
+
699
741
  def calculate_last_modified(query)
700
742
  return unless conditional_get_support? && ActiveScaffold::OrmChecks.columns_hash(query.klass)['updated_at']
701
743
 
@@ -730,8 +772,11 @@ module ActiveScaffold
730
772
  rel.send(k, v)
731
773
  end
732
774
  end
733
- relation.distinct_value = true if options[:left_outer_joins].present? || options[:left_joins].present?
734
- relation
775
+ distinct_query?(options) ? relation.distinct : relation
776
+ end
777
+
778
+ def distinct_query?(options)
779
+ options[:left_outer_joins].present? || options[:left_joins].present?
735
780
  end
736
781
 
737
782
  def joins_for_finder
@@ -759,7 +804,7 @@ module ActiveScaffold
759
804
  def sort_collection_by_column(collection, column, order)
760
805
  sorter = column.sort[:method]
761
806
  collection = collection.sort_by do |record|
762
- value = sorter.is_a?(Proc) ? record.instance_exec(record, &sorter) : record.instance_eval(sorter.to_s)
807
+ value = sorter.is_a?(Proc) ? record.instance_exec(record, &sorter) : record.send(sorter)
763
808
  value = '' if value.nil?
764
809
  value
765
810
  end
@@ -26,8 +26,8 @@ module ActiveScaffold
26
26
  record = record_or_ul_options unless record_or_ul_options.is_a?(Hash)
27
27
  ul_options ||= {}
28
28
  ul_options[:class] = [ul_options[:class], 'dynamic-menu'].compact.join(' ')
29
- html = content_tag :ul, ul_options do
30
- safe_join(links.map { |link| content_tag :li, link })
29
+ html = as_element :dynamic_action_group, **ul_options do
30
+ safe_join(links.map { |link| as_element :dynamic_action_group_element, link })
31
31
  end
32
32
  raw "ActiveScaffold.display_dynamic_action_group('#{get_action_link_id action_link, record}', '#{escape_javascript html}');" # rubocop:disable Rails/OutputSafety
33
33
  end
@@ -77,8 +77,7 @@ module ActiveScaffold
77
77
  end
78
78
 
79
79
  def display_action_link_separator(options)
80
- tag = options[:level_0_tag] || :a if options[:level].zero?
81
- content_tag(tag || :li, '&nbsp;'.html_safe, class: 'separator')
80
+ as_element(:action_link_separator, '&nbsp;'.html_safe, proc_options: options)
82
81
  end
83
82
 
84
83
  def display_click_menu_link(link, record, options)
@@ -99,25 +98,36 @@ module ActiveScaffold
99
98
 
100
99
  def display_action_link(link, content, record, options)
101
100
  if content
102
- html_classes = hover_via_click? ? 'hover_click ' : ''
103
- if options[:level].zero?
104
- html_classes += 'action_group'
105
- group_tag = :div
106
- else
107
- html_classes += 'top' if options[:first_action]
108
- group_tag = :li
109
- end
110
- content = content_tag(group_tag, class: html_classes.presence, onclick: ('' if hover_via_click?)) do
111
- content_tag(:div, link.label(record), class: link.css_class, title: options[:title]) << content_tag(:ul, content)
112
- end
101
+ content = render_action_link_group(link, content, record, options)
113
102
  else
114
103
  content = render_action_link(link, record, options)
115
- content = content_tag(:li, content, class: ('top' if options[:first_action])) unless options[:level].zero?
104
+ unless options[:level].zero?
105
+ context = {link: link, record: record, options: options}
106
+ content = as_element(
107
+ :action_link_item,
108
+ content,
109
+ proc_options: context,
110
+ class: ('top' if options[:first_action])
111
+ )
112
+ end
116
113
  end
117
114
  content = content_tag(options[:level_0_tag], content, options[:options_level_0_tag]) if options[:level].zero? && options[:level_0_tag]
118
115
  content
119
116
  end
120
117
 
118
+ def render_action_link_group(link, content, record, options, group_label: nil)
119
+ context = options.merge(link: link, record: record)
120
+ as_element(:action_link_group, proc_options: context) do
121
+ as_element(
122
+ :action_link_group_title,
123
+ group_label || link.label(record),
124
+ proc_options: context,
125
+ class: link.css_class,
126
+ title: options[:title]
127
+ ) << as_element(:action_link_group_content, content, proc_options: context)
128
+ end
129
+ end
130
+
121
131
  def render_action_link(link, record = nil, options = {})
122
132
  if link.action.nil? || link.column&.association&.polymorphic?
123
133
  link = action_link_to_inline_form(link, record) if link.column&.association
@@ -167,6 +177,7 @@ module ActiveScaffold
167
177
  class: html_class,
168
178
  title: cache ? '--REASON--' : options[:not_authorized_reason]
169
179
  }
180
+ html_options = action_link_element_attributes(link, record, options, html_options, authorized: false)
170
181
  html = action_link_html(link, nil, html_options, record)
171
182
  cache ? html.to_str : html
172
183
  end
@@ -175,10 +186,16 @@ module ActiveScaffold
175
186
  url = cache ? '--URL--' : action_link_url(link, record)
176
187
  options[:link] = '--LABEL--' if cache
177
188
  html_options = action_link_html_options(link, record, options, cache: cache)
189
+ html_options = action_link_element_attributes(link, record, options, html_options, authorized: true)
178
190
  html = action_link_html(link, url, html_options, record)
179
191
  cache ? html.to_str : html
180
192
  end
181
193
 
194
+ def action_link_element_attributes(link, record, options, attributes, authorized:)
195
+ context = {link: link, record: record, options: options, authorized: authorized}
196
+ as_element_attributes(:action_link, proc_options: context, **attributes)
197
+ end
198
+
182
199
  # setup the action link to inline form
183
200
  def action_link_to_inline_form(link, record)
184
201
  link = link.dup
@@ -302,12 +319,20 @@ module ActiveScaffold
302
319
 
303
320
  def column_in_params_conditions?(key)
304
321
  if key.match?(/!$/)
305
- conditions_from_params[1..].any? { |node| node.left.name.to_s == key[0..-2] }
322
+ conditions_from_params[1..].any? { |node| condition_node_for_column?(node, key[0..-2]) }
306
323
  else
307
324
  conditions_from_params[0].include?(key)
308
325
  end
309
326
  end
310
327
 
328
+ def condition_node_for_column?(node, column_name)
329
+ return node.name.to_s == column_name if node.is_a?(Arel::Attributes::Attribute)
330
+
331
+ %i[expr left right].any? do |child|
332
+ node.respond_to?(child) && condition_node_for_column?(node.public_send(child), column_name)
333
+ end
334
+ end
335
+
311
336
  def ignore_param_for_nested?(key)
312
337
  NESTED_PARAMS.include?(key) || column_in_params_conditions?(key) || (nested? && nested.param_name == key)
313
338
  end
@@ -335,7 +360,7 @@ module ActiveScaffold
335
360
  end
336
361
  end
337
362
  if nested_singular_association? && action_name == 'index'
338
- # pass current path as return_to, for nested listing on singular association, so forms doesn't return to parent listing
363
+ # pass the current path as return_to, for nested listing on singular association, so forms don't return to parent listing
339
364
  @query_string_params << :return_to
340
365
  non_nested_query_string_options[:return_to] = request.fullpath
341
366
  end
@@ -8,7 +8,7 @@ module ActiveScaffold
8
8
  js_tags << active_scaffold_javascript_config
9
9
  unless defined?(Importmap)
10
10
  # Without importmap, active_scaffold/load must be added to the layout too
11
- js_tags << javascript_include_tag('active_scaffold/load')
11
+ js_tags << javascript_include_tag('active_scaffold/load', nonce: true)
12
12
  end
13
13
  safe_join js_tags
14
14
  end
@@ -22,16 +22,16 @@ module ActiveScaffold
22
22
  plugins: ActiveScaffold.javascripts.map { |asset| asset_path(asset, extname: '.js') }
23
23
  )
24
24
  if ActiveScaffold.jquery_ui_included?
25
- config[:datepickerLocalization] = ActiveScaffold::Assets.active_scaffold_js_code(:jquery_ui)
25
+ config[:datepickerLocalization] = ActiveScaffold::Bridges[:date_picker].localization_data
26
26
  end
27
27
 
28
28
  if Object.const_defined?(:Jquery) && Jquery.const_defined?(:Ui)
29
29
  config[:jqueryUi] = ActiveScaffold::Assets::JqueryUiManifest.all_dependencies.map { |asset| asset_path(asset, extname: '.js') }
30
30
  end
31
31
 
32
- javascript_tag <<~JS
32
+ javascript_tag(<<~JS, nonce: true)
33
33
  window.ActiveScaffold = window.ActiveScaffold || {};
34
- window.ActiveScaffold.config = #{config.to_json};
34
+ window.ActiveScaffold.config = #{json_escape config.to_json};
35
35
  JS
36
36
  end
37
37
  end
@@ -5,8 +5,8 @@ module ActiveScaffold
5
5
  # Helpers rendering filters
6
6
  module FilterHelpers
7
7
  def clear_filters_params
8
- active_scaffold_config.list.filters.each_with_object({}) do |filter, url_options|
9
- url_options[filter.name] = nil
8
+ active_scaffold_config.list.filters.to_h do |filter|
9
+ [filter.name, nil]
10
10
  end
11
11
  end
12
12
 
@@ -14,7 +14,7 @@ module ActiveScaffold
14
14
  return unless filters_enabled?
15
15
 
16
16
  content = render_filters filters
17
- content_tag :div, content, class: 'filters' if content.present?
17
+ as_element :filters, content if content.present?
18
18
  end
19
19
 
20
20
  def render_filters(filters)
@@ -79,14 +79,16 @@ module ActiveScaffold
79
79
  end
80
80
 
81
81
  def active_scaffold_subform_record_actions(association_column, record, locked, scope)
82
- return unless association_column.association.collection? && !locked
82
+ # association_column nil when called for batch_create for example
83
+ return if locked || association_column&.association&.singular?
83
84
 
84
- auth = %i[destroy delete_all delete].exclude?(association_column.association.dependent)
85
+ auth = %i[destroy delete_all delete].exclude?(association_column&.association&.dependent)
85
86
  auth, reason = record.authorized_for?(crud_type: :delete, reason: true) unless auth
86
87
  if auth
87
- link_to(as_(:remove), '#', class: 'destroy')
88
+ attributes = as_element_attributes(:subform_record_remove, class: 'destroy')
89
+ link_to(as_(:remove), '#', attributes)
88
90
  else
89
- content_tag :span, reason, class: 'destroy reason'
91
+ as_element(:subform_record_remove_reason, reason)
90
92
  end
91
93
  end
92
94
 
@@ -121,7 +123,9 @@ module ActiveScaffold
121
123
 
122
124
  collapsible_id = "container_#{id_control}" if (column.form_ui_options || column.options)[:collapsible]
123
125
 
124
- {name: name, class: classes, id: id_control, collapsible_id: collapsible_id}.merge(options)
126
+ attributes = {name: name, class: classes, id: id_control, collapsible_id: collapsible_id}.merge(options)
127
+ context = {column: column, form_ui: column.form_ui}
128
+ as_element_attributes(:form_control, proc_options: context, **attributes)
125
129
  end
126
130
 
127
131
  def current_form_columns(record, scope, subform_controller = nil)
@@ -174,10 +178,10 @@ module ActiveScaffold
174
178
  {}
175
179
  end
176
180
 
177
- def render_subsection(column, record, scope, form_action, partial: 'form')
178
- subsection_id = sub_section_id(sub_section: column.label)
181
+ def render_subsection(column, record, scope, form_action, partial: 'form', subsection_id: nil, &)
182
+ subsection_id ||= sub_section_id(sub_section: column.label)
179
183
  locals = {columns: column, form_action: form_action, scope: scope}
180
- header = content_tag(:h5) do
184
+ header = as_element(:form_subsection_header) do
181
185
  h(column.label) <<
182
186
  link_to_visibility_toggle(subsection_id, default_visible: !column.collapsed)
183
187
  end
@@ -188,6 +192,8 @@ module ActiveScaffold
188
192
  render partial, locals.merge(subsection_id: "#{subsection_id}-#{tab_id}", tab_id: tab_id, tab_value: tab_value)
189
193
  end
190
194
  end
195
+ elsif block_given?
196
+ header << capture(subsection_id, &)
191
197
  else
192
198
  header << render(partial, locals.merge(subsection_id: subsection_id))
193
199
  end
@@ -217,6 +223,8 @@ module ActiveScaffold
217
223
  end
218
224
  end
219
225
 
226
+ def form_ui_class(form_ui) = form_ui
227
+
220
228
  def form_column_is_hidden?(column, record, scope = nil) # rubocop:disable Naming/PredicateMethod
221
229
  if column.clear_form_column_if.nil?
222
230
  condition_to_hide = column.hide_form_column_if
@@ -238,35 +246,38 @@ module ActiveScaffold
238
246
 
239
247
  def column_description(column, record, scope = nil)
240
248
  desc = column.description(record, scope)
241
- content_tag(:span, h(desc) + content_tag(:span, nil, class: 'close'), class: 'description') if desc.present?
249
+ as_element(:form_field_description, h(desc) + as_element(:form_field_description_close)) if desc.present?
242
250
  end
243
251
 
244
- def form_attribute(column, record, scope = nil, only_value = false, col_class = nil, form_columns: nil)
252
+ def form_attribute(column, record, scope = nil, only_value = false, col_class = nil, form_columns: nil, field: nil)
245
253
  column_options = active_scaffold_input_options(column, scope, object: record)
246
254
  collapsible_id = column_options.delete :collapsible_id
247
255
  attributes = field_attributes(column, record)
248
256
  attributes[:class] = "#{attributes[:class]} #{col_class}" if col_class.present?
249
257
  if only_value
250
- field = content_tag(:span, show_column_value(record, column), column_options.except(:name, :object))
258
+ field ||= content_tag(:span, show_column_value(record, column), column_options.except(:name, :object))
251
259
  if column.association.nil? || column.association.belongs_to?
252
260
  # hidden field probably not needed, but leaving it just in case
253
261
  # but it isn't working for assocations which are not belongs_to
254
- method = column.association ? column.association.foreign_key : column.name
255
- field << hidden_field(:record, method, column_options)
262
+ field << hidden_field(:record, column.association&.foreign_key || column.name, column_options)
256
263
  end
257
264
  else
258
- field = active_scaffold_input_for column, scope, column_options, form_columns: form_columns
259
- end
260
- if field
261
- field << loading_indicator_tag(action: :render_field, id: params[:id]) if column.update_columns
262
- desc = column_description(column, record, scope)
263
- field << desc if desc.present?
265
+ field ||= active_scaffold_input_for column, scope, column_options, form_columns: form_columns
264
266
  end
267
+ field << loading_indicator_tag(action: :render_field, id: params[:id]) if field && column.update_columns
268
+ description = column_description(column, record, scope)
265
269
 
266
270
  label = label_tag(label_for(column, column_options), form_column_label(column, record, scope))
267
271
  label << h(' ') << link_to_visibility_toggle(collapsible_id) if collapsible_id
268
- content_tag :dl, attributes do
269
- content_tag(:dt, label) << content_tag(:dd, field, id: collapsible_id)
272
+ form_attribute_html(column, label, field, description, attributes, collapsible_id: collapsible_id)
273
+ end
274
+
275
+ def form_attribute_html(column, label, field, description, attributes, collapsible_id: nil)
276
+ (field ||= h('')) << description if description.present?
277
+ context = {column: column}
278
+ as_element :form_attribute, proc_options: context, **attributes do
279
+ as_element(:form_attribute_label, label, proc_options: context) <<
280
+ as_element(:form_attribute_field, field, proc_options: context, id: collapsible_id)
270
281
  end
271
282
  end
272
283
 
@@ -322,7 +333,8 @@ module ActiveScaffold
322
333
 
323
334
  # Should this column be displayed in the subform?
324
335
  def in_subform?(column, parent_record, parent_column)
325
- return true unless column.association
336
+ # parent_column nil when called for batch_create for example
337
+ return true if parent_column.nil? || column.association.nil?
326
338
 
327
339
  if column.association.reverse.nil?
328
340
  # Polymorphic associations can't appear because they *might* be the reverse association
@@ -27,18 +27,23 @@ module ActiveScaffold
27
27
 
28
28
  def active_scaffold_input_singular_association(column, html_options, options = {}, ui_options: column.options)
29
29
  record = html_options.delete(:object)
30
- associated = html_options.include?(:associated) ? html_options.delete(:associated) : record.send(column.association.name)
31
-
32
30
  helper_method = association_helper_method(column.association, :sorted_association_options_find)
33
31
  select_options = send(helper_method, column.association, nil, record)
34
- select_options.unshift(associated) if associated&.persisted? && select_options.exclude?(associated)
32
+
33
+ if html_options.include?(:associated)
34
+ options[:selected] ||= html_options.delete(:associated)
35
+ else
36
+ associated = record.send(column.association.name)
37
+ select_options.unshift(associated) if associated&.persisted? && select_options.exclude?(associated)
38
+ options[:selected] ||= associated&.id
39
+ end
35
40
 
36
41
  method = column.name
37
- options.merge! selected: associated&.id, include_blank: as_(:_select_), object: record
42
+ options.reverse_merge! include_blank: as_(:_select_), object: record
38
43
 
39
44
  html_options.merge!(ui_options[:html_options] || {})
40
45
  options.merge!(ui_options)
41
- html_options.delete(:multiple) # no point using multiple in a form for singular assoc, but may be set for field search
46
+ active_scaffold_select_name_with_multiple html_options # doesn't make sense most of time, but AS_batch use it
42
47
  active_scaffold_translate_select_options(options)
43
48
 
44
49
  html =
@@ -101,7 +106,7 @@ module ActiveScaffold
101
106
  url_options[:parent_model] ||= record.class.name
102
107
  url_options[:parent_column] ||= column.name
103
108
  url_options.reverse_merge! options[:url_options] if options[:url_options]
104
- link_to(link_text, url_options, remote: true, data: {position: :popup}, class: 'as_action')
109
+ link_to(link_text, url_options, as_element_attributes(:field_add_new_popup, remote: true, data: {position: :popup}, class: 'as_action'))
105
110
  end
106
111
 
107
112
  def active_scaffold_new_record_subform(column, record, html_options, options: {}, new_record_attributes: nil, locals: {}, skip_link: false)
@@ -141,7 +146,7 @@ module ActiveScaffold
141
146
  create_new = active_scaffold_add_new_text(options, :add_new_text, :create_new)
142
147
  data = {select_id: select_id, subform_id: subform_id, subform_text: add_existing, select_text: create_new}
143
148
  label = data[record.send(column.name)&.new_record? ? :subform_text : :select_text]
144
- link_to(label, '#', data: data, class: 'show-new-subform')
149
+ link_to(label, '#', as_element_attributes(:field_add_new_subform, data: data, class: 'show-new-subform'))
145
150
  end
146
151
 
147
152
  def active_scaffold_file_with_content(column, options, content, remove_file_prefix, controls_class, ui_options: column.options)
@@ -237,8 +242,8 @@ module ActiveScaffold
237
242
  end
238
243
  end
239
244
 
240
- def active_scaffold_checkbox_option(option, label_method, associated_ids, checkbox_options, li_options = {})
241
- content_tag(:li, li_options) do
245
+ def active_scaffold_checkbox_option(option, label_method, associated_ids, checkbox_options, item_options = {})
246
+ content_tag(:li, item_options) do
242
247
  option_id = option.is_a?(Array) ? option[1] : option.id
243
248
  label = option.is_a?(Array) ? option[0] : option.send(label_method)
244
249
  check_box_tag(checkbox_options[:name], option_id, associated_ids.include?(option_id), checkbox_options) <<
@@ -261,7 +266,7 @@ module ActiveScaffold
261
266
  html << content_tag(:ul, options.merge(class: "#{options[:class]} checkbox-list#{' draggable-lists' if draggable}")) do
262
267
  content = []
263
268
  select_options.each_with_index do |option, i|
264
- content << active_scaffold_checkbox_option(option, label_method, associated_ids, name: "#{options[:name]}[]", id: "#{options[:id]}_#{i}_id")
269
+ content << active_scaffold_checkbox_option(option, label_method, associated_ids, {name: "#{options[:name]}[]", id: "#{options[:id]}_#{i}_id"}, ui_options[:item_options] || {})
265
270
  end
266
271
  safe_join content
267
272
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveScaffold
4
+ module Helpers
5
+ module FrameworkUiHelpers
6
+ def as_element(key, content = nil, proc_options: {}, **options, &)
7
+ tag, attributes = as_tag_and_attributes(key, options, proc_options)
8
+ content_tag(tag, content, attributes, &)
9
+ end
10
+
11
+ def as_element_attributes(key, proc_options: {}, **options)
12
+ as_tag_and_attributes(key, options, proc_options).last
13
+ end
14
+
15
+ def as_tag_and_attributes(key, options, proc_options = {})
16
+ tag, attributes, proc = ActiveScaffold.ui_elements[key]&.values_at(:tag, :attributes, :proc)
17
+ tag, attributes = instance_exec(proc_options, &proc) if proc
18
+ [tag, attributes&.smart_merge(options) || options&.as_html_attrs]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -72,6 +72,7 @@ module ActiveScaffold
72
72
  alias active_scaffold_human_condition_time active_scaffold_human_condition_datetime
73
73
  alias active_scaffold_human_condition_date active_scaffold_human_condition_datetime
74
74
  alias active_scaffold_human_condition_timestamp active_scaffold_human_condition_datetime
75
+ alias active_scaffold_human_condition_timestamptz active_scaffold_human_condition_datetime
75
76
 
76
77
  def active_scaffold_human_condition_datetime_range_format(range_type, range)
77
78
  case range