diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 91e875dca54..9cf25915e92 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -41,6 +41,7 @@ module IssuableCollections return if pagination_disabled? @issuables = @issuables.page(params[:page]) + @issuables = per_page_for_relative_position if params[:sort] == 'relative_position' @issuable_meta_data = issuable_meta_data(@issuables, collection_type) @total_pages = issuable_page_count end @@ -80,6 +81,11 @@ module IssuableCollections (row_count.to_f / limit).ceil end + # manual / relative_position sorting allows for 100 items on the page + def per_page_for_relative_position + @issuables.per(100) # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + def issuable_finder_for(finder_class) finder_class.new(current_user, finder_options) end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index f2d814e6930..26692934456 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -3,29 +3,30 @@ module SortingHelper def sort_options_hash { - sort_value_created_date => sort_title_created_date, - sort_value_downvotes => sort_title_downvotes, - sort_value_due_date => sort_title_due_date, - sort_value_due_date_later => sort_title_due_date_later, - sort_value_due_date_soon => sort_title_due_date_soon, - sort_value_label_priority => sort_title_label_priority, - sort_value_largest_group => sort_title_largest_group, - sort_value_largest_repo => sort_title_largest_repo, - sort_value_milestone => sort_title_milestone, - sort_value_milestone_later => sort_title_milestone_later, - sort_value_milestone_soon => sort_title_milestone_soon, - sort_value_name => sort_title_name, - sort_value_name_desc => sort_title_name_desc, - sort_value_oldest_created => sort_title_oldest_created, - sort_value_oldest_signin => sort_title_oldest_signin, - sort_value_oldest_updated => sort_title_oldest_updated, - sort_value_recently_created => sort_title_recently_created, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_popularity => sort_title_popularity, - sort_value_priority => sort_title_priority, - sort_value_upvotes => sort_title_upvotes, - sort_value_contacted_date => sort_title_contacted_date + sort_value_created_date => sort_title_created_date, + sort_value_downvotes => sort_title_downvotes, + sort_value_due_date => sort_title_due_date, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_label_priority => sort_title_label_priority, + sort_value_largest_group => sort_title_largest_group, + sort_value_largest_repo => sort_title_largest_repo, + sort_value_milestone => sort_title_milestone, + sort_value_milestone_later => sort_title_milestone_later, + sort_value_milestone_soon => sort_title_milestone_soon, + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_popularity => sort_title_popularity, + sort_value_priority => sort_title_priority, + sort_value_upvotes => sort_title_upvotes, + sort_value_contacted_date => sort_title_contacted_date, + sort_value_relative_position => sort_title_relative_position } end @@ -397,6 +398,10 @@ module SortingHelper s_('SortOptions|Recent last activity') end + def sort_title_relative_position + s_('SortOptions|Manual') + end + # Values. def sort_value_access_level_asc 'access_level_asc' @@ -545,4 +550,8 @@ module SortingHelper def sort_value_recently_last_activity 'last_activity_on_desc' end + + def sort_value_relative_position + 'relative_position' + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index eb5544f2a12..6da6fbe55cb 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,6 +58,7 @@ class Issue < ApplicationRecord scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') } + scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) } scope :preload_associations, -> { preload(:labels, project: :namespace) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } @@ -130,9 +131,10 @@ class Issue < ApplicationRecord def self.sort_by_attribute(method, excluded_labels: []) case method.to_s when 'closest_future_date' then order_closest_future_date - when 'due_date' then order_due_date_asc - when 'due_date_asc' then order_due_date_asc - when 'due_date_desc' then order_due_date_desc + when 'due_date' then order_due_date_asc + when 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + when 'relative_position' then order_relative_position_asc else super end diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml index 967f31c8325..9c151dc96f3 100644 --- a/app/views/shared/issuable/_sort_dropdown.html.haml +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -10,12 +10,13 @@ = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort %li - = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title) - = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title) - = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title) - = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title) - = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues - = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title) - = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title) + = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title) + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title) + = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title) + = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title) + = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues + = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title) + = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title) + = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title) = issuable_sort_direction_button(sort_value) diff --git a/changelogs/unreleased/9121-sort-relative-position.yml b/changelogs/unreleased/9121-sort-relative-position.yml new file mode 100644 index 00000000000..adc9e87e5bb --- /dev/null +++ b/changelogs/unreleased/9121-sort-relative-position.yml @@ -0,0 +1,5 @@ +--- +title: Allow issue list to be sorted by relative order +merge_request: 28566 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 72bff621059..c0b3eaf1e8b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9061,6 +9061,9 @@ msgstr "" msgid "SortOptions|Least popular" msgstr "" +msgid "SortOptions|Manual" +msgstr "" + msgid "SortOptions|Milestone due date" msgstr "" diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 0c46b43f080..32607fc5f56 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -130,6 +130,37 @@ describe Projects::IssuesController do end end + context 'with relative_position sorting' do + let!(:issue_list) { create_list(:issue, 2, project: project) } + + before do + sign_in(user) + project.add_developer(user) + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + end + + it 'overrides the number allowed on the page' do + get :index, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + sort: 'relative_position' + } + + expect(assigns(:issues).count).to eq 2 + end + + it 'allows the default number on the page' do + get :index, + params: { + namespace_id: project.namespace.to_param, + project_id: project + } + + expect(assigns(:issues).count).to eq 1 + end + end + context 'external authorization' do before do sign_in user diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index cc777cbf749..a5c7e9db2a1 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -93,6 +93,21 @@ describe Issue do end end + describe '#sort' do + let(:project) { create(:project) } + + context "by relative_position" do + let!(:issue) { create(:issue, project: project) } + let!(:issue2) { create(:issue, project: project, relative_position: 2) } + let!(:issue3) { create(:issue, project: project, relative_position: 1) } + + it "sorts asc with nulls at the end" do + issues = project.issues.sort_by_attribute('relative_position') + expect(issues).to eq([issue3, issue2, issue]) + end + end + end + describe '#card_attributes' do it 'includes the author name' do allow(subject).to receive(:author).and_return(double(name: 'Robert'))