active_scaffold 4.3.1 → 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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rdoc +26 -1
  3. data/README.md +1 -1
  4. data/app/assets/javascripts/active_scaffold/active_scaffold.js +107 -16
  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/add_tab.js.erb +2 -3
  35. data/app/views/active_scaffold_overrides/render_field.js.erb +1 -1
  36. data/config/locales/de.yml +2 -1
  37. data/config/locales/en.yml +33 -32
  38. data/config/locales/es.yml +15 -14
  39. data/config/locales/fr.yml +3 -2
  40. data/config/locales/hu.yml +3 -2
  41. data/config/locales/ja.yml +2 -1
  42. data/config/locales/ro.yml +169 -0
  43. data/config/locales/ru.yml +8 -7
  44. data/config/locales/ta.yml +7 -6
  45. data/config/locales/zh_Hans.yml +14 -13
  46. data/lib/active_scaffold/actions/common_search.rb +1 -14
  47. data/lib/active_scaffold/actions/core.rb +55 -16
  48. data/lib/active_scaffold/actions/field_search.rb +2 -2
  49. data/lib/active_scaffold/actions/list.rb +35 -13
  50. data/lib/active_scaffold/actions/nested.rb +31 -7
  51. data/lib/active_scaffold/actions/show.rb +1 -1
  52. data/lib/active_scaffold/active_record_permissions.rb +1 -1
  53. data/lib/active_scaffold/attribute_params.rb +2 -1
  54. data/lib/active_scaffold/bridges/date_picker/helper.rb +14 -0
  55. data/lib/active_scaffold/bridges/date_picker.rb +10 -0
  56. data/lib/active_scaffold/config/list.rb +10 -1
  57. data/lib/active_scaffold/core.rb +9 -6
  58. data/lib/active_scaffold/data_structures/action_columns.rb +2 -0
  59. data/lib/active_scaffold/data_structures/column.rb +27 -6
  60. data/lib/active_scaffold/data_structures/filter.rb +4 -0
  61. data/lib/active_scaffold/data_structures/nested_info.rb +8 -1
  62. data/lib/active_scaffold/data_structures/sorting.rb +1 -1
  63. data/lib/active_scaffold/extensions/action_view_rendering.rb +1 -1
  64. data/lib/active_scaffold/finder.rb +66 -21
  65. data/lib/active_scaffold/helpers/action_link_helpers.rb +43 -18
  66. data/lib/active_scaffold/helpers/assets_helpers.rb +4 -4
  67. data/lib/active_scaffold/helpers/filter_helpers.rb +3 -3
  68. data/lib/active_scaffold/helpers/form_column_helpers.rb +34 -22
  69. data/lib/active_scaffold/helpers/form_ui_helpers.rb +15 -10
  70. data/lib/active_scaffold/helpers/framework_ui_helpers.rb +22 -0
  71. data/lib/active_scaffold/helpers/human_condition_helpers.rb +1 -0
  72. data/lib/active_scaffold/helpers/list_column_helpers.rb +19 -0
  73. data/lib/active_scaffold/helpers/pagination_helpers.rb +28 -7
  74. data/lib/active_scaffold/helpers/search_column_helpers.rb +32 -5
  75. data/lib/active_scaffold/helpers/tabs_helpers.rb +4 -6
  76. data/lib/active_scaffold/helpers/view_helpers.rb +12 -0
  77. data/lib/active_scaffold/tableless.rb +11 -8
  78. data/lib/active_scaffold/version.rb +2 -2
  79. data/lib/active_scaffold.rb +169 -0
  80. data/lib/tasks/bundle.rake +2 -1
  81. data/lib/tasks/mock_app.rake +43 -0
  82. metadata +20 -7
  83. data/shoulda_macros/macros.rb +0 -145
@@ -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
@@ -431,6 +431,24 @@ module ActiveScaffold
431
431
  data
432
432
  end
433
433
 
434
+ def local_sorting?
435
+ @page&.pager&.number_of_pages == 1 && active_scaffold_config.list.local_sorting
436
+ end
437
+
438
+ def column_sort_value(record, column)
439
+ value = record.send(column.name)
440
+ return if value.nil?
441
+
442
+ case column.column.type
443
+ when :time, :datetime, Time then value.to_i
444
+ when :boolean then value ? 1 : 0
445
+ when :binary then nil
446
+ when :integer then value
447
+ when :float, :decimal then (value % 1).zero? ? value.to_i : value
448
+ else value.to_s
449
+ end
450
+ end
451
+
434
452
  # MARK
