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.
- checksums.yaml +4 -4
- data/CHANGELOG.rdoc +26 -1
- data/README.md +1 -1
- data/app/assets/javascripts/active_scaffold/active_scaffold.js +107 -16
- data/app/assets/javascripts/active_scaffold/draggable_lists.js +4 -3
- data/app/assets/javascripts/active_scaffold/load.js +20 -5
- data/app/assets/stylesheets/active_scaffold/_images.scss +53 -1
- data/app/assets/stylesheets/active_scaffold/_layout.scss +1 -66
- data/app/views/active_scaffold_overrides/_add_existing_form.html.erb +17 -11
- data/app/views/active_scaffold_overrides/_base_form.html.erb +29 -19
- data/app/views/active_scaffold_overrides/_field_search.html.erb +9 -9
- data/app/views/active_scaffold_overrides/_field_search_columns.html.erb +29 -33
- data/app/views/active_scaffold_overrides/_form.html.erb +43 -34
- data/app/views/active_scaffold_overrides/_form_association.html.erb +3 -3
- data/app/views/active_scaffold_overrides/_form_association_footer.html.erb +20 -16
- data/app/views/active_scaffold_overrides/_form_association_record.html.erb +3 -4
- data/app/views/active_scaffold_overrides/_horizontal_subform_header.html.erb +2 -1
- data/app/views/active_scaffold_overrides/_list.html.erb +6 -4
- data/app/views/active_scaffold_overrides/_list_calculations.html.erb +1 -1
- data/app/views/active_scaffold_overrides/_list_header.html.erb +3 -3
- data/app/views/active_scaffold_overrides/_list_messages.html.erb +19 -19
- data/app/views/active_scaffold_overrides/_list_messages_content.html.erb +5 -5
- data/app/views/active_scaffold_overrides/_list_pagination.html.erb +7 -5
- data/app/views/active_scaffold_overrides/_list_pagination_links.html.erb +5 -3
- data/app/views/active_scaffold_overrides/_list_record.html.erb +13 -8
- data/app/views/active_scaffold_overrides/_list_with_header.html.erb +8 -8
- data/app/views/active_scaffold_overrides/_messages.html.erb +3 -3
- data/app/views/active_scaffold_overrides/_render_field.js.erb +8 -11
- data/app/views/active_scaffold_overrides/_search.html.erb +7 -6
- data/app/views/active_scaffold_overrides/_show.html.erb +4 -4
- data/app/views/active_scaffold_overrides/_show_actions.html.erb +2 -2
- data/app/views/active_scaffold_overrides/_show_columns.html.erb +28 -30
- data/app/views/active_scaffold_overrides/add_existing.js.erb +7 -4
- data/app/views/active_scaffold_overrides/add_tab.js.erb +2 -3
- data/app/views/active_scaffold_overrides/render_field.js.erb +1 -1
- data/config/locales/de.yml +2 -1
- data/config/locales/en.yml +33 -32
- data/config/locales/es.yml +15 -14
- data/config/locales/fr.yml +3 -2
- data/config/locales/hu.yml +3 -2
- data/config/locales/ja.yml +2 -1
- data/config/locales/ro.yml +169 -0
- data/config/locales/ru.yml +8 -7
- data/config/locales/ta.yml +7 -6
- data/config/locales/zh_Hans.yml +14 -13
- data/lib/active_scaffold/actions/common_search.rb +1 -14
- data/lib/active_scaffold/actions/core.rb +55 -16
- data/lib/active_scaffold/actions/field_search.rb +2 -2
- data/lib/active_scaffold/actions/list.rb +35 -13
- data/lib/active_scaffold/actions/nested.rb +31 -7
- data/lib/active_scaffold/actions/show.rb +1 -1
- data/lib/active_scaffold/active_record_permissions.rb +1 -1
- data/lib/active_scaffold/attribute_params.rb +2 -1
- data/lib/active_scaffold/bridges/date_picker/helper.rb +14 -0
- data/lib/active_scaffold/bridges/date_picker.rb +10 -0
- data/lib/active_scaffold/config/list.rb +10 -1
- data/lib/active_scaffold/core.rb +9 -6
- data/lib/active_scaffold/data_structures/action_columns.rb +2 -0
- data/lib/active_scaffold/data_structures/column.rb +27 -6
- data/lib/active_scaffold/data_structures/filter.rb +4 -0
- data/lib/active_scaffold/data_structures/nested_info.rb +8 -1
- data/lib/active_scaffold/data_structures/sorting.rb +1 -1
- data/lib/active_scaffold/extensions/action_view_rendering.rb +1 -1
- data/lib/active_scaffold/finder.rb +66 -21
- data/lib/active_scaffold/helpers/action_link_helpers.rb +43 -18
- data/lib/active_scaffold/helpers/assets_helpers.rb +4 -4
- data/lib/active_scaffold/helpers/filter_helpers.rb +3 -3
- data/lib/active_scaffold/helpers/form_column_helpers.rb +34 -22
- data/lib/active_scaffold/helpers/form_ui_helpers.rb +15 -10
- data/lib/active_scaffold/helpers/framework_ui_helpers.rb +22 -0
- data/lib/active_scaffold/helpers/human_condition_helpers.rb +1 -0
- data/lib/active_scaffold/helpers/list_column_helpers.rb +19 -0
- data/lib/active_scaffold/helpers/pagination_helpers.rb +28 -7
- data/lib/active_scaffold/helpers/search_column_helpers.rb +32 -5
- data/lib/active_scaffold/helpers/tabs_helpers.rb +4 -6
- data/lib/active_scaffold/helpers/view_helpers.rb +12 -0
- data/lib/active_scaffold/tableless.rb +11 -8
- data/lib/active_scaffold/version.rb +2 -2
- data/lib/active_scaffold.rb +169 -0
- data/lib/tasks/bundle.rake +2 -1
- data/lib/tasks/mock_app.rake +43 -0
- metadata +20 -7
- 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
|
-
|
|
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.
|
|
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
|
|
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,
|
|
241
|
-
content_tag(:li,
|
|
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
|
-
|
|
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 <<
|
|
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 <<
|
|
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
|
-
|
|
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 <<
|
|
83
|
+
html << pagination_gap(options) << pagination_ajax_link(current_page.number + offset, url_options, options)
|
|
63
84
|
end
|
|
64
85
|
else
|
|
65
|
-
html <<
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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(
|
|
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 ||= [' ', 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
data/lib/active_scaffold.rb
CHANGED
|
@@ -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'
|
data/lib/tasks/bundle.rake
CHANGED
|
@@ -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
|
-
|
|
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
|