From 806a181bfa4c8944b4fcb5ef25a326aeda5f3278 Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Thu, 10 Oct 2024 14:12:13 +0200 Subject: [PATCH 01/10] support operators =, null and not null for :select search_ui, so it's possible to search for NULL with :select search UI. --- CHANGELOG.rdoc | 1 + lib/active_scaffold/finder.rb | 15 +++-- .../helpers/human_condition_helpers.rb | 7 ++ .../helpers/search_column_helpers.rb | 65 +++++++++++++------ 4 files changed, 62 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index baad3adeb..6cb948d10 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,4 +1,5 @@ - Rollback previous behaviour when submitting empty values, broken when default_value was added. Default value set in column is not used when trying to save empty value, DB default is used in that case, and save NULL when string is empty, as before. +- Support operators =, null and not null for :select search_ui, so it's possible to search for NULL with :select search UI. = 3.7.7 - Fix usage with mongoid, broken on 3.7.5 diff --git a/lib/active_scaffold/finder.rb b/lib/active_scaffold/finder.rb index 19ba582e7..aca5ca2c5 100644 --- a/lib/active_scaffold/finder.rb +++ b/lib/active_scaffold/finder.rb @@ -155,6 +155,7 @@ def subquery_condition(column, sql, options, values) end def condition_for_search_ui(column, value, like_pattern, search_ui) + Rails.logger.debug "param for #{column.name} #{value.inspect} #{value.is_a? Hash}" case search_ui when :boolean, :checkbox if value == 'null' @@ -164,13 +165,17 @@ def condition_for_search_ui(column, value, like_pattern, search_ui) end when :integer, :decimal, :float condition_for_numeric(column, value) - when :string, :range - condition_for_range(column, value, like_pattern) when :date, :time, :datetime, :timestamp condition_for_datetime(column, value) + when :string, :range + condition_for_range(column, value, like_pattern) when :select, :select_multiple, :draggable, :multi_select, :country, :usa_state, :chosen, :multi_chosen - values = Array(value).select(&:present?) - ['%s in (?)', values] if values.present? + if value.is_a?(Hash) + condition_for_range(column, value, like_pattern) + else + values = Array(value).select(&:present?) + ['%s in (?)', values] if values.present? + end else if column.text? value = column.active_record? ? column.active_record_class.sanitize_sql_like(value) : value @@ -178,7 +183,7 @@ def condition_for_search_ui(column, value, like_pattern, search_ui) else ['%s = ?', ActiveScaffold::Core.column_type_cast(value, column.column)] end - end + end.tap{|v|Rails.logger.debug "conditions for #{column.name}: #{v.inspect}"} end def condition_for_numeric(column, value) diff --git a/lib/active_scaffold/helpers/human_condition_helpers.rb b/lib/active_scaffold/helpers/human_condition_helpers.rb index 7c29b27f7..a0ab74ec0 100644 --- a/lib/active_scaffold/helpers/human_condition_helpers.rb +++ b/lib/active_scaffold/helpers/human_condition_helpers.rb @@ -104,6 +104,13 @@ def active_scaffold_human_condition_null(column, value) def active_scaffold_human_condition_select(column, associated) attribute = column.active_record_class.human_attribute_name(column.name) + if associated.is_a?(Hash) + if associated['opt'] == '=' + associated = associated['from'] + else + return active_scaffold_human_condition_range(column, associated) + end + end associated = [associated].compact unless associated.is_a? Array if column.association method = column.options[:label_method] || :to_label diff --git a/lib/active_scaffold/helpers/search_column_helpers.rb b/lib/active_scaffold/helpers/search_column_helpers.rb index 877a23940..1628b6d92 100644 --- a/lib/active_scaffold/helpers/search_column_helpers.rb +++ b/lib/active_scaffold/helpers/search_column_helpers.rb @@ -54,7 +54,7 @@ def active_scaffold_search_for(column, options = nil) # the standard active scaffold options used for class, name and scope def active_scaffold_search_options(column) - {:name => "search[#{column.name}]", :class => "#{column.name}-input", :id => "search_#{column.name}", :value => field_search_params[column.name.to_s]} + {name: "search[#{column.name}]", class: "#{column.name}-input", id: "search_#{column.name}", value: field_search_params[column.name.to_s]} end def search_attribute(column, record) @@ -98,6 +98,13 @@ def active_scaffold_search_multi_select(column, options, ui_options: column.opti def active_scaffold_search_select(column, html_options, options = {}, ui_options: column.options) record = html_options.delete(:object) associated = html_options.delete :value + if include_null_comparators?(column) + range_opts = html_options.slice(:name, :id) + range_opts[:opt_value], associated, _ = field_search_params_range_values(column) + operators = active_scaffold_search_select_comparator_options(column, ui_options: ui_options) + html_options[:name] += '[from]' + end + if column.association associated = associated.is_a?(Array) ? associated.map(&:to_i) : associated.to_i unless associated.nil? method = column.association.belongs_to? ? column.association.foreign_key : column.name @@ -119,13 +126,16 @@ def active_scaffold_search_select(column, html_options, options = {}, ui_options active_scaffold_translate_select_options(options) end - if (optgroup = options.delete(:optgroup)) - select(:record, method, active_scaffold_grouped_options(column, select_options, optgroup), options, html_options) - elsif column.association - collection_select(:record, method, select_options, :id, ui_options[:label_method] || :to_label, options, html_options) - else - select(:record, method, select_options, options, html_options) - end + select = + if (optgroup = options.delete(:optgroup)) + select(:record, method, active_scaffold_grouped_options(column, select_options, optgroup), options, html_options) + elsif column.association + collection_select(:record, method, select_options, :id, ui_options[:label_method] || :to_label, options, html_options) + else + select(:record, method, select_options, options, html_options) + end + + operators ? build_active_scaffold_search_range_ui(operators, select, **range_opts) : select end def active_scaffold_search_select_multiple(column, options, ui_options: column.options) @@ -207,6 +217,12 @@ def active_scaffold_search_range_comparator_options(column, ui_options: column.o select_options end + def active_scaffold_search_select_comparator_options(column, ui_options: column.options) + select_options = [[as_('='.to_sym), '=']] + select_options.concat(ActiveScaffold::Finder::NULL_COMPARATORS.collect { |comp| [as_(comp), comp] }) + select_options + end + def include_null_comparators?(column, ui_options: column.options) return ui_options[:null_comparators] if ui_options.key? :null_comparators if column.association @@ -219,30 +235,37 @@ def include_null_comparators?(column, ui_options: column.options) def active_scaffold_search_range(column, options, input_method = :text_field_tag, input_options = {}, ui_options: column.options) opt_value, from_value, to_value = field_search_params_range_values(column) - select_options = active_scaffold_search_range_comparator_options(column, ui_options: ui_options) + operators = active_scaffold_search_range_comparator_options(column, ui_options: ui_options) text_field_size = active_scaffold_search_range_string?(column) ? 15 : 10 - opt_value ||= select_options[0][1] from_value = controller.class.condition_value_for_numeric(column, from_value) to_value = controller.class.condition_value_for_numeric(column, to_value) from_value = format_number_value(from_value, ui_options) if from_value.is_a?(Numeric) to_value = format_number_value(to_value, ui_options) if to_value.is_a?(Numeric) - html = select_tag("#{options[:name]}[opt]", options_for_select(select_options, opt_value), - :id => "#{options[:id]}_opt", :class => 'as_search_range_option') from_options = active_scaffold_input_text_options(input_options.merge(:id => options[:id], :size => text_field_size)) to_options = from_options.merge(:id => "#{options[:id]}_to") - html << content_tag('span', :id => "#{options[:id]}_numeric", :style => ActiveScaffold::Finder::NULL_COMPARATORS.include?(opt_value) ? 'display: none' : nil) do - send(input_method, "#{options[:name]}[from]", from_value, input_options) << - content_tag( - :span, - safe_join([' - ', send(input_method, "#{options[:name]}[to]", to_value, to_options)]), - :id => "#{options[:id]}_between", :class => 'as_search_range_between', :style => ('display: none' unless opt_value == 'BETWEEN') - ) - end - content_tag :span, html, :class => 'search_range' + + from_field = send(input_method, "#{options[:name]}[from]", from_value, input_options) + to_field = send(input_method, "#{options[:name]}[to]", to_value, to_options) + build_active_scaffold_search_range_ui(operators, from_field, to_field, opt_value: opt_value, **options.slice(:name, :id)) end alias active_scaffold_search_string active_scaffold_search_range + def build_active_scaffold_search_range_ui(operators, from, to = nil, name:, id:, opt_value: nil) + opt_value ||= operators[0][1] + html = select_tag("#{name}[opt]", options_for_select(operators, opt_value), + id: "#{id}_opt", class: 'as_search_range_option') + if to + from << content_tag( + :span, + safe_join([' - ', to]), + id: "#{id}_between", class: 'as_search_range_between', style: ('display: none' unless opt_value == 'BETWEEN') + ) + end + html << content_tag('span', from, id: "#{id}_numeric", style: ActiveScaffold::Finder::NULL_COMPARATORS.include?(opt_value) ? 'display: none' : nil) + content_tag :span, html, class: 'search_range' + end + def active_scaffold_search_integer(column, options, ui_options: column.options) number_opts = ui_options.slice(:step, :min, :max).reverse_merge(step: '1') active_scaffold_search_range(column, options, :number_field_tag, number_opts, ui_options: ui_options) From 2642e489ca3fcb86e7efc323ad502c58c604723f Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Thu, 10 Oct 2024 14:29:49 +0200 Subject: [PATCH 02/10] fix rubocop offenses, remove debug code --- lib/active_scaffold/finder.rb | 7 +++---- lib/active_scaffold/helpers/search_column_helpers.rb | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/active_scaffold/finder.rb b/lib/active_scaffold/finder.rb index aca5ca2c5..a99d6dc8e 100644 --- a/lib/active_scaffold/finder.rb +++ b/lib/active_scaffold/finder.rb @@ -155,7 +155,6 @@ def subquery_condition(column, sql, options, values) end def condition_for_search_ui(column, value, like_pattern, search_ui) - Rails.logger.debug "param for #{column.name} #{value.inspect} #{value.is_a? Hash}" case search_ui when :boolean, :checkbox if value == 'null' @@ -165,10 +164,10 @@ def condition_for_search_ui(column, value, like_pattern, search_ui) end when :integer, :decimal, :float condition_for_numeric(column, value) - when :date, :time, :datetime, :timestamp - condition_for_datetime(column, value) when :string, :range condition_for_range(column, value, like_pattern) + when :date, :time, :datetime, :timestamp + condition_for_datetime(column, value) when :select, :select_multiple, :draggable, :multi_select, :country, :usa_state, :chosen, :multi_chosen if value.is_a?(Hash) condition_for_range(column, value, like_pattern) @@ -183,7 +182,7 @@ def condition_for_search_ui(column, value, like_pattern, search_ui) else ['%s = ?', ActiveScaffold::Core.column_type_cast(value, column.column)] end - end.tap{|v|Rails.logger.debug "conditions for #{column.name}: #{v.inspect}"} + end end def condition_for_numeric(column, value) diff --git a/lib/active_scaffold/helpers/search_column_helpers.rb b/lib/active_scaffold/helpers/search_column_helpers.rb index 1628b6d92..a24a220b7 100644 --- a/lib/active_scaffold/helpers/search_column_helpers.rb +++ b/lib/active_scaffold/helpers/search_column_helpers.rb @@ -100,7 +100,7 @@ def active_scaffold_search_select(column, html_options, options = {}, ui_options associated = html_options.delete :value if include_null_comparators?(column) range_opts = html_options.slice(:name, :id) - range_opts[:opt_value], associated, _ = field_search_params_range_values(column) + range_opts[:opt_value], associated, = field_search_params_range_values(column) operators = active_scaffold_search_select_comparator_options(column, ui_options: ui_options) html_options[:name] += '[from]' end From 7c03d1b1cada79f564cc476ff2f25bd6fbbcfb3a Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Thu, 10 Oct 2024 14:31:01 +0200 Subject: [PATCH 03/10] fix rubocop offenses --- lib/active_scaffold/helpers/human_condition_helpers.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/active_scaffold/helpers/human_condition_helpers.rb b/lib/active_scaffold/helpers/human_condition_helpers.rb index a0ab74ec0..14f46c2da 100644 --- a/lib/active_scaffold/helpers/human_condition_helpers.rb +++ b/lib/active_scaffold/helpers/human_condition_helpers.rb @@ -105,11 +105,8 @@ def active_scaffold_human_condition_null(column, value) def active_scaffold_human_condition_select(column, associated) attribute = column.active_record_class.human_attribute_name(column.name) if associated.is_a?(Hash) - if associated['opt'] == '=' - associated = associated['from'] - else - return active_scaffold_human_condition_range(column, associated) - end + return active_scaffold_human_condition_range(column, associated) unless associated['opt'] == '=' + associated = associated['from'] end associated = [associated].compact unless associated.is_a? Array if column.association From 8b285f4cb7b9382da67038452bf7f0864394eec7 Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Fri, 18 Oct 2024 11:26:02 +0200 Subject: [PATCH 04/10] bump to 3.7.8 --- CHANGELOG.rdoc | 1 + Gemfile.lock | 2 +- gemfiles/Gemfile.rails-5.2.x.lock | 2 +- gemfiles/Gemfile.rails-6.0.x.lock | 2 +- gemfiles/Gemfile.rails-6.1.x.lock | 2 +- lib/active_scaffold/version.rb | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 6cb948d10..89c76ca36 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,4 @@ += 3.7.8 - Rollback previous behaviour when submitting empty values, broken when default_value was added. Default value set in column is not used when trying to save empty value, DB default is used in that case, and save NULL when string is empty, as before. - Support operators =, null and not null for :select search_ui, so it's possible to search for NULL with :select search UI. diff --git a/Gemfile.lock b/Gemfile.lock index 5887463c4..1081c7eb7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GIT PATH remote: . specs: - active_scaffold (3.7.7) + active_scaffold (3.7.8) cow_proxy (~> 0.3) ice_nine (~> 0.11) rails (>= 5.2.0) diff --git a/gemfiles/Gemfile.rails-5.2.x.lock b/gemfiles/Gemfile.rails-5.2.x.lock index ac7b95937..a73c5b0c6 100644 --- a/gemfiles/Gemfile.rails-5.2.x.lock +++ b/gemfiles/Gemfile.rails-5.2.x.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - active_scaffold (3.7.7) + active_scaffold (3.7.8) cow_proxy (~> 0.3) ice_nine (~> 0.11) rails (>= 5.2.0) diff --git a/gemfiles/Gemfile.rails-6.0.x.lock b/gemfiles/Gemfile.rails-6.0.x.lock index 7233f8f6d..dcb3af26f 100644 --- a/gemfiles/Gemfile.rails-6.0.x.lock +++ b/gemfiles/Gemfile.rails-6.0.x.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - active_scaffold (3.7.7) + active_scaffold (3.7.8) cow_proxy (~> 0.3) ice_nine (~> 0.11) rails (>= 5.2.0) diff --git a/gemfiles/Gemfile.rails-6.1.x.lock b/gemfiles/Gemfile.rails-6.1.x.lock index 82483869f..2ad7cf997 100644 --- a/gemfiles/Gemfile.rails-6.1.x.lock +++ b/gemfiles/Gemfile.rails-6.1.x.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - active_scaffold (3.7.7) + active_scaffold (3.7.8) cow_proxy (~> 0.3) ice_nine (~> 0.11) rails (>= 5.2.0) diff --git a/lib/active_scaffold/version.rb b/lib/active_scaffold/version.rb index ccd26e197..6617c8327 100644 --- a/lib/active_scaffold/version.rb +++ b/lib/active_scaffold/version.rb @@ -2,7 +2,7 @@ module ActiveScaffold module Version MAJOR = 3 MINOR = 7 - PATCH = 7 + PATCH = 8 FIX = nil STRING = [MAJOR, MINOR, PATCH, FIX].compact.join('.') From 8a738a83d25fe3afb6b5628da4539b21e9cb65b3 Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Fri, 18 Oct 2024 15:40:52 +0200 Subject: [PATCH 05/10] Add tabbed_by to group of columns in forms --- CHANGELOG.rdoc | 2 + .../active_scaffold_overrides/_form.html.erb | 25 ++-- .../_form_association.html.erb | 31 +++-- .../_form_association_footer.html.erb | 4 +- .../_horizontal_subform.html.erb | 2 +- .../_vertical_subform.html.erb | 2 +- .../active_scaffold_overrides/add_tab.js.erb | 15 +++ lib/active_scaffold/actions/core.rb | 21 +++- .../data_structures/action_columns.rb | 5 + .../helpers/controller_helpers.rb | 2 +- .../helpers/form_column_helpers.rb | 116 ++++++++++++++++-- lib/active_scaffold/helpers/id_helpers.rb | 4 +- 12 files changed, 187 insertions(+), 42 deletions(-) create mode 100644 app/views/active_scaffold_overrides/add_tab.js.erb diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 89c76ca36..a6cbc5194 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,5 @@ +- Add tabbed_by to group of columns in forms. The columns in the group must be collection associations, with a common column or association, used to partition them in different tabs. + = 3.7.8 - Rollback previous behaviour when submitting empty values, broken when default_value was added. Default value set in column is not used when trying to save empty value, DB default is used in that case, and save NULL when string is empty, as before. - Support operators =, null and not null for :select search_ui, so it's possible to search for NULL with :select search UI. diff --git a/app/views/active_scaffold_overrides/_form.html.erb b/app/views/active_scaffold_overrides/_form.html.erb index a9fc57f8d..a536c3fe8 100644 --- a/app/views/active_scaffold_overrides/_form.html.erb +++ b/app/views/active_scaffold_overrides/_form.html.erb @@ -1,31 +1,28 @@ <% scope ||= nil subsection_id ||= nil + tab_value ||= nil + tabbed_by ||= nil + tab_id ||= nil show_unauthorized_columns = active_scaffold_config.send(form_action).show_unauthorized_columns if active_scaffold_config.actions.include? form_action %>
    <%= "style=\"display: none;\"".html_safe if columns.collapsed %>> <% columns.each_column(for: @record, crud_type: (:read if show_unauthorized_columns)) do |column| %> <% column_css_class = column.css_class unless column.css_class.nil? || column.css_class.is_a?(Proc) %> <% renders_as = column_renders_as(column) %> - <% authorized = show_unauthorized_columns || renders_as == :subsection ? @record.authorized_for?(:crud_type => form_action, :column => column.name) : true %> - <% if renders_as == :subsection -%> - <% if authorized %> - <% subsection_id = sub_section_id(:sub_section => column.label) %> -
  1. -
    - <%= column.label %> - <%= link_to_visibility_toggle(subsection_id, {:default_visible => !column.collapsed}) -%> -
    - <%= render :partial => 'form', :locals => { :columns => column, :subsection_id => subsection_id, :form_action => form_action, :scope => scope } %> + <% authorized = show_unauthorized_columns || renders_as == :subsection ? @record.authorized_for?(crud_type: form_action, column: column.name) : true %> + <% if renders_as == :subsection || renders_as == :tabbed -%> + <% next unless authorized %> +
  2. + <%= render_subsection(column, @record, scope, form_action) %>
  3. - <% end %> <% elsif renders_as == :subform and authorized -%> - <%= content_tag :li, active_scaffold_subform_attributes(column, column_css_class) do %> - <%= render_column(column, @record, renders_as, scope) %> + <%= content_tag :li, active_scaffold_subform_attributes(column, column_css_class, tab_id: tab_id) do %> + <%= render_column(column, @record, renders_as, scope, tabbed_by: (column.options[:tabbed_by] || tabbed_by if tabbed_by), tab_value: tab_value, tab_id: tab_id) %> <% end %> <% else -%>
  4. - <%= render_column(column, @record, renders_as, scope, !authorized) %> + <%= render_column(column, @record, renders_as, scope, only_value: !authorized) %>
  5. <% end -%> <% end -%> diff --git a/app/views/active_scaffold_overrides/_form_association.html.erb b/app/views/active_scaffold_overrides/_form_association.html.erb index cbb829217..d11690419 100644 --- a/app/views/active_scaffold_overrides/_form_association.html.erb +++ b/app/views/active_scaffold_overrides/_form_association.html.erb @@ -1,15 +1,24 @@ <% -associated = column.association.singular? ? [parent_record.send(column.name)].compact : parent_record.send(column.name).to_a -if column.show_blank_record?(associated) - show_blank_record = build_associated(column.association, parent_record) -end -disable_required_for_new = @disable_required_for_new -@disable_required_for_new = !!show_blank_record unless (column.association.singular? && column.required?(action_for_validation?(parent_record))) -subform_div_id = "#{sub_form_id(:association => column.name, :id => parent_record.id || generated_id(parent_record) || 99999999999)}-div" + tabbed_by ||= nil + tab_value ||= nil + tab_id ||= nil + associated = column.association.singular? ? [parent_record.send(column.name)].compact : parent_record.send(column.name).to_a + associated = associated.select { |record| record.send(tabbed_by) == tab_value } if tabbed_by && tab_value + if column.show_blank_record?(associated) + show_blank_record = build_associated(column.association, parent_record) do |blank_record| + blank_record.send("#{tabbed_by}=", tab_value) if tabbed_by && tab_value + end + end + if column == :labor_line_items + @financial_months = (associated.first || show_blank_record)&.monthly_task_costs.map(&:financial_month) + end + disable_required_for_new = @disable_required_for_new + @disable_required_for_new = !!show_blank_record unless (column.association.singular? && column.required?(action_for_validation?(parent_record))) + subform_div_id = "#{sub_form_id(association: column.name, tab_id: tab_id, id: parent_record.id || generated_id(parent_record) || 99999999999)}-div" -# render footer before rendering associated records, fixes create new on self-associations -# so generated_id for blank associated record is not used in create new button -footer = render(:partial => 'form_association_footer', :locals => {:parent_record => parent_record, :column => column, :associated => associated, :scope => scope}) + # render footer before rendering associated records, fixes create new on self-associations + # so generated_id for blank associated record is not used in create new button + footer = render('form_association_footer', parent_record: parent_record, column: column, associated: associated, scope: scope, subform_div_id: subform_div_id) -%>
    <%= column.label -%> @@ -19,7 +28,7 @@ footer = render(:partial => 'form_association_footer', :locals => {:parent_recor
    > <%# HACK: to be able to delete all associated records %> <%= hidden_field_tag "#{(opts = active_scaffold_input_options(column, scope, :object => parent_record))[:name]}[0]", '', :id => "#{opts[:id]}_0" if column.association.collection? %> - <%= render :partial => subform_partial_for_column(column), :locals => {:column => column, :parent_record => parent_record, :associated => associated, :show_blank_record => show_blank_record, :scope => scope} %> + <%= render subform_partial_for_column(column), column: column, parent_record: parent_record, associated: associated, show_blank_record: show_blank_record, scope: scope, tab_id: tab_id %> <%= footer -%>
    <% diff --git a/app/views/active_scaffold_overrides/_form_association_footer.html.erb b/app/views/active_scaffold_overrides/_form_association_footer.html.erb index 3a08b4cd1..3be18cbb1 100644 --- a/app/views/active_scaffold_overrides/_form_association_footer.html.erb +++ b/app/views/active_scaffold_overrides/_form_association_footer.html.erb @@ -28,7 +28,7 @@ add_new_url = params_for(url_options) if show_add_new add_label = as_(:replace_with_new) add_class = 'as_replace_with_new' end - create_another_id = "#{sub_form_id(:association => column.name, :id => parent_record.id || temporary_id || 99999999999)}-create-another" %> + create_another_id = "#{subform_div_id}-create-another" %> <%= link_to add_label, add_new_url, :id => create_another_id, :remote => true, :class => "as-js-button #{add_class}", :style=> "display: none;" %> <% end -%> @@ -39,7 +39,7 @@ add_new_url = params_for(url_options) if show_add_new <%= link_to_record_select as_(:add_existing), remote_controller.controller_path, record_select_params_for_add_existing(column.association, edit_associated_url, parent_record) -%> <% else -%> <% select_options = options_from_collection_for_select(sorted_association_options_find(column.association, nil, parent_record), :to_param, :to_label) - add_existing_id = "#{sub_form_id(:association => column.name, :id => parent_record.id || temporary_id || 99999999999)}-add-existing" + add_existing_id = "#{subform_div_id}-add-existing" add_existing_label = column.association.collection? ? :add_existing : :replace_existing %> <%= select_tag 'associated_id', content_tag(:option, as_(:_select_), value: '') + select_options %> <%= link_to as_(add_existing_label), edit_associated_url, :id => add_existing_id, :remote => true, :class=> "as-js-button as_#{add_existing_label}", :style => "display: none;" %> diff --git a/app/views/active_scaffold_overrides/_horizontal_subform.html.erb b/app/views/active_scaffold_overrides/_horizontal_subform.html.erb index d7fced7b9..739547137 100644 --- a/app/views/active_scaffold_overrides/_horizontal_subform.html.erb +++ b/app/views/active_scaffold_overrides/_horizontal_subform.html.erb @@ -1,4 +1,4 @@ - +
    <% header_record_class = (show_blank_record && show_blank_record.class) || column.association.klass -%> diff --git a/app/views/active_scaffold_overrides/_vertical_subform.html.erb b/app/views/active_scaffold_overrides/_vertical_subform.html.erb index 1b20e729d..759ebfaa2 100644 --- a/app/views/active_scaffold_overrides/_vertical_subform.html.erb +++ b/app/views/active_scaffold_overrides/_vertical_subform.html.erb @@ -1,4 +1,4 @@ -
    +
    <%= render partial: 'form_association_record', collection: associated, locals: {scope: scope, parent_record: parent_record, column: column, columns: local_assigns[:columns], layout: :vertical} %> <%= render partial: 'form_association_record', object: show_blank_record, locals: {scope: scope, parent_record: parent_record, column: column, columns: local_assigns[:columns], layout: :vertical, locked: true, index: associated.size} if show_blank_record %>
    diff --git a/app/views/active_scaffold_overrides/add_tab.js.erb b/app/views/active_scaffold_overrides/add_tab.js.erb new file mode 100644 index 000000000..98c06df92 --- /dev/null +++ b/app/views/active_scaffold_overrides/add_tab.js.erb @@ -0,0 +1,15 @@ +<% + subsection_id = sub_section_id(:sub_section => @column.label) + tab_options = active_scaffold_tab_options(@column, @record) + tab_label, tab_value, tab_record = tab_options.find { |_, value, _| params[:value] == value.to_s } + if tab_label + tab_id_suffix = clean_id(tab_record&.id&.to_s || tab_value.to_s) + tab_id = "#{subsection_id}-#{tab_id_suffix}-tab" + tab_content = render('form', columns: @column, subsection_id: "#{subsection_id}-#{tab_id_suffix}", form_action: @form_action, scope: @scope, tab_value: tab_record || tab_value, tab_id: tab_id_suffix, tabbed_by: @column.tabbed_by) + tab = active_scaffold_tab_content(tab_id, true, tab_content) +%> +$('#<%= subsection_id %> .tab-content > .tab-pane.active').removeClass('in active'); +ActiveScaffold.create_associated_record_form('<%= subsection_id %> .tab-content', '<%= j tab %>', {singular: false}); +$('#<%= subsection_id %> .nav-tabs').find('.nav-item.active').removeClass('active').end().append('<%= j active_scaffold_tab(tab_label, tab_id, true) %>'); +$('#<%= @source_id %> option[value=<%= tab_value %>]').hide().parent().val(''); +<% end %> \ No newline at end of file diff --git a/lib/active_scaffold/actions/core.rb b/lib/active_scaffold/actions/core.rb index 5ed47b63c..969de0092 100644 --- a/lib/active_scaffold/actions/core.rb +++ b/lib/active_scaffold/actions/core.rb @@ -29,6 +29,11 @@ def render_field respond_to do |format| format.js { render :action => 'render_field_inplace', :layout => false } end + elsif params[:tabbed_by] + add_tab + respond_to do |format| + format.js { render :action => 'add_tab', :layout => false } + end else render_field_for_update_columns respond_to { |format| format.js } @@ -58,10 +63,14 @@ def render_field_for_inplace_editing @record = find_if_allowed(params[:id], :crud_type => :update, :column => params[:update_column]) end - def render_field_for_update_columns - return if (@column = active_scaffold_config.columns[params.delete(:column)]).nil? + def add_tab + process_render_field_params + @column = @main_columns.find_by_name(params[:column]) + @record = updated_record_with_form(@main_columns, {}, @scope) + end + + def process_render_field_params @source_id = params.delete(:source_id) - @columns = @column.update_columns || [] @scope = params.delete(:scope) if @scope @form_action = :subform @@ -70,7 +79,13 @@ def render_field_for_update_columns end @form_action ||= params[:id] ? :update : :create @main_columns = active_scaffold_config.send(@form_action).columns + end + + def render_field_for_update_columns + return if (@column = active_scaffold_config.columns[params.delete(:column)]).nil? + @columns = @column.update_columns || [] @columns << @column.name if @column.options[:refresh_link] && @columns.exclude?(@column.name) + process_render_field_params @record = if @column.send_form_on_update_column diff --git a/lib/active_scaffold/data_structures/action_columns.rb b/lib/active_scaffold/data_structures/action_columns.rb index 75888a666..a5fc37de8 100644 --- a/lib/active_scaffold/data_structures/action_columns.rb +++ b/lib/active_scaffold/data_structures/action_columns.rb @@ -9,6 +9,11 @@ class ActionColumns < ActiveScaffold::DataStructures::Set # labels are useful for the Create/Update forms, when we display columns in a grouped fashion and want to name them separately attr_writer :label + + # a common column in the association columns included in the group, used to group the records from the + # association columns and split them in tabs + attr_accessor :tabbed_by + def label as_(@label) if @label end diff --git a/lib/active_scaffold/helpers/controller_helpers.rb b/lib/active_scaffold/helpers/controller_helpers.rb index 809c59fd3..903303ff4 100644 --- a/lib/active_scaffold/helpers/controller_helpers.rb +++ b/lib/active_scaffold/helpers/controller_helpers.rb @@ -184,7 +184,7 @@ def build_associated(association, parent_record) assign_default_attributes record save_record_to_association(record, association.reverse_association, parent_record) # set inverse end - end + end.tap { |record| yield record if block_given? } end def save_record_to_association(record, association, value, reverse = nil) diff --git a/lib/active_scaffold/helpers/form_column_helpers.rb b/lib/active_scaffold/helpers/form_column_helpers.rb index 20f23f443..f978cad65 100644 --- a/lib/active_scaffold/helpers/form_column_helpers.rb +++ b/lib/active_scaffold/helpers/form_column_helpers.rb @@ -64,16 +64,16 @@ def active_scaffold_render_subform_column(column, scope, crud_type, readonly, ad form_attribute(column, record, scope, true, col_class) else renders_as = column_renders_as(column) - html = render_column(column, record, renders_as, scope, false, col_class) + html = render_column(column, record, renders_as, scope, only_value: false, col_class: col_class) html = content_tag(:div, html, active_scaffold_subform_attributes(column)) if renders_as == :subform html end end - def active_scaffold_subform_attributes(column, column_css_class = nil, klass = nil) + def active_scaffold_subform_attributes(column, column_css_class = nil, klass = nil, tab_id: nil) { - :class => "sub-form #{active_scaffold_config_for(klass || column.association.klass).subform.layout}-sub-form #{column_css_class} #{column.name}-sub-form", - :id => sub_form_id(:association => column.name) + class: "sub-form #{active_scaffold_config_for(klass || column.association.klass).subform.layout}-sub-form #{column_css_class} #{column.name}-sub-form", + id: sub_form_id(association: column.name, tab_id: tab_id) } end @@ -155,7 +155,24 @@ def field_attributes(column, record) {} end - def render_column(column, record, renders_as, scope = nil, only_value = false, col_class = nil) # rubocop:disable Metrics/ParameterLists + def render_subsection(column, record, scope, form_action) + subsection_id = sub_section_id(:sub_section => column.label) + locals = {columns: column, form_action: form_action, scope: scope} + if column.tabbed_by + locals[:tabbed_by] = column.tabbed_by + active_scaffold_tabbed_by(column, record, scope, subsection_id) do |tab_value, tab_id| + render 'form', locals.merge(subsection_id: "#{subsection_id}-#{tab_id}", tab_id: tab_id, tab_value: tab_value) + end + else + header = content_tag(:h5) do + h(column.label) << + link_to_visibility_toggle(subsection_id, default_visible: !column.collapsed) + end + header << render('form', locals.merge(subsection_id: subsection_id)) + end + end + + def render_column(column, record, renders_as, scope = nil, only_value: false, col_class: nil, **opts) if form_column_is_hidden?(column, record, scope) # creates an element that can be replaced by the update_columns routine, # but will not affect the value of the submitted form in this state: @@ -164,16 +181,101 @@ def render_column(column, record, renders_as, scope = nil, only_value = false, c hidden_field_tag(nil, nil, :class => "#{column.name}-input") end elsif (partial = override_form_field_partial(column)) - render :partial => partial, :locals => {:column => column, :only_value => only_value, :scope => scope, :col_class => col_class, :record => record} + render :partial => partial, :locals => {column: column, only_value: only_value, scope: scope, col_class: col_class, record: record} elsif renders_as == :field || override_form_field?(column) form_attribute(column, record, scope, only_value, col_class) elsif renders_as == :subform - render :partial => 'form_association', :locals => {:column => column, :scope => scope, :parent_record => record} + render 'form_association', opts.slice(:tabbed_by, :tab_value, :tab_id).merge(column: column, scope: scope, parent_record: record) else form_hidden_attribute(column, record, scope) end end + def active_scaffold_tabbed_by(column, record, scope, subsection_id, &block) + add_tab_url = params_for(action: 'render_field', tabbed_by: column.tabbed_by, id: record.to_param, column: column.label) + refresh_opts = {refresh_link: {text: 'Add tab', class: 'refresh-link add-tab'}} + tab_options = active_scaffold_tab_options(column, record) + used_tabs = active_scaffold_current_tabs(column, record) + field = content_tag :dl do + content_tag(:dt, label_tag("#{subsection_id}_input", column.label)) << content_tag(:dd) do + active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, used_tabs.map(&:first)) << + active_scaffold_refresh_link(nil, {'data-update_url' => url_for(add_tab_url)}, record, refresh_opts) + end + end + field << active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs, &block) + end + + def active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, used_tabs) + hidden_style = 'display: none;' + blank_choice = content_tag(:option, as_(:_select_), value: '') + option_tags = tab_options.inject(blank_choice) do |html, (label, value, tab_record)| + used = used_tabs.include? tab_record || value + html << content_tag(:option, label, value: value, style: (hidden_style if used)) + end + select_tag(nil, option_tags, class: "#{column.tabbed_by}-input", id: "#{subsection_id}_input") + end + + def active_scaffold_current_tabs(column, record) + used_choices = Set.new + column.each_column do |col| + tabbed_by = col.options[:tabbed_by] || column.tabbed_by + tab_values = record.send(col.name).map(&tabbed_by).compact + tab_values.map! { |value| [value, value.id.to_s] } if tabbed_by_association?(col, tabbed_by) + used_choices.merge tab_values + end + used_choices + end + + def tabbed_by_association?(assoc_column, tabbed_by) + assoc_column.association.klass.reflect_on_association(tabbed_by).present? + end + + def active_scaffold_tab_options(column, record) + subform_column = column.each_column { |col| break col } + if subform_column + tabbed_by = subform_column.options[:tabbed_by] || column.tabbed_by + if tabbed_by_association?(subform_column, tabbed_by) + subform_record = record.send(subform_column.name).first_or_initialize + tab_column = active_scaffold_config_for(subform_column.association.klass).columns[tabbed_by] + end + end + if tab_column + label_method = (tab_column.form_ui_options || tab_column.options)[:label_method] || :to_label + sorted_association_options_find(tab_column.association, nil, subform_record).map do |opt_record| + [opt_record.send(label_method), opt_record.id, opt_record] + end + else + [] + end + end + + def active_scaffold_tab(label, tab_id, active) + content_tag :li, class: "nav-item #{:active if active}" do + link_to(label, "##{tab_id}", class: 'nav-link', data: {toggle: :tab}) + end + end + + def active_scaffold_tab_content(tab_id, active, content) + content_tag(:div, content, class: "tab-pane fade#{' in active' if active}", id: tab_id) + end + + def active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs) + used_tabs = used_tabs.map { |value, value_id| [value, "#{clean_id(value_id || value.to_s)}"] } + content_tag(:div, id: subsection_id, class: 'tabbed') do + content_tag(:ul, class: 'nav nav-tabs') do + tabs = used_tabs.map.with_index do |(tab_value, id), i| + active_scaffold_tab tab_options.find { |_, value, tab_record| tab_value == (tab_record || value) }&.first, "#{subsection_id}-#{id}-tab", i.zero? + end + safe_join tabs + end << content_tag(:div, class: 'tab-content') do + tabs = used_tabs.map.with_index do |(tab_value, id), i| + active_scaffold_tab_content("#{subsection_id}-#{id}-tab", i.zero?, yield(tab_value, id)) + end + safe_join(tabs) + end + end + end + def form_column_is_hidden?(column, record, scope = nil) if column.hide_form_column_if&.respond_to?(:call) column.hide_form_column_if.call(record, column, scope) diff --git a/lib/active_scaffold/helpers/id_helpers.rb b/lib/active_scaffold/helpers/id_helpers.rb index 8472e80f8..f235dcf98 100644 --- a/lib/active_scaffold/helpers/id_helpers.rb +++ b/lib/active_scaffold/helpers/id_helpers.rb @@ -106,13 +106,13 @@ def sub_section_id(options = {}) def sub_form_id(options = {}) options[:id] ||= params[:id] options[:id] ||= nested_parent_id if nested? - clean_id "#{controller_id}-#{options[:id]}-#{options[:association]}-subform" + clean_id "#{controller_id}-#{options[:id]}-#{options[:association]}#{'-' if options[:tab_id]}#{options[:tab_id]}-subform" end def sub_form_list_id(options = {}) options[:id] ||= params[:id] options[:id] ||= nested_parent_id if nested? - clean_id "#{controller_id}-#{options[:id]}-#{options[:association]}-subform-list" + clean_id "#{controller_id}-#{options[:id]}-#{options[:association]}#{'-' if options[:tab_id]}#{options[:tab_id]}-subform-list" end def element_messages_id(options = {}) From 45872ab9c5367cf6cfa7243e69a69b6db85325eb Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Fri, 18 Oct 2024 15:55:31 +0200 Subject: [PATCH 06/10] cleanup rubocop offenses, update gems --- Gemfile.lock | 128 +++++++++--------- .../helpers/form_column_helpers.rb | 4 +- 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1081c7eb7..c11dd773d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,65 +16,65 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.8) - actionpack (= 6.1.7.8) - activesupport (= 6.1.7.8) + actioncable (6.1.7.9) + actionpack (= 6.1.7.9) + activesupport (= 6.1.7.9) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.8) - actionpack (= 6.1.7.8) - activejob (= 6.1.7.8) - activerecord (= 6.1.7.8) - activestorage (= 6.1.7.8) - activesupport (= 6.1.7.8) + actionmailbox (6.1.7.9) + actionpack (= 6.1.7.9) + activejob (= 6.1.7.9) + activerecord (= 6.1.7.9) + activestorage (= 6.1.7.9) + activesupport (= 6.1.7.9) mail (>= 2.7.1) - actionmailer (6.1.7.8) - actionpack (= 6.1.7.8) - actionview (= 6.1.7.8) - activejob (= 6.1.7.8) - activesupport (= 6.1.7.8) + actionmailer (6.1.7.9) + actionpack (= 6.1.7.9) + actionview (= 6.1.7.9) + activejob (= 6.1.7.9) + activesupport (= 6.1.7.9) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.8) - actionview (= 6.1.7.8) - activesupport (= 6.1.7.8) + actionpack (6.1.7.9) + actionview (= 6.1.7.9) + activesupport (= 6.1.7.9) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.8) - actionpack (= 6.1.7.8) - activerecord (= 6.1.7.8) - activestorage (= 6.1.7.8) - activesupport (= 6.1.7.8) + actiontext (6.1.7.9) + actionpack (= 6.1.7.9) + activerecord (= 6.1.7.9) + activestorage (= 6.1.7.9) + activesupport (= 6.1.7.9) nokogiri (>= 1.8.5) - actionview (6.1.7.8) - activesupport (= 6.1.7.8) + actionview (6.1.7.9) + activesupport (= 6.1.7.9) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.7.8) - activesupport (= 6.1.7.8) + activejob (6.1.7.9) + activesupport (= 6.1.7.9) globalid (>= 0.3.6) - activemodel (6.1.7.8) - activesupport (= 6.1.7.8) - activerecord (6.1.7.8) - activemodel (= 6.1.7.8) - activesupport (= 6.1.7.8) + activemodel (6.1.7.9) + activesupport (= 6.1.7.9) + activerecord (6.1.7.9) + activemodel (= 6.1.7.9) + activesupport (= 6.1.7.9) activerecord-jdbc-adapter (61.2-java) activerecord (~> 6.1.0) activerecord-jdbcsqlite3-adapter (61.2-java) activerecord-jdbc-adapter (= 61.2) jdbc-sqlite3 (~> 3.8, < 3.30) - activestorage (6.1.7.8) - actionpack (= 6.1.7.8) - activejob (= 6.1.7.8) - activerecord (= 6.1.7.8) - activesupport (= 6.1.7.8) + activestorage (6.1.7.9) + actionpack (= 6.1.7.9) + activejob (= 6.1.7.9) + activerecord (= 6.1.7.9) + activesupport (= 6.1.7.9) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.8) + activesupport (6.1.7.9) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -83,11 +83,11 @@ GEM ansi (1.5.0) ast (2.4.2) brakeman (5.4.1) - builder (3.2.4) + builder (3.3.0) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.4) cow_proxy (0.3.3) crass (1.0.6) date (3.3.4) @@ -95,7 +95,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - erubi (1.12.0) + erubi (1.13.0) gli (2.21.0) globalid (1.2.1) activesupport (>= 6.1) @@ -103,7 +103,7 @@ GEM http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (0.9.36) activesupport (>= 4.0.2) @@ -140,7 +140,7 @@ GEM mime-types-data (3.2022.0105) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.23.1) + minitest (5.25.1) minitest-reporters (1.6.0) ansi builder @@ -173,25 +173,25 @@ GEM stringio psych (5.1.2-java) jar-dependencies (>= 0.1.7) - racc (1.8.0) - racc (1.8.0-java) - rack (2.2.9) + racc (1.8.1) + racc (1.8.1-java) + rack (2.2.10) rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.8) - actioncable (= 6.1.7.8) - actionmailbox (= 6.1.7.8) - actionmailer (= 6.1.7.8) - actionpack (= 6.1.7.8) - actiontext (= 6.1.7.8) - actionview (= 6.1.7.8) - activejob (= 6.1.7.8) - activemodel (= 6.1.7.8) - activerecord (= 6.1.7.8) - activestorage (= 6.1.7.8) - activesupport (= 6.1.7.8) + rails (6.1.7.9) + actioncable (= 6.1.7.9) + actionmailbox (= 6.1.7.9) + actionmailer (= 6.1.7.9) + actionpack (= 6.1.7.9) + actiontext (= 6.1.7.9) + actionview (= 6.1.7.9) + activejob (= 6.1.7.9) + activemodel (= 6.1.7.9) + activerecord (= 6.1.7.9) + activestorage (= 6.1.7.9) + activesupport (= 6.1.7.9) bundler (>= 1.15.0) - railties (= 6.1.7.8) + railties (= 6.1.7.9) sprockets-rails (>= 2.0.0) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) @@ -202,9 +202,9 @@ GEM rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - railties (6.1.7.8) - actionpack (= 6.1.7.8) - activesupport (= 6.1.7.8) + railties (6.1.7.9) + actionpack (= 6.1.7.9) + activesupport (= 6.1.7.9) method_source rake (>= 12.2) thor (~> 1.0) @@ -244,7 +244,7 @@ GEM sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.5.0) + sprockets-rails (3.5.2) actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) @@ -252,7 +252,7 @@ GEM stringio (3.0.6) terminal-table (3.0.0) unicode-display_width (~> 1.1, >= 1.1.1) - thor (1.3.1) + thor (1.3.2) timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -266,7 +266,7 @@ GEM websocket-driver (0.7.6-java) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.15) + zeitwerk (2.6.18) PLATFORMS java diff --git a/lib/active_scaffold/helpers/form_column_helpers.rb b/lib/active_scaffold/helpers/form_column_helpers.rb index f978cad65..ae4ab293d 100644 --- a/lib/active_scaffold/helpers/form_column_helpers.rb +++ b/lib/active_scaffold/helpers/form_column_helpers.rb @@ -209,7 +209,7 @@ def active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, hidden_style = 'display: none;' blank_choice = content_tag(:option, as_(:_select_), value: '') option_tags = tab_options.inject(blank_choice) do |html, (label, value, tab_record)| - used = used_tabs.include? tab_record || value + used = used_tabs.include?(tab_record || value) html << content_tag(:option, label, value: value, style: (hidden_style if used)) end select_tag(nil, option_tags, class: "#{column.tabbed_by}-input", id: "#{subsection_id}_input") @@ -260,7 +260,7 @@ def active_scaffold_tab_content(tab_id, active, content) end def active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs) - used_tabs = used_tabs.map { |value, value_id| [value, "#{clean_id(value_id || value.to_s)}"] } + used_tabs = used_tabs.map { |value, value_id| [value, clean_id(value_id || value.to_s)] } content_tag(:div, id: subsection_id, class: 'tabbed') do content_tag(:ul, class: 'nav nav-tabs') do tabs = used_tabs.map.with_index do |(tab_value, id), i| From a982b658d53a76ada4f448be2948d48996e8740c Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Sat, 19 Oct 2024 00:41:05 +0200 Subject: [PATCH 07/10] use sub-section for tabbed, with the same header, add styles for tabs move tabs to own helper to reduce module size (fix rubocop offense) --- .../stylesheets/active_scaffold_layout.css | 54 ++++++++++ .../active_scaffold_overrides/_form.html.erb | 2 +- .../helpers/form_column_helpers.rb | 99 ++----------------- lib/active_scaffold/helpers/tabs_helpers.rb | 92 +++++++++++++++++ lib/active_scaffold/helpers/view_helpers.rb | 1 + 5 files changed, 156 insertions(+), 92 deletions(-) create mode 100644 lib/active_scaffold/helpers/tabs_helpers.rb diff --git a/app/assets/stylesheets/active_scaffold_layout.css b/app/assets/stylesheets/active_scaffold_layout.css index b377aaaf8..8caf68bbb 100644 --- a/app/assets/stylesheets/active_scaffold_layout.css +++ b/app/assets/stylesheets/active_scaffold_layout.css @@ -957,6 +957,60 @@ font-size: 100%; float:left; } +.active-scaffold .tabbed .nav-tabs { + border-bottom: 1px solid #ddd; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.active-scaffold .tabbed .nav-tabs:before, +.active-scaffold .tabbed .nav-tabs:after { + display: table; + content: ' '; +} +.active-scaffold .tabbed .nav-tabs:after { + clear: both; +} + +.active-scaffold .tabbed .nav-tabs > li { + float: left; + margin-bottom: -1px; + position: relative; + display: block; +} + +.active-scaffold .tabbed .nav-tabs > li > a { + margin-right: 2px; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; + position: relative; + display: block; + padding: 10px 15px; +} +.active-scaffold .tabbed .nav-tabs > li > a:focus, +.active-scaffold .tabbed .nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; + text-decoration: none; + background-color: #eee; +} +.active-scaffold .tabbed .nav-tabs > li.active > a, +.active-scaffold .tabbed .nav-tabs > li.active > a:focus, +.active-scaffold .tabbed .nav-tabs > li.active > a:hover { + border-color: #ddd; + border-bottom-color: transparent; + cursor: default; + background-color: #fff; +} + +.active-scaffold .tabbed .tab-content > .tab-pane { + display: none; +} +.active-scaffold .tabbed .tab-content > .active { + display: block; +} + + .as_touch a.inline-adapter-close { width: 25px; height: 27px; diff --git a/app/views/active_scaffold_overrides/_form.html.erb b/app/views/active_scaffold_overrides/_form.html.erb index a536c3fe8..81b938aa9 100644 --- a/app/views/active_scaffold_overrides/_form.html.erb +++ b/app/views/active_scaffold_overrides/_form.html.erb @@ -13,7 +13,7 @@ <% authorized = show_unauthorized_columns || renders_as == :subsection ? @record.authorized_for?(crud_type: form_action, column: column.name) : true %> <% if renders_as == :subsection || renders_as == :tabbed -%> <% next unless authorized %> -
  6. +
  7. <%= render_subsection(column, @record, scope, form_action) %>
  8. <% elsif renders_as == :subform and authorized -%> diff --git a/lib/active_scaffold/helpers/form_column_helpers.rb b/lib/active_scaffold/helpers/form_column_helpers.rb index ae4ab293d..4e06d37c0 100644 --- a/lib/active_scaffold/helpers/form_column_helpers.rb +++ b/lib/active_scaffold/helpers/form_column_helpers.rb @@ -158,16 +158,18 @@ def field_attributes(column, record) def render_subsection(column, record, scope, form_action) subsection_id = sub_section_id(:sub_section => column.label) locals = {columns: column, form_action: form_action, scope: scope} + header = content_tag(:h5) do + h(column.label) << + link_to_visibility_toggle(subsection_id, default_visible: !column.collapsed) + end if column.tabbed_by locals[:tabbed_by] = column.tabbed_by - active_scaffold_tabbed_by(column, record, scope, subsection_id) do |tab_value, tab_id| - render 'form', locals.merge(subsection_id: "#{subsection_id}-#{tab_id}", tab_id: tab_id, tab_value: tab_value) + header << content_tag(:div, id: subsection_id) do + active_scaffold_tabbed_by(column, record, scope, subsection_id) do |tab_value, tab_id| + render 'form', locals.merge(subsection_id: "#{subsection_id}-#{tab_id}", tab_id: tab_id, tab_value: tab_value) + end end else - header = content_tag(:h5) do - h(column.label) << - link_to_visibility_toggle(subsection_id, default_visible: !column.collapsed) - end header << render('form', locals.merge(subsection_id: subsection_id)) end end @@ -191,91 +193,6 @@ def render_column(column, record, renders_as, scope = nil, only_value: false, co end end - def active_scaffold_tabbed_by(column, record, scope, subsection_id, &block) - add_tab_url = params_for(action: 'render_field', tabbed_by: column.tabbed_by, id: record.to_param, column: column.label) - refresh_opts = {refresh_link: {text: 'Add tab', class: 'refresh-link add-tab'}} - tab_options = active_scaffold_tab_options(column, record) - used_tabs = active_scaffold_current_tabs(column, record) - field = content_tag :dl do - content_tag(:dt, label_tag("#{subsection_id}_input", column.label)) << content_tag(:dd) do - active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, used_tabs.map(&:first)) << - active_scaffold_refresh_link(nil, {'data-update_url' => url_for(add_tab_url)}, record, refresh_opts) - end - end - field << active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs, &block) - end - - def active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, used_tabs) - hidden_style = 'display: none;' - blank_choice = content_tag(:option, as_(:_select_), value: '') - option_tags = tab_options.inject(blank_choice) do |html, (label, value, tab_record)| - used = used_tabs.include?(tab_record || value) - html << content_tag(:option, label, value: value, style: (hidden_style if used)) - end - select_tag(nil, option_tags, class: "#{column.tabbed_by}-input", id: "#{subsection_id}_input") - end - - def active_scaffold_current_tabs(column, record) - used_choices = Set.new - column.each_column do |col| - tabbed_by = col.options[:tabbed_by] || column.tabbed_by - tab_values = record.send(col.name).map(&tabbed_by).compact - tab_values.map! { |value| [value, value.id.to_s] } if tabbed_by_association?(col, tabbed_by) - used_choices.merge tab_values - end - used_choices - end - - def tabbed_by_association?(assoc_column, tabbed_by) - assoc_column.association.klass.reflect_on_association(tabbed_by).present? - end - - def active_scaffold_tab_options(column, record) - subform_column = column.each_column { |col| break col } - if subform_column - tabbed_by = subform_column.options[:tabbed_by] || column.tabbed_by - if tabbed_by_association?(subform_column, tabbed_by) - subform_record = record.send(subform_column.name).first_or_initialize - tab_column = active_scaffold_config_for(subform_column.association.klass).columns[tabbed_by] - end - end - if tab_column - label_method = (tab_column.form_ui_options || tab_column.options)[:label_method] || :to_label - sorted_association_options_find(tab_column.association, nil, subform_record).map do |opt_record| - [opt_record.send(label_method), opt_record.id, opt_record] - end - else - [] - end - end - - def active_scaffold_tab(label, tab_id, active) - content_tag :li, class: "nav-item #{:active if active}" do - link_to(label, "##{tab_id}", class: 'nav-link', data: {toggle: :tab}) - end - end - - def active_scaffold_tab_content(tab_id, active, content) - content_tag(:div, content, class: "tab-pane fade#{' in active' if active}", id: tab_id) - end - - def active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs) - used_tabs = used_tabs.map { |value, value_id| [value, clean_id(value_id || value.to_s)] } - content_tag(:div, id: subsection_id, class: 'tabbed') do - content_tag(:ul, class: 'nav nav-tabs') do - tabs = used_tabs.map.with_index do |(tab_value, id), i| - active_scaffold_tab tab_options.find { |_, value, tab_record| tab_value == (tab_record || value) }&.first, "#{subsection_id}-#{id}-tab", i.zero? - end - safe_join tabs - end << content_tag(:div, class: 'tab-content') do - tabs = used_tabs.map.with_index do |(tab_value, id), i| - active_scaffold_tab_content("#{subsection_id}-#{id}-tab", i.zero?, yield(tab_value, id)) - end - safe_join(tabs) - end - end - end - def form_column_is_hidden?(column, record, scope = nil) if column.hide_form_column_if&.respond_to?(:call) column.hide_form_column_if.call(record, column, scope) diff --git a/lib/active_scaffold/helpers/tabs_helpers.rb b/lib/active_scaffold/helpers/tabs_helpers.rb new file mode 100644 index 000000000..dc9e82cdb --- /dev/null +++ b/lib/active_scaffold/helpers/tabs_helpers.rb @@ -0,0 +1,92 @@ +module ActiveScaffold + module Helpers + # Helpers that assist with rendering of tabs in forms + module TabsHelpers + def active_scaffold_tabbed_by(column, record, scope, subsection_id, &block) + add_tab_url = params_for(action: 'render_field', tabbed_by: column.tabbed_by, id: record.to_param, column: column.label) + refresh_opts = {refresh_link: {text: 'Add tab', class: 'refresh-link add-tab'}} + tab_options = active_scaffold_tab_options(column, record) + used_tabs = active_scaffold_current_tabs(column, record) + #field = content_tag :dl do + # content_tag(:dt, label_tag("#{subsection_id}_input", column.label)) << content_tag(:dd) do + active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, used_tabs.map(&:first)) << + active_scaffold_refresh_link(nil, {'data-update_url' => url_for(add_tab_url)}, record, refresh_opts) << + # end + #end + #field << + active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs, &block) + end + + def active_scaffold_input_for_tabbed(column, record, subsection_id, tab_options, used_tabs) + hidden_style = 'display: none;' + blank_choice = content_tag(:option, as_(:_select_), value: '') + option_tags = tab_options.inject(blank_choice) do |html, (label, value, tab_record)| + used = used_tabs.include?(tab_record || value) + html << content_tag(:option, label, value: value, style: (hidden_style if used)) + end + select_tag(nil, option_tags, class: "#{column.tabbed_by}-input", id: "#{subsection_id}_input") + end + + def active_scaffold_current_tabs(column, record) + used_choices = Set.new + column.each_column do |col| + tabbed_by = col.options[:tabbed_by] || column.tabbed_by + tab_values = record.send(col.name).map(&tabbed_by).compact + tab_values.map! { |value| [value, value.id.to_s] } if tabbed_by_association?(col, tabbed_by) + used_choices.merge tab_values + end + used_choices + end + + def tabbed_by_association?(assoc_column, tabbed_by) + assoc_column.association.klass.reflect_on_association(tabbed_by).present? + end + + def active_scaffold_tab_options(column, record) + subform_column = column.each_column { |col| break col } + if subform_column + tabbed_by = subform_column.options[:tabbed_by] || column.tabbed_by + if tabbed_by_association?(subform_column, tabbed_by) + subform_record = record.send(subform_column.name).first_or_initialize + tab_column = active_scaffold_config_for(subform_column.association.klass).columns[tabbed_by] + end + end + if tab_column + label_method = (tab_column.form_ui_options || tab_column.options)[:label_method] || :to_label + sorted_association_options_find(tab_column.association, nil, subform_record).map do |opt_record| + [opt_record.send(label_method), opt_record.id, opt_record] + end + else + [] + end + end + + def active_scaffold_tab(label, tab_id, active) + content_tag :li, class: "nav-item #{:active if active}" do + link_to(label, "##{tab_id}", class: 'nav-link', data: {toggle: :tab}) + end + end + + def active_scaffold_tab_content(tab_id, active, content) + content_tag(:div, content, class: "tab-pane fade#{' in active' if active}", id: tab_id) + end + + def active_scaffold_tabs_for(column, record, subsection_id, tab_options, used_tabs) + used_tabs = used_tabs.map { |value, value_id| [value, clean_id(value_id || value.to_s)] } + content_tag(:div, class: 'tabbed') do + content_tag(:ul, class: 'nav nav-tabs') do + tabs = used_tabs.map.with_index do |(tab_value, id), i| + active_scaffold_tab tab_options.find { |_, value, tab_record| tab_value == (tab_record || value) }&.first, "#{subsection_id}-#{id}-tab", i.zero? + end + safe_join tabs + end << content_tag(:div, class: 'tab-content') do + tabs = used_tabs.map.with_index do |(tab_value, id), i| + active_scaffold_tab_content("#{subsection_id}-#{id}-tab", i.zero?, yield(tab_value, id)) + end + safe_join(tabs) + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/active_scaffold/helpers/view_helpers.rb b/lib/active_scaffold/helpers/view_helpers.rb index ea29a0d83..c567cda09 100644 --- a/lib/active_scaffold/helpers/view_helpers.rb +++ b/lib/active_scaffold/helpers/view_helpers.rb @@ -10,6 +10,7 @@ module ViewHelpers include ActiveScaffold::Helpers::ListColumnHelpers include ActiveScaffold::Helpers::ShowColumnHelpers include ActiveScaffold::Helpers::FormColumnHelpers + include ActiveScaffold::Helpers::TabsHelpers include ActiveScaffold::Helpers::SearchColumnHelpers include ActiveScaffold::Helpers::HumanConditionHelpers From f1cf935c004187e2799f1d1f9ba84c1fef4e3f95 Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Sat, 19 Oct 2024 00:48:58 +0200 Subject: [PATCH 08/10] add javascript to change tab --- app/assets/javascripts/jquery/active_scaffold.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/javascripts/jquery/active_scaffold.js b/app/assets/javascripts/jquery/active_scaffold.js index 06d117c72..985b4cba5 100644 --- a/app/assets/javascripts/jquery/active_scaffold.js +++ b/app/assets/javascripts/jquery/active_scaffold.js @@ -389,6 +389,15 @@ form.find('select.as_search_date_time_option').val('BETWEEN'); }); + jQuery(document).on('click', '.active-scaffold .tabbed .nav-tabs a', function(e) { + e.preventDefault(); + var tab_ctrl = jQuery(this), tabbed = tab_ctrl.closest('.tabbed') + tabbed.find('.nav-tabs .active').removeClass('active'); + tabbed.find('.tab-content .active').removeClass('in active'); + tab_ctrl.closest('li').addClass('active'); + jQuery(tab_ctrl.attr('href')).addClass('in active'); + }) + jQuery(document).on('turbolinks:before-visit turbo:before-visit', function() { if (history.state.active_scaffold) { history.replaceState({turbolinks: true, url: document.location.href}, '', document.location.href); From 68c80366adb1ba735e0ceab597e382d2e495ce14 Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Sat, 19 Oct 2024 00:59:30 +0200 Subject: [PATCH 09/10] let bootstrap handle tabs if it's loaded --- app/assets/javascripts/jquery/active_scaffold.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/jquery/active_scaffold.js b/app/assets/javascripts/jquery/active_scaffold.js index 985b4cba5..49a4b28ab 100644 --- a/app/assets/javascripts/jquery/active_scaffold.js +++ b/app/assets/javascripts/jquery/active_scaffold.js @@ -390,13 +390,14 @@ }); jQuery(document).on('click', '.active-scaffold .tabbed .nav-tabs a', function(e) { + if (typeof $().tab == 'function') return; // bootstrap tab plugin is loaded and will handle tabs e.preventDefault(); var tab_ctrl = jQuery(this), tabbed = tab_ctrl.closest('.tabbed') tabbed.find('.nav-tabs .active').removeClass('active'); tabbed.find('.tab-content .active').removeClass('in active'); tab_ctrl.closest('li').addClass('active'); jQuery(tab_ctrl.attr('href')).addClass('in active'); - }) + }); jQuery(document).on('turbolinks:before-visit turbo:before-visit', function() { if (history.state.active_scaffold) { From 832a0ba4db8533c57bed0da9d6580a96b2c51211 Mon Sep 17 00:00:00 2001 From: Sergio Cambra Date: Sat, 19 Oct 2024 12:24:46 +0200 Subject: [PATCH 10/10] rename new arg --- lib/active_scaffold/helpers/form_column_helpers.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/active_scaffold/helpers/form_column_helpers.rb b/lib/active_scaffold/helpers/form_column_helpers.rb index 4e06d37c0..158e5e11f 100644 --- a/lib/active_scaffold/helpers/form_column_helpers.rb +++ b/lib/active_scaffold/helpers/form_column_helpers.rb @@ -174,7 +174,7 @@ def render_subsection(column, record, scope, form_action) end end - def render_column(column, record, renders_as, scope = nil, only_value: false, col_class: nil, **opts) + def render_column(column, record, renders_as, scope = nil, only_value: false, col_class: nil, **subform_locals) if form_column_is_hidden?(column, record, scope) # creates an element that can be replaced by the update_columns routine, # but will not affect the value of the submitted form in this state: @@ -183,11 +183,11 @@ def render_column(column, record, renders_as, scope = nil, only_value: false, co hidden_field_tag(nil, nil, :class => "#{column.name}-input") end elsif (partial = override_form_field_partial(column)) - render :partial => partial, :locals => {column: column, only_value: only_value, scope: scope, col_class: col_class, record: record} + render partial, column: column, only_value: only_value, scope: scope, col_class: col_class, record: record elsif renders_as == :field || override_form_field?(column) form_attribute(column, record, scope, only_value, col_class) elsif renders_as == :subform - render 'form_association', opts.slice(:tabbed_by, :tab_value, :tab_id).merge(column: column, scope: scope, parent_record: record) + render 'form_association', subform_locals.slice(:tabbed_by, :tab_value, :tab_id).merge(column: column, scope: scope, parent_record: record) else form_hidden_attribute(column, record, scope) end