435
453
 
436
454
  def all_marked?
@@ -465,6 +483,7 @@ module ActiveScaffold
465
483
  elsif column.inplace_edit
466
484
  tag_options[:data] = inplace_edit_data(column)
467
485
  end
486
+
468
487
  content_tag(:th, column_heading_value(column, sorting, sort_direction) + inplace_edit_control(column), tag_options)
469
488
  end
470
489
 
@@ -3,8 +3,13 @@
3
3
  module ActiveScaffold
4
4
  module Helpers
5
5
  module PaginationHelpers
6
- def pagination_ajax_link(page_number, url_options, options)
7
- link_to page_number, url_options.merge(page: page_number), options.merge(class: 'as_paginate')
6
+ def pagination_ajax_link(page_number, url_options, options, text: nil)
7
+ attributes = as_element_attributes(
8
+ :pagination_link,
9
+ proc_options: {page_number: page_number, text: text},
10
+ **options.smart_merge(class: 'as_paginate')
11
+ )
12
+ link_to text ? as_(text) : page_number, url_options.merge(page: page_number), attributes
8
13
  end
9
14
 
10
15
  def pagination_url_options(url_options = nil)
@@ -17,6 +22,22 @@ module ActiveScaffold
17
22
  url_options
18
23
  end
19
24
 
25
+ def pagination_active_page(page_number, url_options, options)
26
+ attributes = options.slice(:id, :class).smart_merge(class: 'as_paginate current')
27
+ as_element :pagination_active_page, page_number,
28
+ proc_options: {page_number: page_number}, **attributes
29
+ end
30
+
31
+ def pagination_gap(options)
32
+ '..'
33
+ end
34
+
35
+ # Wraps the complete pagination sequence, including previous and next links.
36
+ # UI adapters may override this method and call +super+ to add nested markup.
37
+ def pagination_html(&)
38
+ capture(&)
39
+ end
40
+
20
41
  def pagination_ajax_links(current_page, url_options, options, inner_window, outer_window)
21
42
  start_number = current_page.number - inner_window
22
43
  end_number = current_page.number + inner_window
@@ -42,16 +63,16 @@ module ActiveScaffold
42
63
  page = current_page.number - offset
43
64
  next unless page < start_number && page > last_page
44
65
 
45
- html << '..' if page > last_page + 1
66
+ html << pagination_gap(options) if page > last_page + 1
46
67
  html << pagination_ajax_link(page, url_options, options)
47
68
  last_page = page
48
69
  end
49
70
  end
50
- html << '..' if start_number > last_page + 1
71
+ html << pagination_gap(options) if start_number > last_page + 1
51
72
 
52
73
  [start_number, last_page + 1].max.upto(end_number) do |num|
53
74
  html << if current_page.number == num
54
- content_tag(:span, num.to_s, class: 'as_paginate current')
75
+ pagination_active_page(num, url_options, options)
55
76
  else
56
77
  pagination_ajax_link(num, url_options, options)
57
78
  end
@@ -59,10 +80,10 @@ module ActiveScaffold
59
80
 
60
81
  if current_page.pager.infinite?
61
82
  offsets.each do |offset|
62
- html << '..' << pagination_ajax_link(current_page.number + offset, url_options, options)
83
+ html << pagination_gap(options) << pagination_ajax_link(current_page.number + offset, url_options, options)
63
84
  end
64
85
  else
65
- html << '..' unless end_number >= current_page.pager.last.number - outer_window - 1
86
+ html << pagination_gap(options) unless end_number >= current_page.pager.last.number - outer_window - 1
66
87
  [end_number + 1, current_page.pager.last.number - outer_window].max.upto(current_page.pager.last.number) do |num|
67
88
  html << pagination_ajax_link(num, url_options, options)
68
89
  end
@@ -50,14 +50,29 @@ module ActiveScaffold
50
50
 
51
51
  # the standard active scaffold options used for class, name and scope
52
52
  def active_scaffold_search_options(column)
