diff --git a/lib/ransack/helpers/form_helper.rb b/lib/ransack/helpers/form_helper.rb index 461406ec5..e2ba1553f 100644 --- a/lib/ransack/helpers/form_helper.rb +++ b/lib/ransack/helpers/form_helper.rb @@ -55,6 +55,7 @@ def search_form_for(record, options = {}, &proc) form_for(record, options, &proc) end + # sort_link @q, :name, [:name, 'kind ASC'], 'Player Name' def sort_link(search, attribute, *args) # Extract out a routing proxy for url_for scoping later if search.is_a?(Array) @@ -65,39 +66,75 @@ def sort_link(search, attribute, *args) raise TypeError, "First argument must be a Ransack::Search!" unless Search === search + # This is the field that this link represents. The direction of the sort icon (up/down arrow) will + # depend on the sort status of this field + field_name = attribute.to_s + + # Determine the fields we want to sort on + sort_fields = if Array === args.first + args.shift + else + Array(field_name) + end + + label = if ! args.first.try(:is_a?, Hash) + args.shift.to_s + else + Translate.attribute(field_name, :context => search.context) + end + + options = args.first.is_a?(Hash) ? args.shift.dup : {} + default_order = options.delete :default_order + # If the default order is a hash of fields, duplicate it and let us access it with strings or symbols + default_order = default_order.dup.with_indifferent_access if Hash === default_order + search_params = params[search.context.search_key].presence || {}.with_indifferent_access - attr_name = attribute.to_s + # Find the current direction (if there is one) of the primary sort field + if existing_sort = search.sorts.detect { |s| s.name == field_name } + field_current_dir = existing_sort.dir + end - name = ( - if args.size > 0 && !args.first.is_a?(Hash) - args.shift.to_s - else - Translate.attribute(attr_name, :context => search.context) + sort_params = [] + + Array(sort_fields).each do |sort_field| + attr_name, new_dir = sort_field.to_s.downcase.split(/\s+/) + current_dir = nil + + # if the user didn't specify the sort direction, detect the previous + # sort direction on this field and reverse it + if %w{ asc desc }.none? { |d| d == new_dir } + if existing_sort = search.sorts.detect { |s| s.name == attr_name } + current_dir = existing_sort.dir + end + + new_dir = if current_dir + current_dir == desc ? asc : desc + else + if Hash === default_order + default_order[attr_name] || asc + else + default_order || asc + end + end end - ) - if existing_sort = search.sorts.detect { |s| s.name == attr_name } - prev_attr, prev_dir = existing_sort.name, existing_sort.dir + sort_params << "#{attr_name} #{new_dir}" end - options = args.first.is_a?(Hash) ? args.shift.dup : {} - default_order = options.delete :default_order - current_dir = prev_attr == attr_name ? prev_dir : nil - - if current_dir - new_dir = current_dir == desc ? asc : desc - else - new_dir = default_order || asc + # if there is only one sort parameter, remove it from the array and just + # use the string as the parameter + if sort_params.size == 1 + sort_params = sort_params.first end html_options = args.first.is_a?(Hash) ? args.shift.dup : {} - css = ['sort_link', current_dir].compact.join(' ') + css = ['sort_link', field_current_dir].compact.join(' ') html_options[:class] = [css, html_options[:class]].compact.join(' ') query_hash = {} query_hash[search.context.search_key] = search_params - .merge(:s => "#{attr_name} #{new_dir}") + .merge(:s => sort_params) options.merge!(query_hash) options_for_url = params.merge options @@ -108,7 +145,7 @@ def sort_link(search, attribute, *args) end link_to( - [ERB::Util.h(name), order_indicator_for(current_dir)] + [ERB::Util.h(label), order_indicator_for(field_current_dir)] .compact .join(non_breaking_space) .html_safe, diff --git a/spec/ransack/helpers/form_helper_spec.rb b/spec/ransack/helpers/form_helper_spec.rb index 198bf463b..5de8df803 100644 --- a/spec/ransack/helpers/form_helper_spec.rb +++ b/spec/ransack/helpers/form_helper_spec.rb @@ -145,6 +145,145 @@ module Helpers } end + describe '#sort_link with multiple search_keys defined as an array' do + subject { @controller.view_context + .sort_link( + [:main_app, Person.search(:sorts => ['name desc', 'email asc'])], + :name, [:name, 'email DESC'], + :controller => 'people' + ) + } + it { + should match( + if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./ + /people\?q%5Bs%5D%5B%5D=name\+asc&q%5Bs%5D%5B%5D=email+desc/ + else + /people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/ + end + ) + } + it { + should match /sort_link desc/ + } + it { + should match /Full Name ▼/ + } + end + + describe '#sort_link with multiple search_keys should allow a label to be specified' do + subject { @controller.view_context + .sort_link( + [:main_app, Person.search(:sorts => ['name desc', 'email asc'])], + :name, [:name, 'email DESC'], + 'Property Name', + :controller => 'people' + ) + } + it { + should match /Property Name ▼/ + } + end + + describe '#sort_link with multiple search_keys should flip multiple fields specified without a direction' do + subject { @controller.view_context + .sort_link( + [:main_app, Person.search(:sorts => ['name desc', 'email asc'])], + :name, [:name, :email], + :controller => 'people' + ) + } + it { + should match( + if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./ + /people\?q%5Bs%5D%5B%5D=name\+asc&q%5Bs%5D%5B%5D=email+desc/ + else + /people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/ + end + ) + } + it { + should match /sort_link desc/ + } + it { + should match /Full Name ▼/ + } + end + + describe '#sort_link with multiple search_keys should allow a default_order to be specified' do + subject { @controller.view_context + .sort_link( + [:main_app, Person.search()], + :name, [:name, :email], + :controller => 'people', + :default_order => 'desc' + ) + } + it { + should match( + if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./ + /people\?q%5Bs%5D%5B%5D=name\+desc&q%5Bs%5D%5B%5D=email+desc/ + else + /people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/ + end + ) + } + it { + should match /sort_link/ + } + it { + should match /Full Name/ + } + end + + describe '#sort_link with multiple search_keys should allow multiple default_orders to be specified' do + subject { @controller.view_context + .sort_link( + [:main_app, Person.search()], + :name, [:name, :email], + :controller => 'people', + :default_order => { 'name' => 'desc', :email => 'asc' } + ) + } + it { + should match( + if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./ + /people\?q%5Bs%5D%5B%5D=name\+desc&q%5Bs%5D%5B%5D=email+asc/ + else + /people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+asc/ + end) + } + it { + should match /sort_link/ + } + it { + should match /Full Name/ + } + end + + describe '#sort_link with multiple search_keys with multiple default_orders should not override a specified order' do + subject { @controller.view_context + .sort_link( + [:main_app, Person.search()], + :name, [:name, 'email desc'], + :controller => 'people', + :default_order => { 'name' => 'desc', :email => 'asc' } + ) + } + it { + should match( + if ActiveRecord::VERSION::STRING =~ /^3\.[1-2]\./ + /people\?q%5Bs%5D%5B%5D=name\+desc&q%5Bs%5D%5B%5D=email+desc/ + else + /people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/ + end) + } + it { + should match /sort_link/ + } + it { + should match /Full Name/ + } + end context 'view has existing parameters' do before do @controller.view_context.params.merge!({ :exist => 'existing' })