Add multiple sort field support in sort_link

This patch allows users to sort on multiple fields with the sort_link
helper.

To specify sorting on multiple fields:

  sort_link(:kind, [:kind, 'name asc'])

This will create a sort link that sorts first by kind, and then by
name. The first `:kind` parameter ensures that the link generated
shows the sort status of the `kind` field.

When you specify a sort direction in the sort fields array, the
direction is locked to that direction. In the above example, clicking
the resulting link would toggle sorting of the kind field, but the name
field would always sort ascending.

Also added was the ability to specify multiple default_order fields
with a hash:

  sort_link(:kind, [:kind, :name],
            :default_order => { :name => 'asc', :kind => 'desc' })

Clicking the resulting link will toggle the sort directions of both
`name` and `kind`, sorting the `name` field by default ascending, and
the `kind` field descending.
This commit is contained in:
Caleb Land 2014-09-30 18:58:33 -04:00
parent 9568dd48e0
commit bd22760223
2 changed files with 183 additions and 21 deletions

View File

@ -55,6 +55,7 @@ module Ransack
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 @@ module Ransack
raise TypeError, "First argument must be a Ransack::Search!" unless
Search === search
search_params = params[search.context.search_key].presence ||
{}.with_indifferent_access
# 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
attr_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
name = (
if args.size > 0 && !args.first.is_a?(Hash)
args.shift.to_s
else
Translate.attribute(attr_name, :context => search.context)
end
)
if existing_sort = search.sorts.detect { |s| s.name == attr_name }
prev_attr, prev_dir = existing_sort.name, existing_sort.dir
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
current_dir = prev_attr == attr_name ? prev_dir : nil
# 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
if current_dir
new_dir = current_dir == desc ? asc : desc
else
new_dir = default_order || asc
search_params = params[search.context.search_key].presence ||
{}.with_indifferent_access
# 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
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
sort_params << "#{attr_name} #{new_dir}"
end
# 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 @@ module Ransack
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,

View File

@ -145,6 +145,131 @@ module Ransack
}
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&amp;q%5Bs%5D%5B%5D=email+desc/
else
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&amp;q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
end
)
}
it {
should match /sort_link desc/
}
it {
should match /Full Name&nbsp;&#9660;/
}
end
describe '#sort_link with multiple search_keys defined as an array 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&amp;q%5Bs%5D%5B%5D=email+desc/
else
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+asc&amp;q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=email\+desc/
end
)
}
it {
should match /sort_link desc/
}
it {
should match /Full Name&nbsp;&#9660;/
}
end
describe '#sort_link with multiple search_keys defined as an array 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&amp;q%5Bs%5D%5B%5D=email+desc/
else
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&amp;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 defined as an array 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&amp;q%5Bs%5D%5B%5D=email+asc/
else
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&amp;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&amp;q%5Bs%5D%5B%5D=email+desc/
else
/people\?q(%5B|\[)s(%5D|\])(%5B|\[)(%5D|\])=name\+desc&amp;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' })