53
- {name: "search[#{column.name}]", class: "#{column.name}-input", id: "search_#{column.name}", value: field_search_params[column.name.to_s]}
53
+ attributes = {
54
+ name: "search[#{column.name}]",
55
+ class: "#{column.name}-input",
56
+ id: "search_#{column.name}",
57
+ value: field_search_params[column.name.to_s]
58
+ }
59
+ as_element_attributes(:search_control, proc_options: {column: column, search_ui: column.search_ui}, **attributes)
54
60
  end
55
61
 
56
62
  def search_attribute(column, record)
57
63
  column_options = active_scaffold_search_options(column).merge(object: record)
58
- content_tag :dl do
59
- content_tag(:dt, label_tag(search_label_for(column, column_options), search_column_label(column, record))) <<
60
- content_tag(:dd, active_scaffold_search_for(column, column_options))
64
+ search_attribute_html(
65
+ column,
66
+ label_tag(search_label_for(column, column_options), search_column_label(column, record)),
67
+ active_scaffold_search_for(column, column_options)
68
+ )
69
+ end
70
+
71
+ def search_attribute_html(column, label, field)
72
+ context = {column: column}
73
+ as_element :search_attribute, proc_options: context do
74
+ as_element(:search_attribute_label, label, proc_options: context) <<
75
+ as_element(:search_attribute_field, field, proc_options: context)
61
76
  end
62
77
  end
63
78
 
@@ -156,7 +171,7 @@ module ActiveScaffold
156
171
  select_options << [as_(:true), true] # rubocop:disable Lint/BooleanSymbol
157
172
  select_options << [as_(:false), false] # rubocop:disable Lint/BooleanSymbol
158
173
 
159
- select_tag(options[:name], options_for_select(select_options, ActiveScaffold::Core.column_type_cast(options[:value], column.column)), id: options[:id])
174
+ select_tag(options[:name], options_for_select(select_options, ActiveScaffold::Core.column_type_cast(options[:value], column.column, column.model)), id: options[:id])
160
175
  end
161
176
  # we can't use checkbox ui because it's not possible to decide whether search for this field or not
162
177
  alias active_scaffold_search_checkbox active_scaffold_search_boolean
@@ -296,6 +311,7 @@ module ActiveScaffold
296
311
  def active_scaffold_search_timestamp(column, options, ui_options: column.options)
297
312
  active_scaffold_search_datetime(column, options, ui_options: ui_options, field_ui: :datetime)
298
313
  end
314
+ alias active_scaffold_search_timestamptz active_scaffold_search_timestamp
299
315
 
300
316
  def active_scaffold_search_time(column, options, ui_options: column.options)
301
317
  active_scaffold_search_datetime(column, options, ui_options: ui_options, field_ui: :time)
@@ -425,6 +441,17 @@ module ActiveScaffold
425
441
  [visibles, hiddens]
426
442
  end
427
443
 
444
+ def render_field_search_column(column, record)
445
+ column_css_class = column.css_class unless column.css_class.is_a?(Proc)
446
+ if column.respond_to? :each_column
447
+ as_element :field_search_subsection, class: column_css_class do
448
+ render_subsection column, record, nil, :field_search, partial: 'field_search_columns'
449
+ end
450
+ else
451
+ as_element :field_search_element, search_attribute(column, record), class: "form-element #{column_css_class}"
452
+ end
453
+ end
454
+
428
455
  def searched_by?(column)
429
456
  value = field_search_params[column.name.to_s]
430
457
  case value
@@ -6,11 +6,11 @@ module ActiveScaffold
6
6
  module TabsHelpers
7
7
  def active_scaffold_tabbed_by(column, record, scope, subsection_id, &)
8
8
  add_tab_url = params_for(action: 'render_field', tabbed_by: column.tabbed_by, id: record.to_param, column: column.label)
9
- refresh_opts = {refresh_link: {text: 'Add tab', class: 'refresh-link add-tab'}}
9
+ refresh_opts = {field_selector: "##{subsection_id}_input", refresh_link: {text: 'Add tab', class: 'refresh-link add-tab'}}
10
10
  tab_options = send(override_helper_per_model(:active_scaffold_tab_options, record.class), column, record)
11
11
  used_tabs = send(override_helper_per_model(:active_scaffold_current_tabs, record.class), column, record, tab_options)
12
12
  input_helper = override_helper_per_model(:active_scaffold_input_for_tabbed, record.class)
