diff --git a/app/views/queries/_form.html.erb b/app/views/queries/_form.html.erb
index 6a94712e01..00b65ed157 100644
--- a/app/views/queries/_form.html.erb
+++ b/app/views/queries/_form.html.erb
@@ -83,7 +83,16 @@
<%= label_tag "query_sort_criteria_direction_" + i.to_s,
l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
<%= select_tag("query[sort_criteria][#{i}][]",
- options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
+ options_for_select(
+ [
+ [],
+ [l(:label_ascending), 'asc'],
+ [l(:label_descending), 'desc'],
+ [l(:label_ascending_null_last), 'asc nulls last'],
+ [l(:label_descending_null_first), 'desc nulls first']
+ ],
+ @query.sort_criteria_order(i)
+ ),
:id => "query_sort_criteria_direction_" + i.to_s) %>
<% end %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 761e4194ca..4bc648f89d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1376,3 +1376,5 @@ en:
twofa_text_group_disabled: "This setting is only effective when the global two factor authentication setting is set to 'optional'. Currently, two factor authentication is disabled."
text_user_destroy_confirmation: "Are you sure you want to delete this user and remove all references to them? This cannot be undone. Often, locking a user instead of deleting them is the better solution. To confirm, please enter their login (%{login}) below."
text_project_destroy_enter_identifier: "To confirm, please enter the project's identifier (%{identifier}) below."
+ label_ascending_null_last: Ascending nulls last
+ label_descending_null_first: Descending nulls first
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index ed89f3a6a7..d049d35994 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -1513,3 +1513,5 @@ ru:
label_subtask: Subtask
label_default_query: Default query
field_default_project_query: Default project query
+ label_ascending_null_last: По возрастанию с пустыми значениями в конце
+ label_descending_null_first: По убыванию с пустыми значениями в начале
diff --git a/lib/redmine/sort_criteria.rb b/lib/redmine/sort_criteria.rb
index 8ef1ba7b5d..d2433d7252 100644
--- a/lib/redmine/sort_criteria.rb
+++ b/lib/redmine/sort_criteria.rb
@@ -34,7 +34,16 @@ module Redmine
end
def to_param
- self.collect {|k,o| k + (o == 'desc' ? ':desc' : '')}.join(',')
+ collect do |k, o|
+ k + (case o
+ when 'desc'
+ ':desc'
+ when /nulls (first|last)/
+ ":#{o}"
+ else
+ ''
+ end)
+ end.join(',')
end
def to_a
@@ -89,10 +98,20 @@ module Redmine
private
def normalize!
- self.reject! {|s| s.first.blank?}
- self.uniq! {|s| s.first}
- self.collect! {|s| s = Array(s); [s.first, (s.last == false || s.last.to_s == 'desc') ? 'desc' : 'asc']}
- self.replace self.first(3)
+ reject! {|s| s.first.blank? }
+ uniq! {|s| s.first }
+ collect! do |s|
+ s = Array(s)
+ [s.first,
+ if s.last == false || s.last.to_s == 'desc' # if sort is empty or desc => desc
+ 'desc'
+ elsif !s.last.blank? && s.size > 1 && /nulls (first|last)/.match?(s.last) # nulls sort
+ s.last
+ else
+ 'asc'
+ end]
+ end
+ replace first(3)
end
# Appends ASC/DESC to the sort criterion unless it has a fixed order
diff --git a/test/helpers/sort_helper_test.rb b/test/helpers/sort_helper_test.rb
index 163e48d6f9..363a111d02 100644
--- a/test/helpers/sort_helper_test.rb
+++ b/test/helpers/sort_helper_test.rb
@@ -102,6 +102,23 @@ class SortHelperTest < Redmine::HelperTest
assert_equal 'sort-by-foo-bar sort-asc', sort_css_classes
end
+ def test_nulls_sort
+ sort_init 'attr1', 'desc nulls first'
+ sort_update(['attr1', 'attr2'])
+
+ assert_equal ['attr1 DESC NULLS FIRST'], sort_clause
+ end
+
+ def test_params_nulls_sort
+ @sort_param = 'attr1,attr2:desc nulls last'
+
+ sort_init 'attr1', 'desc'
+ sort_update({'attr1' => 'table1.attr1', 'attr2' => 'table2.attr2'})
+
+ assert_equal ['table1.attr1 ASC', 'table2.attr2 DESC NULLS LAST'], sort_clause
+ assert_equal 'attr1,attr2:desc nulls last', @session['foo_bar_sort']
+ end
+
private
def controller_name; 'foo'; end