Commit 6671e21f authored by Nick Thomas's avatar Nick Thomas

Merge branch '8035-show-closed-epics-in-roadmap' into 'master'

Show closed epics in roadmap

Closes #8035

See merge request gitlab-org/gitlab-ee!8658
parents d8d38332 5b55bfdc
......@@ -51,3 +51,5 @@ class UserPreference < ActiveRecord::Base
"#{field_key}_notes_filter"
end
end
UserPreference.prepend(EE::UserPreference)
......@@ -2838,6 +2838,7 @@ ActiveRecord::Schema.define(version: 20181204135932) do
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "epics_sort"
t.integer "roadmap_epics_state"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end
......
......@@ -6,6 +6,12 @@ An Epic within a group containing **Start date** and/or **Due date**
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.
![roadmap view](img/roadmap_view.png)
A dropdown allows you to show only open or closed epics. By default, all epics are shown.
![epics state dropdown](img/epics_state_dropdown.png)
Epics in the view can be sorted by:
- **Created date**
......@@ -15,8 +21,6 @@ Epics in the view can be sorted by:
Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [epics list view](../epics/index.md).
![roadmap view](img/roadmap_view.png)
## Timeline duration
Starting with [GitLab Ultimate][ee] 11.0, Roadmap supports three different date ranges; Quarters, Months (Default) and Weeks.
......
......@@ -54,6 +54,7 @@ export default () => {
filterQueryString,
presetType,
timeframe,
state: dataset.epicsState,
});
const store = new RoadmapStore(parseInt(dataset.groupId, 0), timeframe, presetType);
......
......@@ -184,6 +184,7 @@ export const getEpicsPathForPreset = ({
filterQueryString = '',
presetType = '',
timeframe = [],
state = 'all',
}) => {
let start;
let end;
......@@ -213,7 +214,7 @@ export const getEpicsPathForPreset = ({
const startDate = `${start.getFullYear()}-${start.getMonth() + 1}-${start.getDate()}`;
const endDate = `${end.getFullYear()}-${end.getMonth() + 1}-${end.getDate()}`;
epicsPath += `?start_date=${startDate}&end_date=${endDate}`;
epicsPath += `?state=${state}&start_date=${startDate}&end_date=${endDate}`;
if (filterQueryString) {
epicsPath += `&${filterQueryString}`;
......
......@@ -13,7 +13,7 @@ module Groups
def show
# Used to persist the order and show the correct sorting dropdown on UI.
@sort = set_sort_order
@epics_state = epics_state_in_user_preference || 'all'
@epics_count = EpicsFinder.new(current_user, group_id: @group.id).execute.count
end
......@@ -33,5 +33,19 @@ module Groups
Users::UpdateService.new(current_user, user: current_user, roadmap_layout: roadmap_layout).execute
end
def epics_state_in_user_preference
return unless current_user
preference = current_user.user_preference
if params[:state].present?
preference.roadmap_epics_state = Epic.states[params[:state]]
preference.save if preference.changed? && Gitlab::Database.read_write?
end
Epic.states.key(preference.roadmap_epics_state)
end
end
end
......@@ -77,4 +77,16 @@ module EpicsHelper
opts
end
def epic_state_dropdown_link(state, selected_state)
link_to epic_state_title(state), page_filter_path(state: state), class: state == selected_state ? 'is-active' : ''
end
def epic_state_title(state)
titles = {
"opened" => "Open"
}
_("%{state} epics") % { state: (titles[state.to_s] || state.to_s.humanize) }
end
end
module EE
module UserPreference
extend ActiveSupport::Concern
prepended do
validates :roadmap_epics_state, allow_nil: true, inclusion: {
in: ::Epic.states.values, message: "%{value} is not a valid epic state"
}
end
end
end
......@@ -9,6 +9,6 @@
- if @epics_count != 0
= 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, epics_state: @epics_state } }
- else
= render 'shared/empty_states/roadmap'
......@@ -19,6 +19,8 @@
%label.btn.btn-default.btn-roadmap-preset{ class: ("active" if is_weeks) }
%input{ type: 'radio', name: 'presetType', autocomplete: 'off', checked: ("checked" if is_weeks), value: 'WEEKS' }
= _('Weeks')
.filter-dropdown-container.append-right-default
= render 'shared/epic/state_dropdown'
= form_tag page_filter_path(without: [:author_id, :search]), method: :get, class: 'flex-fill filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
......
.dropdown.inline.dropdown-epics-state
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
= epic_state_title(@epics_state)
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-selectable
%li
= epic_state_dropdown_link('all', @epics_state)
.dropdown-divider
%li
= epic_state_dropdown_link('opened', @epics_state)
%li
= epic_state_dropdown_link('closed', @epics_state)
---
title: Add epics state filtering in roadmap view
merge_request: 8658
author:
type: changed
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddEpicsStateToUserPreferences < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :user_preferences, :roadmap_epics_state, :integer
end
end
......@@ -8,6 +8,7 @@ describe 'group epic roadmap', :js do
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_label) { '#js-dropdown-label' }
let(:filter_dropdown) { find("#{js_dropdown_label} .filter-dropdown") }
let(:state_dropdown) { find('.dropdown-epics-state') }
let(:bug_label) { create(:group_label, group: group, title: 'Bug') }
let(:critical_label) { create(:group_label, group: group, title: 'Critical') }
......@@ -27,6 +28,7 @@ describe 'group epic roadmap', :js do
context 'when epics exist for the group' do
let!(:epic_with_bug) { create(:labeled_epic, group: group, start_date: 10.days.ago, end_date: 1.day.ago, labels: [bug_label]) }
let!(:epic_with_critical) { create(:labeled_epic, group: group, start_date: 20.days.ago, end_date: 2.days.ago, labels: [critical_label]) }
let!(:closed_epic) { create(:epic, :closed, group: group, start_date: 20.days.ago, end_date: 2.days.ago) }
before do
visit group_roadmap_path(group)
......@@ -49,7 +51,7 @@ describe 'group epic roadmap', :js do
end
it 'renders the sort dropdown correctly' do
page.within('.content-wrapper .content .epics-filters') do
page.within('.content-wrapper .content .epics-other-filters') do
expect(page).to have_css('.filter-dropdown-container')
find('.dropdown-toggle').click
page.within('.dropdown-menu') do
......@@ -69,6 +71,40 @@ describe 'group epic roadmap', :js do
end
it 'renders all group epics within roadmap' do
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 3)
end
end
end
describe 'roadmap page with epics state filter' do
before do
state_dropdown.find('.dropdown-toggle').click
end
it 'renders open epics only' do
state_dropdown.find('a', text: 'Open epics').click
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 2)
end
end
it 'renders closed epics only' do
state_dropdown.find('a', text: 'Closed epics').click
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 1)
end
end
it 'saves last selected epic state' do
state_dropdown.find('a', text: 'Open epics').click
visit group_roadmap_path(group)
wait_for_requests
expect(state_dropdown.find('.dropdown-toggle')).to have_text("Open epics")
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 2)
end
......
......@@ -149,7 +149,7 @@ describe('getEpicsPathForPreset', () => {
presetType: PRESET_TYPES.QUARTERS,
});
expect(epicsPath).toBe(`${basePath}?start_date=2017-10-1&end_date=2019-3-31`);
expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-10-1&end_date=2019-3-31`);
});
it('returns epics path string based on provided basePath and timeframe for Months', () => {
......@@ -160,7 +160,7 @@ describe('getEpicsPathForPreset', () => {
presetType: PRESET_TYPES.MONTHS,
});
expect(epicsPath).toBe(`${basePath}?start_date=2017-12-1&end_date=2018-6-30`);
expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-12-1&end_date=2018-6-30`);
});
it('returns epics path string based on provided basePath and timeframe for Weeks', () => {
......@@ -171,7 +171,7 @@ describe('getEpicsPathForPreset', () => {
presetType: PRESET_TYPES.WEEKS,
});
expect(epicsPath).toBe(`${basePath}?start_date=2017-12-24&end_date=2018-2-3`);
expect(epicsPath).toBe(`${basePath}?state=all&start_date=2017-12-24&end_date=2018-2-3`);
});
it('returns epics path string while preserving filterQueryString', () => {
......@@ -184,7 +184,7 @@ describe('getEpicsPathForPreset', () => {
});
expect(epicsPath).toBe(
`${basePath}?start_date=2017-12-1&end_date=2018-6-30&scope=all&utf8=✓&state=opened&label_name[]=Bug`,
`${basePath}?state=all&start_date=2017-12-1&end_date=2018-6-30&scope=all&utf8=✓&state=opened&label_name[]=Bug`,
);
});
});
require 'spec_helper'
describe UserPreference do
let(:user_preference) { create(:user_preference) }
shared_examples 'updates roadmap_epics_state' do |state|
it 'saves roadmap_epics_state in user_preference' do
user_preference.update(roadmap_epics_state: state)
expect(user_preference.reload.roadmap_epics_state).to eq(state)
end
end
describe 'roadmap_epics_state' do
context 'when set to open epics' do
it_behaves_like 'updates roadmap_epics_state', Epic.states[:opened]
end
context 'when set to closed epics' do
it_behaves_like 'updates roadmap_epics_state', Epic.states[:closed]
end
context 'when reset to all epics' do
it_behaves_like 'updates roadmap_epics_state', nil
end
end
end
......@@ -171,6 +171,9 @@ msgstr ""
msgid "%{percent}%% complete"
msgstr ""
msgid "%{state} epics"
msgstr ""
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
msgstr[0] ""
......
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