13
- send(input_helper, column, record, subsection_id, tab_options, used_tabs.map(&:first)) <<
13
+ send(input_helper, column, record, subsection_id, tab_options, used_tabs.map { |v, _| v }) <<
14
14
  active_scaffold_refresh_link(nil, {'data-update_url' => url_for(add_tab_url)}, record, refresh_opts) <<
15
15
  active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs, &)
16
16
  end
@@ -32,8 +32,6 @@ module ActiveScaffold
32
32
  tab_values = record.send(col.name).map(&tabbed_by).compact
33
33
  if tabbed_by_association(col, tabbed_by)
34
34
  tab_values.map! { |value| [value, value.id.to_s] }
35
- else
36
- tab_values.map! { |value| [tab_options.find { |_, tab_value, _| value == tab_value }&.first || value, value] }
37
35
  end
38
36
  used_choices.merge tab_values
39
37
  end
@@ -41,7 +39,7 @@ module ActiveScaffold
41
39
  end
42
40
 
43
41
  def active_scaffold_tab_options(column, record)
44
- subform_column = column.first
42
+ subform_column = column.visible_columns.first
45
43
  if subform_column
46
44
  tabbed_by = subform_column.options[:tabbed_by] || column.tabbed_by
47
45
  if tabbed_by_association(subform_column, tabbed_by)
@@ -51,7 +49,7 @@ module ActiveScaffold
51
49
  end
52
50
  if tab_column
53
51
  label_method = (tab_column.form_ui_options || tab_column.options)[:label_method] || :to_label
54
- helper_method = association_helper_method(column.association, :sorted_association_options_find)
52
+ helper_method = association_helper_method(tab_column.association, :sorted_association_options_find)
55
53
  send(helper_method, tab_column.association, nil, subform_record).map do |opt_record|
56
54
  [opt_record.send(label_method), opt_record.id, opt_record]
57
55
  end
@@ -18,6 +18,7 @@ module ActiveScaffold
18
18
  include ActiveScaffold::Helpers::SearchColumnHelpers
19
19
  include ActiveScaffold::Helpers::HumanConditionHelpers
20
20
  include ActiveScaffold::Helpers::FilterHelpers
21
+ include ActiveScaffold::Helpers::FrameworkUiHelpers
21
22
 
22
23
  ##
23
24
  ## Delegates
@@ -31,6 +32,12 @@ module ActiveScaffold
31
32
  ## Uncategorized
32
33
  ##
33
34
 
35
+ # UI adapters may override this method and call +super+ to wrap the table,
36
+ # for example in a responsive-table container.
37
+ def list_table_html(&)
38
+ capture(&)
39
+ end
40
+
34
41
  def handle_exception_on_column(exception, column, record = nil)
35
42
  raise exception if exception.instance_variable_get(:@_active_scaffold_wrapped)
36
43
 
@@ -126,6 +133,7 @@ module ActiveScaffold
126
133
  def column_heading_class(column, sorting)
127
134
  classes = "#{column.name}-column_heading "
128
135
  classes << "sorted #{sorting.direction_of(column).downcase} " if sorting.sorts_on? column
136
+ classes << 'skip-local-sorting' if column.skip_local_sorting?
129
137
  classes << column.css_class unless column.css_class.nil? || column.css_class.is_a?(Proc)
130
138
  classes
131
139
  end
@@ -140,6 +148,10 @@ module ActiveScaffold
140
148
  params[:eid] ? {eid: id_from_controller(params[:eid])} : {}
141
149
  end
142
150
 
151
+ def server_error_msg(content = nil, **attributes, &)
152
+ as_element :server_error, content, **attributes, &
153
+ end
154
+
143
155
  def column_empty?(column_value)
144
156
  @_empty_values ||= ['&nbsp;', empty_field_text].compact
145
157
  empty = column_value.nil?
@@ -24,16 +24,19 @@ class ActiveScaffold::Tableless < ActiveRecord::Base
24
24
 
25
25
  class Column < ActiveRecord::ConnectionAdapters::Column
26
26
  def initialize(name, default, type, null = true, *, **) # rubocop:disable Style/OptionalBooleanParameter,Metrics/ParameterLists
