Commit b600f42c authored by Jarka Kadlecová's avatar Jarka Kadlecová Committed by Jarka Košanová

Add support for sorting epics

parent 9554796e
module SortingHelper module SortingHelper
prepend ::EE::SortingHelper
def sort_options_hash def sort_options_hash
{ {
sort_value_created_date => sort_title_created_date, sort_value_created_date => sort_title_created_date,
......
...@@ -77,7 +77,7 @@ It will display a dropdown menu, from which you can add an author. You can also ...@@ -77,7 +77,7 @@ It will display a dropdown menu, from which you can add an author. You can also
text to search by epic title or description. When done, press <kbd>Enter</kbd> on your text to search by epic title or description. When done, press <kbd>Enter</kbd> on your
keyboard to filter the list. keyboard to filter the list.
You can also sort epics list by **Created date** or **Last updated**. You can also sort epics list by **Created date**, **Last updated**, **Planned start date**, or **Planned finish date**.
![epics sort](img/epics_sort.png) ![epics sort](img/epics_sort.png)
......
...@@ -6,6 +6,8 @@ An Epic within a group containing **Planned start date** and/or **Planned finish ...@@ -6,6 +6,8 @@ An Epic within a group containing **Planned start date** and/or **Planned finish
can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page
shows such a visualization for all the epics which are under a group and/or its subgroups. shows such a visualization for all the epics which are under a group and/or its subgroups.
Epics in the view can be sorted by **Created date**, **Last updated**, **Planned start date**, or **Planned finish date**.
![roadmap view](img/roadmap_view.png) ![roadmap view](img/roadmap_view.png)
## Timeline duration ## Timeline duration
......
# frozen_string_literal: true
module EpicsActions
private
def finder_type
EpicsFinder
end
def collection_type
@collection_type ||= 'Epic'
end
def default_sort_order
sort_value_end_date
end
def update_cookie_value(value)
case value
when 'start_date_asc' then sort_value_start_date
when 'end_date_asc' then sort_value_end_date
else
super(value)
end
end
end
...@@ -4,6 +4,7 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -4,6 +4,7 @@ class Groups::EpicsController < Groups::ApplicationController
include ToggleAwardEmoji include ToggleAwardEmoji
include ToggleSubscriptionAction include ToggleSubscriptionAction
include RendersNotes include RendersNotes
include EpicsActions
before_action :check_epics_available! before_action :check_epics_available!
before_action :epic, except: [:index, :create] before_action :epic, except: [:index, :create]
...@@ -90,14 +91,6 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -90,14 +91,6 @@ class Groups::EpicsController < Groups::ApplicationController
EpicsFinder EpicsFinder
end end
def collection_type
@collection_type ||= 'Epic'
end
# we don't support custom sorting for epics and therefore don't want to use the issuable_sort cookie
def set_sort_order_from_cookie
end
def preload_for_collection def preload_for_collection
@preload_for_collection ||= [:group, :author] @preload_for_collection ||= [:group, :author]
end end
...@@ -113,6 +106,7 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -113,6 +106,7 @@ class Groups::EpicsController < Groups::ApplicationController
end end
def filter_params def filter_params
set_sort_order_from_cookie
super.merge(start_date: params[:start_date], end_date: params[:end_date]) super.merge(start_date: params[:start_date], end_date: params[:end_date])
end end
end end
module Groups module Groups
class RoadmapController < Groups::ApplicationController class RoadmapController < Groups::ApplicationController
include IssuableCollections
include EpicsActions
before_action :check_epics_available!
before_action :group before_action :group
before_action :persist_roadmap_layout, only: [:show] before_action :persist_roadmap_layout, only: [:show]
def show def show
# show roadmap for a group # show roadmap for a group
set_sort_order_from_cookie
@sort = params[:sort] || default_sort_order
@epics_count = EpicsFinder.new(current_user, group_id: @group.id).execute.count @epics_count = EpicsFinder.new(current_user, group_id: @group.id).execute.count
end end
......
# frozen_string_literal: true
module EE
module SortingHelper
extend ::Gitlab::Utils::Override
override :sort_options_hash
def sort_options_hash
{
sort_value_start_date => sort_title_start_date,
sort_value_end_date => sort_title_end_date
}.merge(super)
end
def sort_title_start_date
s_('SortOptions|Planned start date')
end
def sort_title_end_date
s_('SortOptions|Planned finish date')
end
def sort_value_start_date
'start_date_asc'
end
def sort_value_end_date
'end_date_asc'
end
end
end
...@@ -29,6 +29,14 @@ module EE ...@@ -29,6 +29,14 @@ module EE
nulls_first = ::Gitlab::Database.postgresql? ? 'NULLS FIRST' : '' nulls_first = ::Gitlab::Database.postgresql? ? 'NULLS FIRST' : ''
reorder("COALESCE(start_date, end_date) ASC #{nulls_first}") reorder("COALESCE(start_date, end_date) ASC #{nulls_first}")
end end
scope :order_start_date_asc, -> do
reorder(::Gitlab::Database.nulls_last_order('start_date'), 'id DESC')
end
scope :order_end_date_asc, -> do
reorder(::Gitlab::Database.nulls_last_order('end_date'), 'id DESC')
end
end end
class_methods do class_methods do
...@@ -71,8 +79,10 @@ module EE ...@@ -71,8 +79,10 @@ module EE
end end
def order_by(method) def order_by(method)
if method.to_s == 'start_or_end_date' case method.to_s
order_start_or_end_date_asc when 'start_or_end_date' then order_start_or_end_date_asc
when 'start_date_asc' then order_start_date_asc
when 'end_date_asc' then order_end_date_asc
else else
super super
end end
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- has_filters_applied = params[:label_name].present? || params[:author_username].present? || params[:search].present? - has_filters_applied = params[:label_name].present? || params[:author_username].present? || params[:search].present?
- if @epics_count != 0 - if @epics_count != 0
= render 'shared/epic/search_bar', type: :epics, hide_sort_dropdown: true, show_roadmap_presets: true = render 'shared/epic/search_bar', type: :epics, show_roadmap_presets: true
#js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), has_filters_applied: "#{has_filters_applied}", new_epic_endpoint: group_epics_path(@group), preset_type: roadmap_layout } } #js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), has_filters_applied: "#{has_filters_applied}", new_epic_endpoint: group_epics_path(@group), preset_type: roadmap_layout } }
- else - else
......
...@@ -8,3 +8,5 @@ ...@@ -8,3 +8,5 @@
%li %li
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
= sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by) = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
= sortable_item(sort_title_start_date, page_filter_path(sort: sort_value_start_date, label: true), sorted_by)
= sortable_item(sort_title_end_date, page_filter_path(sort: sort_value_end_date, label: true), sorted_by)
---
title: Add support for sorting epics
merge_request: 6885
author:
type: added
...@@ -57,6 +57,13 @@ describe Groups::EpicsController do ...@@ -57,6 +57,13 @@ describe Groups::EpicsController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'stores sorting param in a cookie' do
get :index, group_id: group, sort: 'start_date_asc'
expect(cookies['epic_sort']).to eq('start_date_asc')
expect(response).to have_gitlab_http_status(200)
end
context 'with page param' do context 'with page param' do
let(:last_page) { group.epics.page.total_pages } let(:last_page) { group.epics.page.total_pages }
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::RoadmapController do
let(:group) { create(:group, :private) }
let(:epic) { create(:epic, group: group) }
let(:user) { create(:user) }
describe '#show' do
before do
sign_in(user)
group.add_developer(user)
end
context 'when epics feature is disabled' do
it "returns 404 status" do
get :show, group_id: group
expect(response).to have_gitlab_http_status(404)
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
it "returns 200 status" do
get :show, group_id: group
expect(response).to have_gitlab_http_status(200)
end
it 'stores sorting param in a cookie' do
get :show, group_id: group, sort: 'start_date_asc'
expect(cookies['epic_sort']).to eq('start_date_asc')
expect(response).to have_gitlab_http_status(200)
end
end
end
end
...@@ -11,7 +11,9 @@ describe 'epics list', :js do ...@@ -11,7 +11,9 @@ describe 'epics list', :js do
end end
context 'when epics exist for the group' do context 'when epics exist for the group' do
let!(:epics) { create_list(:epic, 2, group: group) } let!(:epic1) { create(:epic, group: group, end_date: 10.days.ago) }
let!(:epic2) { create(:epic, group: group, start_date: 2.days.ago) }
let!(:epic3) { create(:epic, group: group, start_date: 10.days.ago, end_date: 5.days.ago) }
before do before do
visit group_epics_path(group) visit group_epics_path(group)
...@@ -19,7 +21,7 @@ describe 'epics list', :js do ...@@ -19,7 +21,7 @@ describe 'epics list', :js do
it 'shows the epics in the navigation sidebar' do it 'shows the epics in the navigation sidebar' do
expect(first('.nav-sidebar .active a .nav-item-name')).to have_content('Epics') expect(first('.nav-sidebar .active a .nav-item-name')).to have_content('Epics')
expect(first('.nav-sidebar .active a .count')).to have_content('2') expect(first('.nav-sidebar .active a .count')).to have_content('3')
end end
it 'renders the filtered search bar correctly' do it 'renders the filtered search bar correctly' do
...@@ -28,24 +30,78 @@ describe 'epics list', :js do ...@@ -28,24 +30,78 @@ describe 'epics list', :js do
end end
end end
it 'renders the list correctly' do it 'sorts by end_date ASC by default' do
expect(page).to have_button('Planned finish date')
page.within('.content-wrapper .content') do
expect(find('.top-area')).to have_content('All 3')
page.within(".issuable-list") do
page.within("li:nth-child(1)") do
expect(page).to have_content(epic1.title)
end
page.within("li:nth-child(2)") do
expect(page).to have_content(epic3.title)
end
page.within("li:nth-child(3)") do
expect(page).to have_content(epic2.title)
end
end
end
end
it 'sorts by the selected value and stores the selection for epic list & roadmap' do
page.within('.epics-other-filters') do
click_button 'Planned finish date'
sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text)
expect(sort_options[0]).to eq('Created date')
expect(sort_options[1]).to eq('Last updated')
expect(sort_options[2]).to eq('Planned start date')
expect(sort_options[3]).to eq('Planned finish date')
click_link 'Planned start date'
end
expect(page).to have_button('Planned start date')
page.within('.content-wrapper .content') do page.within('.content-wrapper .content') do
expect(find('.top-area')).to have_content('All 2') expect(find('.top-area')).to have_content('All 3')
within('.issuable-list') do
expect(page).to have_content(epics.first.title) page.within(".issuable-list") do
expect(page).to have_content(epics.second.title) page.within("li:nth-child(1)") do
expect(page).to have_content(epic3.title)
end
page.within("li:nth-child(2)") do
expect(page).to have_content(epic2.title)
end
page.within("li:nth-child(3)") do
expect(page).to have_content(epic1.title)
end
end end
end end
visit group_epics_path(group)
expect(page).to have_button('Planned start date')
visit group_roadmap_path(group)
expect(page).to have_button('Planned start date')
end end
it 'renders the epic detail correctly after clicking the link' do it 'renders the epic detail correctly after clicking the link' do
page.within('.content-wrapper .content .issuable-list') do page.within('.content-wrapper .content .issuable-list') do
click_link(epics.first.title) click_link(epic1.title)
end end
wait_for_requests wait_for_requests
expect(page.find('.issuable-details h2.title')).to have_content(epics.first.title) expect(page.find('.issuable-details h2.title')).to have_content(epic1.title)
end end
end end
......
...@@ -43,8 +43,22 @@ describe 'group epic roadmap', :js do ...@@ -43,8 +43,22 @@ describe 'group epic roadmap', :js do
end end
it 'renders the filtered search bar correctly' do it 'renders the filtered search bar correctly' do
page.within('.content-wrapper .content') do page.within('.content-wrapper .content .epics-filters') do
expect(page).to have_css('.epics-filters') expect(page).to have_css('.filtered-search-box')
end
end
it 'renders the sort dropdown correctly' do
page.within('.content-wrapper .content .epics-filters') do
expect(page).to have_css('.filter-dropdown-container')
find('.dropdown-toggle').click
page.within('.dropdown-menu') do
expect(page).to have_selector('li a', count: 4)
expect(page).to have_content('Created date')
expect(page).to have_content('Last updated')
expect(page).to have_content('Planned start date')
expect(page).to have_content('Planned finish date')
end
end end
end end
......
...@@ -6,7 +6,7 @@ describe EpicsFinder do ...@@ -6,7 +6,7 @@ describe EpicsFinder do
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
let(:another_group) { create(:group) } let(:another_group) { create(:group) }
let!(:epic1) { create(:epic, group: group, title: 'This is awesome epic', created_at: 1.week.ago) } let!(:epic1) { create(:epic, group: group, title: 'This is awesome epic', created_at: 1.week.ago) }
let!(:epic2) { create(:epic, group: group, created_at: 4.days.ago, author: user, start_date: 2.days.ago) } let!(:epic2) { create(:epic, group: group, created_at: 4.days.ago, author: user, start_date: 2.days.ago, end_date: 3.days.from_now) }
let!(:epic3) { create(:epic, group: group, description: 'not so awesome', start_date: 5.days.ago, end_date: 3.days.ago) } let!(:epic3) { create(:epic, group: group, description: 'not so awesome', start_date: 5.days.ago, end_date: 3.days.ago) }
let!(:epic4) { create(:epic, group: another_group) } let!(:epic4) { create(:epic, group: another_group) }
...@@ -61,6 +61,16 @@ describe EpicsFinder do ...@@ -61,6 +61,16 @@ describe EpicsFinder do
expect(amount).to be <= 7 expect(amount).to be <= 7
end end
context 'sorting' do
it 'sorts correctly when supported sorting param provided' do
expect(epics(sort: :start_date_asc)).to eq([epic3, epic2, epic1])
end
it 'sorts by id when not supported sorting param provided' do
expect(epics(sort: :not_supported_param)).to eq([epic3, epic2, epic1])
end
end
context 'by created_at' do context 'by created_at' do
it 'returns all epics created before the given date' do it 'returns all epics created before the given date' do
expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2) expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2)
......
...@@ -30,16 +30,42 @@ describe Epic do ...@@ -30,16 +30,42 @@ describe Epic do
end end
end end
describe '.order_start_or_end_date_asc' do describe 'ordering' do
let(:group) { create(:group) } let!(:epic1) { create(:epic, start_date: 7.days.ago, end_date: 3.days.ago, updated_at: 3.days.ago, created_at: 7.days.ago) }
let!(:epic2) { create(:epic, start_date: 3.days.ago, updated_at: 10.days.ago, created_at: 12.days.ago) }
let!(:epic3) { create(:epic, end_date: 5.days.ago, updated_at: 5.days.ago, created_at: 6.days.ago) }
let!(:epic4) { create(:epic) }
def epics(order_by)
described_class.order_by(order_by)
end
it 'orders by start_or_end_date' do
expect(epics(:start_or_end_date)).to eq([epic4, epic1, epic3, epic2])
end
it 'orders by start_date ASC' do
expect(epics(:start_date_asc)).to eq([epic1, epic2, epic4, epic3])
end
it 'returns epics sorted by start or end date' do it 'orders by end_date ASC' do
epic1 = create(:epic, group: group, start_date: 7.days.ago, end_date: 3.days.ago) expect(epics(:end_date_asc)).to eq([epic3, epic1, epic4, epic2])
epic2 = create(:epic, group: group, start_date: 3.days.ago) end
epic3 = create(:epic, group: group, end_date: 5.days.ago)
epic4 = create(:epic, group: group) it 'orders by updated_at ASC' do
expect(epics(:updated_asc)).to eq([epic2, epic3, epic1, epic4])
end
it 'orders by updated_at DESC' do
expect(epics(:updated_desc)).to eq([epic4, epic1, epic3, epic2])
end
it 'orders by created_at ASC' do
expect(epics(:created_asc)).to eq([epic2, epic1, epic3, epic4])
end
expect(described_class.order_start_or_end_date_asc).to eq([epic4, epic1, epic3, epic2]) it 'orders by created_at DESC' do
expect(epics(:created_desc)).to eq([epic4, epic3, epic1, epic2])
end end
end end
......
...@@ -6744,6 +6744,12 @@ msgstr "" ...@@ -6744,6 +6744,12 @@ msgstr ""
msgid "SortOptions|Oldest updated" msgid "SortOptions|Oldest updated"
msgstr "" msgstr ""
msgid "SortOptions|Planned finish date"
msgstr ""
msgid "SortOptions|Planned start date"
msgstr ""
msgid "SortOptions|Popularity" msgid "SortOptions|Popularity"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment