Commit 2f04dac8 authored by Annabel Dunstone Gray's avatar Annabel Dunstone Gray

Merge branch '7014-epic-status-tabs' into 'master'

Add Open/Closed epics tabs in list view

Closes #7014

See merge request gitlab-org/gitlab-ee!7424
parents 7bbe8949 69807e09
...@@ -128,7 +128,7 @@ class IssuableFinder ...@@ -128,7 +128,7 @@ class IssuableFinder
labels_count = 1 if use_cte_for_search? labels_count = 1 if use_cte_for_search?
finder.execute.reorder(nil).group(:state).count.each do |key, value| finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[Array(key).last.to_sym] += value / labels_count counts[count_key(key)] += value / labels_count
end end
counts[:all] = counts.values.sum counts[:all] = counts.values.sum
...@@ -297,6 +297,10 @@ class IssuableFinder ...@@ -297,6 +297,10 @@ class IssuableFinder
klass.all klass.all
end end
def count_key(value)
Array(value).last.to_sym
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def by_scope(items) def by_scope(items)
return items.none if current_user_related? && !current_user return items.none if current_user_related? && !current_user
......
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
<template> <template>
<div class="dropdown new-epic-dropdown"> <div class="dropdown new-epic-dropdown">
<button <button
class="btn btn-success " class="btn btn-success"
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
@click="focusInput" @click="focusInput"
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
:disabled="isCreatingDisabled" :disabled="isCreatingDisabled"
:loading="creating" :loading="creating"
:label="buttonLabel" :label="buttonLabel"
container-class="btn btn-success btn-inverted" container-class="btn btn-success btn-inverted prepend-top-10"
@click.stop="createEpic" @click.stop="createEpic"
/> />
</div> </div>
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
.btn-success { .btn-success {
display: flex; display: flex;
margin-top: $gl-btn-padding;
} }
} }
......
...@@ -101,7 +101,7 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -101,7 +101,7 @@ class Groups::EpicsController < Groups::ApplicationController
# we need to override the default state which is opened for now because we don't have # we need to override the default state which is opened for now because we don't have
# states for epics and need all as default for navigation to work correctly (#4017) # states for epics and need all as default for navigation to work correctly (#4017)
def set_default_state def set_default_state
params[:state] = 'all' params[:state] = 'opened' if params[:state].blank?
end end
def authorize_create_epic! def authorize_create_epic!
......
...@@ -11,28 +11,12 @@ class EpicsFinder < IssuableFinder ...@@ -11,28 +11,12 @@ class EpicsFinder < IssuableFinder
items = by_search(items) items = by_search(items)
items = by_author(items) items = by_author(items)
items = by_timeframe(items) items = by_timeframe(items)
items = by_state(items)
items = by_label(items) items = by_label(items)
sort(items) sort(items)
end end
def row_count
count = execute.count
# When filtering by multiple labels, count returns a hash of
# records grouped by id - so we just have to get length of the Hash.
# Once we have state for epics, we can use default issuables row_count
# method.
count.is_a?(Hash) ? count.length : count
end
# we don't have states for epics for now this method (#4017)
def count_by_state
{
all: row_count
}
end
def group def group
return nil unless params[:group_id] return nil unless params[:group_id]
return @group if defined?(@group) return @group if defined?(@group)
...@@ -53,6 +37,10 @@ class EpicsFinder < IssuableFinder ...@@ -53,6 +37,10 @@ class EpicsFinder < IssuableFinder
private private
def count_key(value)
Epic.states.invert[Array(value).last].to_sym
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def groups_user_can_read_epics(groups) def groups_user_can_read_epics(groups)
groups = Gitlab::GroupPlansPreloader.new.preload(groups) groups = Gitlab::GroupPlansPreloader.new.preload(groups)
......
- has_filters_applied = params[:label_name].present? || params[:author_username].present? || params[:search].present?
- page_title "Epics" - page_title "Epics"
- if has_filters_applied || @epics.to_a.any? .top-area
.top-area = render 'shared/issuable/epic_nav', type: :epics
= render 'shared/issuable/epic_nav', type: :epics .nav-controls
.nav-controls - if can?(current_user, :create_epic, @group)
- if can?(current_user, :create_epic, @group) #new-epic-app{ data: { endpoint: request.url, 'align-right' => true } }
#new-epic-app{ data: { endpoint: request.url, 'align-right' => true } }
= render 'shared/epic/search_bar', type: :epics = render 'shared/epic/search_bar', type: :epics
- if @epics.to_a.any? - if @epics.to_a.any?
= render 'shared/epics' = render 'shared/epics'
......
%ul.content-list.issuable-list .card.card-small.card-without-border
= render partial: 'groups/epics/epic', collection: @epics %ul.content-list.issuable-list
= render partial: 'groups/epics/epic', collection: @epics
= paginate @epics, theme: "gitlab" = paginate @epics, theme: "gitlab"
...@@ -2,5 +2,13 @@ ...@@ -2,5 +2,13 @@
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
- display_count = local_assigns.fetch(:display_count, :true) - display_count = local_assigns.fetch(:display_count, :true)
%ul.nav-links.epics-state-filters %ul.nav-links.mobile-separator.epics-state-filters
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: (_("Filter by %{issuable_type} that are currently opened.") % { issuable_type: page_context_word }), data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: (_("Filter by %{issuable_type} that are currently closed.") % { issuable_type: page_context_word }), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count) = render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
---
title: Add Open/Closed epics tabs in list view
merge_request: 7424
author:
type: added
...@@ -13,6 +13,15 @@ FactoryBot.define do ...@@ -13,6 +13,15 @@ FactoryBot.define do
due_date_is_fixed true due_date_is_fixed true
end end
trait :opened do
state :opened
end
trait :closed do
state :closed
closed_at { Time.now }
end
factory :labeled_epic do factory :labeled_epic do
transient do transient do
labels [] labels []
......
...@@ -19,6 +19,19 @@ describe 'epics list', :js do ...@@ -19,6 +19,19 @@ describe 'epics list', :js do
visit group_epics_path(group) visit group_epics_path(group)
end end
it 'shows epics tabs for each status type' do
page.within('.epics-state-filters') do
expect(page).to have_selector('li > a#state-opened')
expect(find('li > a#state-opened')[:title]).to eq('Filter by epics that are currently opened.')
expect(page).to have_selector('li > a#state-closed')
expect(find('li > a#state-closed')[:title]).to eq('Filter by epics that are currently closed.')
expect(page).to have_selector('li > a#state-all')
expect(find('li > a#state-all')[:title]).to eq('Show all epics.')
end
end
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('3') expect(first('.nav-sidebar .active a .count')).to have_content('3')
...@@ -106,13 +119,23 @@ describe 'epics list', :js do ...@@ -106,13 +119,23 @@ describe 'epics list', :js do
end end
context 'when no epics exist for the group' do context 'when no epics exist for the group' do
it 'renders the empty list page' do before do
visit group_epics_path(group) visit group_epics_path(group)
end
it 'renders the empty list page' do
within('#content-body') do within('#content-body') do
expect(find('.empty-state h4')) expect(find('.empty-state h4'))
.to have_content('Epics let you manage your portfolio of projects more efficiently and with less effort') .to have_content('Epics let you manage your portfolio of projects more efficiently and with less effort')
end end
end end
it 'shows epics tabs for each status type' do
page.within('.epics-state-filters') do
expect(page).to have_selector('li > a#state-opened')
expect(page).to have_selector('li > a#state-closed')
expect(page).to have_selector('li > a#state-all')
end
end
end end
end end
...@@ -5,10 +5,10 @@ describe EpicsFinder do ...@@ -5,10 +5,10 @@ describe EpicsFinder do
let(:search_user) { create(:user) } let(:search_user) { create(:user) }
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, :opened, 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, end_date: 3.days.from_now) } let!(:epic2) { create(:epic, :opened, 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, :closed, 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, :closed, group: another_group) }
describe '#execute' do describe '#execute' do
def epics(params = {}) def epics(params = {})
...@@ -106,6 +106,12 @@ describe EpicsFinder do ...@@ -106,6 +106,12 @@ describe EpicsFinder do
end end
end end
context 'by state' do
it 'returns all epics with given state' do
expect(epics(state: :closed)).to contain_exactly(epic3)
end
end
context 'when subgroups are supported', :nested_groups do context 'when subgroups are supported', :nested_groups do
let(:subgroup) { create(:group, :private, parent: group) } let(:subgroup) { create(:group, :private, parent: group) }
let(:subgroup2) { create(:group, :private, parent: subgroup) } let(:subgroup2) { create(:group, :private, parent: subgroup) }
...@@ -184,4 +190,17 @@ describe EpicsFinder do ...@@ -184,4 +190,17 @@ describe EpicsFinder do
expect(described_class.new(search_user, params).row_count).to eq(1) expect(described_class.new(search_user, params).row_count).to eq(1)
end end
end end
describe '#count_by_state' do
before do
group.add_developer(search_user)
stub_licensed_features(epics: true)
end
it 'returns correct counts' do
results = described_class.new(search_user, group_id: group.id).count_by_state
expect(results).to eq('opened' => 2, 'closed' => 1, 'all' => 3)
end
end
end end
...@@ -3212,6 +3212,12 @@ msgstr "" ...@@ -3212,6 +3212,12 @@ msgstr ""
msgid "Filter" msgid "Filter"
msgstr "" msgstr ""
msgid "Filter by %{issuable_type} that are currently closed."
msgstr ""
msgid "Filter by %{issuable_type} that are currently opened."
msgstr ""
msgid "Filter by commit message" msgid "Filter by commit message"
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