27
- if Rails.version >= '8.1'
28
- if type.is_a?(Symbol)
29
- cast_type = ActiveRecord::Type.lookup(type)
30
- else
31
- metadata = ActiveRecord::Base.connection.send :fetch_type_metadata, type
32
- cast_type = ActiveRecord::Type.lookup(metadata.type)
33
- end
27
+ connection = ActiveRecord::Base.connection
28
+ sql_type = type.is_a?(Symbol) ? connection.type_to_sql(type) : type.to_s
29
+ cast_type = connection.send(:lookup_cast_type, sql_type)
30
+ metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
31
+ sql_type: sql_type,
32
+ type: cast_type.type,
33
+ limit: cast_type.limit,
34
+ precision: cast_type.precision,
35
+ scale: cast_type.scale
36
+ )
37
+ if ActiveRecord.version >= Gem::Version.new('8.1')
34
38
  super(name, cast_type, default, metadata, null, *, **)
35
39
  else
36
- metadata = ActiveRecord::Base.connection.send :fetch_type_metadata, type.to_s
37
40
  super(name, default, metadata, null, *, **)
38
41
  end
39
42
  end
@@ -3,8 +3,8 @@
3
3
  module ActiveScaffold
4
4
  module Version
5
5
  MAJOR = 4
6
- MINOR = 3
7
- PATCH = 1
6
+ MINOR = 4
7
+ PATCH = 0
8
8
  FIX = nil
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, FIX].compact.join('.')
@@ -58,6 +58,174 @@ module ActiveScaffold
58
58
  mattr_accessor :javascripts, instance_writer: false
59
59
  self.javascripts = []
60
60
 
61
+ # A way to define the tags and html attributes used in different places.
62
+ # Changing the class or replacing the attributes may break the default AS stylesheets, but not JS or functionality.
63
+ #
64
+ # The keys in the hash are the names used by ActiveScaffold to identify the
65
+ # elements.
66
+ # The values are a hash of attributes used to create the markup, supporting the following keys:
67
+ # * The :tag key can be used to specify a different tag name for the element.
68
+ # * The :attributes key can be used to specify additional html attributes.
69
+ # * The :proc key can be used to specify a method to dinamically return the tag and attributes hash, as an array.
70
+ # When :proc is present, :tag and :attributes won't be used. The proc will receive an options hash, which usually
71
+ # will be a hash with html attributes, but in some cases may have other options:
72
+ # * For :action_link_group the hash will have keys such as :level, :first_action, and other options supported by
73
+ # +display_action_links+
74
+ # * For :action_link_separator the hash will have keys such as :level, :level_0_tag, and other options supported by
75
+ # +display_action_links+
76
+ # * Form and search elements receive the column and the configured UI in the options hash.
77
+ # * Action link elements receive the link, record, rendering options, and authorized state.
78
+ # * Pagination elements receive the page number and, for links, the optional text key.
79
+ # Some keys don't have a :tag key, because only attributes are used, the tag is hardcoded, usually elements that are
80
+ # rendered as a form or table, or a table's element (thead, tbody, tr, td, th, tfoot, etc.). For those elements,
81
+ # the :tag key will be ignored if it's set, so the tag can't be changed.
82
+ mattr_reader :ui_elements
83
+ @@ui_elements ||= {
84
+ list: {tag: :div},
85
+ list_header: {tag: :div},
86
+ list_title: {tag: :h2},
87
+ list_actions: {tag: :div},
88
+ filters: {tag: :div, attributes: {class: 'filters'}},
89
+ before_header_table: {tag: :table},
90
+ list_content: {tag: :div},
91
+ list_table: {}, # table
92
+ list_table_head: {}, # thead
93
+ list_table_head_row: {}, # tr
94
+ list_records: {}, # tbody
95
+ list_messages: {attributes: {class: 'messages'}}, # tbody
96
+ list_messages_container: {}, # td
97
+ list_action_messages: {tag: :div},
98
+ filtered_message: {tag: :div, attributes: {class: 'filtered-message'}},
99
+ applied_filters_message: {tag: :div, attributes: {class: 'filtered-message applied-filters'}},
100
+ empty_message: {tag: :p},
101
+ message_reset: {tag: :div, attributes: {class: 'reset'}},
102
+ message_close: {tag: :a, attributes: {href: '#', class: 'close'}},
103
+ server_error: {tag: :p, attributes: {class: 'error-message message server-error'}},
104
+ info_message: {tag: :div, attributes: {class: 'info-message message'}},
105
+ warning_message: {tag: :div, attributes: {class: 'warning-message message'}},
106
+ error_message: {tag: :div, attributes: {class: 'error-message message'}},
107
+ list_footer: {tag: :div, attributes: {class: 'active-scaffold-footer'}},
108
+ list_found: {tag: :div},
109
+ list_found_count: {tag: :span},
110
+ list_calculations: {}, # tr
111
+ pagination_links: {tag: :div},
112
+ pagination_link: {}, # a
113
+ pagination_active_page: {tag: :span},
114
+ record_actions_cell: {}, # td
115
+ record_action_links: {}, # table
116
+ action_link_group: {
117
+ proc: lambda do |options|
118
+ if options[:level]&.zero?
119
+ tag = :div
120
+ attributes = {class: 'action_group'}
121
+ else
122
+ tag = :li
123
+ attributes = {class: ('top' if options[:first_action]).to_s}
124
+ end
125
+ attributes[:class] += ' hover_click' if hover_via_click?
126
+ [tag, attributes]
127
+ end
128
+ },
129
+ action_link_separator: {
130
+ proc: lambda do |options|
131
+ tag = options[:level_0_tag] || :a if options[:level]&.zero?
132
+ [tag || :li, {class: 'separator'}]
133
+ end
134
+ },
135
+ action_link_group_title: {tag: :div},
136
+ action_link_group_content: {tag: :ul},
137
+ action_link: {}, # a
138
+ action_link_item: {tag: :li},
139
+ dynamic_action_group: {tag: :ul, attributes: {class: 'dynamic-menu'}},
140
+ dynamic_action_group_element: {tag: :li},
141
+ search_form: {}, # form
142
+ search_field: {attributes: {class: 'text-input', size: 50, autocomplete: :off}}, # input type=search
143
+ search_control: {}, # input, select or textarea
144
+ search_submit: {attributes: {class: 'submit'}}, # input type=submit
145
+ search_reset: {attributes: {class: 'reset'}}, # a
146
+ field_search_form: {}, # form
147
+ field_search_fields_columns: {tag: :div, attributes: {class: 'form-columns'}},
148
+ field_search_fields_container: {tag: :ol},
149
+ field_search_subsection: {tag: :li, attributes: {class: 'sub-section'}},
150
+ field_search_element: {tag: :li},
151
+ field_search_submit: {attributes: {class: 'submit'}}, # input type=submit
152
+ field_search_reset: {attributes: {class: 'reset'}}, # a
153
+ field_search_footer: {tag: :p},
154
+ form: {}, # form
155
+ form_title: {tag: :h4},
156
+ form_messages_container: {tag: :div},
157
+ form_fields_columns: {tag: :div, attributes: {class: 'form-columns'}},
158
+ fields_container: {tag: :ol},
159
+ form_subsection: {tag: :li, attributes: {class: 'sub-section'}},
160
+ form_subsection_header: {tag: :h5},
161
+ subform: {tag: :li},
162
+ subform_header: {tag: :h5},
163
+ subform_footer: {tag: :div, attributes: {class: 'footer'}},
164
+ subform_record_remove: {}, # a
165
+ subform_record_remove_reason: {tag: :span, attributes: {class: 'destroy reason'}},
166
+ subform_create_another: {}, # a
167
+ subform_replace_with_new: {}, # a
168
+ subform_add_existing: {}, # a
169
+ form_element: {tag: :li},
170
+ form_attribute: {tag: :dl},
171
+ form_attribute_label: {tag: :dt},
172
+ form_attribute_field: {tag: :dd},
173
+ search_attribute: {tag: :dl},
174
+ search_attribute_label: {tag: :dt},
175
+ search_attribute_field: {tag: :dd},
176
+ form_control: {}, # input, select or textarea
177
+ form_field_description: {tag: :span, attributes: {class: 'description'}},
178
+ form_field_description_close: {tag: :span, attributes: {class: 'close'}},
179
+ form_footer: {tag: :p},
180
+ form_submit: {attributes: {class: 'submit'}}, # input type=submit
181
+ form_apply: {attributes: {class: 'submit'}}, # input type=submit
182
+ form_cancel: {}, # a
183
+ field_add_new_subform: {},
184
+ field_add_new_popup: {},
185
+ show_title: {tag: :h4},
186
+ show_actions: {tag: :div},
187
+ show_columns_block: {tag: :dl},
188
+ show_label_block: {tag: :dt},
189
+ show_columns_group: {tag: :dd},
190
+ show_value_block: {tag: :dd},
191
+ show_footer: {tag: :p, attributes: {class: 'form-footer'}},
192
+ show_cancel: {} # a
193
+ }
194
+
195
+ def self.set_element_tag(name, tag)
196
+ (ui_elements[name] ||= {})[:tag] = tag
197
+ end
198
+
199
+ # Replace an element's complete definition. Use a block instead of +tag+ and
200
+ # +attributes+ to define a dynamic element.
201
+ def self.set_element(name, tag: nil, attributes: nil, &proc)
202
+ if proc && (tag || attributes)
203
+ raise ArgumentError, 'tag and attributes cannot be used with a proc'
204
+ end
205
+
206
+ ui_elements[name] = if proc
207
+ {proc: proc}
208
+ else
209
+ {}.tap do |element|
210
+ element[:tag] = tag if tag
211
+ element[:attributes] = attributes.as_html_attrs if attributes
212
+ end
213
+ end
214
+ end
215
+
216
+ def self.set_element_proc(name, &block)
217
+ (ui_elements[name] ||= {})[:proc] = block
218
+ end
219
+
220
+ def self.set_element_attributes(name, attributes)
221
+ (ui_elements[name] ||= {})[:attributes] = attributes.as_html_attrs
222
+ end
223
+
224
+ def self.add_element_attributes(name, attributes)
225
+ attrs = (ui_elements[name] ||= {})[:attributes] || {}
226
+ ui_elements[name][:attributes] = attrs.smart_merge(attributes)
227
+ end
228
+
61
229
  def self.threadsafe!; end
62
230
 
63
231
  mattr_writer :jquery_ui_loaded, instance_writer: false
@@ -106,3 +274,4 @@ require 'active_scaffold/engine'
106
274
  require 'ice_nine'
107
275
  require 'ice_nine/core_ext/object'
108
276
  require 'request_store'
277
+ require 'html_attrs'
@@ -16,7 +16,8 @@ namespace :bundle do
16
16
  gemfiles.each do |gemfile|
17
17
  puts "Locking #{File.basename(gemfile)}..."
18
18
  Bundler.with_unbundled_env do
19
- system("bundle lock --gemfile='#{gemfile}'")
19
+ command = ['bundle', 'lock', "--gemfile=#{gemfile}", '--add-platform', 'ruby']
20
+ system(*command, exception: true)
20
21
  end
21
22
  end
22
23
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :mock_app do
4
+ desc 'Build the mock app stylesheets'
5
+ task :assets do
6
+ environment = {
7
+ 'BUNDLE_GEMFILE' => File.expand_path(ENV.fetch('BUNDLE_GEMFILE', 'Gemfile'))
8
+ }
9
+ Dir.chdir('test/mock_app') do
10
+ system(environment, 'rails', 'dartsass:build', exception: true)
11
+ end
12
+ end
13
+
14
+ desc 'Start the mock_app server and Dart Sass watcher (default port 3000)'
15
+ task :server do
16
+ args = ARGV.drop_while { |a| a != '--' }.drop(1)
17
+ Dir.chdir('test/mock_app') do
18
+ environment = {'BUNDLE_GEMFILE' => File.expand_path('../../Gemfile', __dir__)}
19
+ system(environment, 'rails', 'dartsass:build', exception: true)
20
+ watcher = Process.spawn(environment, 'rails', 'dartsass:watch')
21
+ _pid, status = Process.wait2(Process.spawn(environment, 'rails', 'server', *args))
22
+ exit status.exitstatus
23
+ ensure
24
+ begin
25
+ if watcher
26
+ Process.kill('TERM', watcher)
27
+ Process.wait(watcher)
28
+ end
29
+ rescue Errno::ECHILD, Errno::ESRCH
30
+ # The watcher already exited.
31
+ end
32
+ end
33
+ end
34
+
35
+ desc 'Start the mock_app console'
36
+ task :console do
37
+ args = ARGV.drop_while { |a| a != '--' }.drop(1)
38
+ Dir.chdir('test/mock_app') do
39
+ exec({'BUNDLE_GEMFILE' => File.expand_path('../../Gemfile', __dir__)},
40
+ 'rails', 'console', *args)
41
+ end
42
+ end
43
+ end