Commit e3f7892e authored by Sean McGivern's avatar Sean McGivern

Merge branch '3553-epics-list' into 'master'

Paginated epics list

Closes #3553

See merge request gitlab-org/gitlab-ee!3254
parents 800f226b bfe77277
......@@ -9,10 +9,7 @@ module IssuableActions
def show
respond_to do |format|
format.html do
render show_view
end
format.html
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
......@@ -154,10 +151,6 @@ module IssuableActions
end
end
def show_view
'show'
end
def serializer
raise NotImplementedError
end
......
......@@ -10,6 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
before_action :set_issuables_index, only: [:index]
# Allow write(create) issue
......
......@@ -159,7 +159,7 @@ module IssuablesHelper
label_names.join(', ')
end
def issuables_state_counter_text(issuable_type, state)
def issuables_state_counter_text(issuable_type, state = :all)
titles = {
opened: "Open"
}
......
......@@ -3,6 +3,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Noteable
include Referable
include Elastic::MergeRequestsSearch
include IgnorableColumn
include TimeTrackable
......
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- epics = EpicsFinder.new(current_user, group_id: @group.id).execute
- epics_items = ['epics#show', 'epics#index']
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show')
......@@ -43,6 +45,21 @@
%span
Contribution Analytics
-# TODO: Add the flag check to only show epics if available
= nav_link(path: epics_items) do
= link_to group_epics_path(@group) do
.nav-icon-container
= sprite_icon('epic')
%span.nav-item-name
Epics
%span.badge.count= number_with_delimiter(epics.count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: epics_items, html_options: { class: "fly-out-top-item" } ) do
= link_to group_epics_path(@group) do
%strong.fly-out-top-item-name
#{ _('Epics') }
%span.badge.count.epic_counter.fly-out-badge= number_with_delimiter(epics.count)
= nav_link(path: issues_sub_menu_items) do
= link_to issues_group_path(@group) do
.nav-icon-container
......
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
- issuables = @issues || @merge_requests
- issuables = @issues || @merge_requests || @epics
%ul.nav-links.issues-state-filters
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)}
- if type != :epics
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged)}
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)}
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)}
%li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
......
---
title: Add epics list and add epics to nav sidebar
merge_request:
author:
type: added
......@@ -12,7 +12,6 @@ module EE
def service_desk
@issues = @issuables
@users.push(::User.support_bot)
end
......
class Groups::EpicsController < Groups::ApplicationController
include IssuableActions
include IssuableCollections
before_action :epic
before_action :epic, except: :index
before_action :set_issuables_index, only: :index
before_action :authorize_update_issuable!, only: :update
skip_before_action :labels
def index
set_default_state
@epics = @issuables
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("groups/epics/_epics")
}
end
end
end
private
def epic
......@@ -38,7 +54,22 @@ class Groups::EpicsController < Groups::ApplicationController
Epics::UpdateService.new(nil, current_user, epic_params)
end
def show_view
'groups/ee/epics/show'
def set_issuables_index
@finder_type = EpicsFinder
super
end
def collection_type
@collection_type ||= 'Epic'
end
def preload_for_collection
@preload_for_collection ||= [:group, :author]
end
# 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)
def set_default_state
params[:state] = 'all'
end
end
class EpicsFinder < IssuableFinder
def klass
Epic
end
def execute
raise ArgumentError, 'group_id argument is missing' unless group
items = init_collection
items = by_created_at(items)
items = by_search(items)
items = by_author(items)
items = by_iids(items)
sort(items)
end
def row_count
execute.count
end
# we don't have states for epics for now this method (#4017)
def count_by_state
{
all: row_count
}
end
def group
return nil unless params[:group_id]
return @group if defined?(@group)
group = Group.find(params[:group_id])
group = nil unless Ability.allowed?(current_user, :read_epic, group)
@group = group
end
def init_collection
group.epics
end
end
......@@ -30,7 +30,12 @@ module EE
enable :destroy_epic
end
rule { auditor }.enable :read_group
rule { auditor }.policy do
enable :read_group
enable :read_epic
end
rule { admin }.enable :read_epic
rule { has_projects }.enable :read_epic
rule { admin | (can_owners_manage_ldap & owner) }.enable :admin_ldap_group_links
......
%li
.issue-box
.issue-info-container
.issue-main-info
.issue-title.title
%span.issue-title-text
= link_to epic.title, epic_path(epic)
.issuable-info
%span.issuable-reference
-# TODO: Use to_reference
= "&#{epic.iid}"
%span.issuable-authored.hidden-xs
&middot;
opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')}
by #{link_to_member(@group, epic.author, avatar: false)}
- page_title "Epics"
- if @epics.to_a.any?
= render 'shared/epics'
- else
= render 'shared/empty_states/epics'
.top-area
= render 'shared/issuable/nav', type: :epics
%ul.content-list.issuable-list
= render partial: 'groups/epics/epic', collection: @epics
= paginate @epics, theme: "gitlab"
.row.empty-state
.col-xs-12
.svg-content
= image_tag('illustrations/epics.svg')
.col-xs-12.text-center
.text-content
%h4
= _('Epics let you manage your portfolio of projects more efficiently and with less effort')
%p
= _('Track groups of issues that share a theme, across projects and milestones')
%button.btn.btn-new{ type: 'button' }
New epic
......@@ -9,6 +9,42 @@ describe Groups::EpicsController do
sign_in(user)
end
describe "GET #index" do
let!(:epic_list) { create_list(:epic, 2, group: group) }
before do
sign_in(user)
group.add_developer(user)
end
it "returns index" do
get :index, group_id: group
expect(response).to have_gitlab_http_status(200)
end
context 'with page param' do
let(:last_page) { group.epics.page.total_pages }
before do
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
it 'redirects to last_page if page number is larger than number of pages' do
get :index, group_id: group, page: (last_page + 1).to_param
expect(response).to redirect_to(group_epics_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
it 'renders the specified page' do
get :index, group_id: group, page: last_page.to_param
expect(assigns(:epics).current_page).to eq(last_page)
expect(response).to have_gitlab_http_status(200)
end
end
end
describe 'GET #show' do
def show_epic(format = :html)
get :show, group_id: group, id: epic.to_param, format: format
......@@ -20,7 +56,7 @@ describe Groups::EpicsController do
show_epic
expect(response.content_type).to eq 'text/html'
expect(response).to render_template 'groups/ee/epics/show'
expect(response).to render_template 'groups/epics/show'
end
context 'with unauthorized user' do
......
require 'spec_helper'
describe 'epics list', :js do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before do
sign_in(user)
end
context 'when epics exist for the group' do
let!(:epics) { create_list(:epic, 2, group: group) }
before do
visit group_epics_path(group)
end
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 .count')).to have_content('2')
end
it 'renders the list correctly' do
page.within('.page-with-new-nav .content') do
expect(find('.top-area')).to have_content('All 2')
within('.issuable-list') do
expect(page).to have_content(epics.first.title)
expect(page).to have_content(epics.second.title)
end
end
end
it 'renders the epic detail correctly after clicking the link' do
page.within('.page-with-new-nav .content .issuable-list') do
click_link(epics.first.title)
end
wait_for_requests
expect(page.find('.issuable-details h2.title')).to have_content(epics.first.title)
end
end
context 'when no epics exist for the group' do
it 'renders the empty list page' do
visit group_epics_path(group)
within('#content-body') do
expect(find('.empty-state h4'))
.to have_content('Epics let you manage your portfolio of projects more efficiently and with less effort')
end
end
end
end
require 'spec_helper'
describe EpicsFinder do
let(:user) { create(:user) }
let(:search_user) { create(:user) }
let(:group) { create(:group, :private) }
let(:another_group) { create(:group) }
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) }
let!(:epic3) { create(:epic, group: group, description: 'not so awesome') }
let!(:epic4) { create(:epic, group: another_group) }
describe '#execute' do
def epics(params = {})
params[:group_id] = group.id
described_class.new(search_user, params).execute
end
context 'without param' do
it 'raises an error when group_id param is missing' do
expect { described_class.new(search_user).execute }.to raise_error { ArgumentError }
end
end
context 'when user can not read epics of a group' do
it 'raises an error when group_id param is missing' do
expect { epics }.to raise_error { ArgumentError }
end
end
context 'wtih correct params' do
before do
group.add_developer(search_user)
end
it 'returns all epics that belong to the given group' do
expect(epics).to contain_exactly(epic1, epic2, epic3)
end
context 'by created_at' do
it 'returns all epics created before the given date' do
expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2)
end
it 'returns all epics created after the given date' do
expect(epics(created_after: 2.days.ago)).to contain_exactly(epic3)
end
it 'returns all epics created within the given interval' do
expect(epics(created_after: 5.days.ago, created_before: 1.day.ago)).to contain_exactly(epic2)
end
end
context 'by search' do
it 'returns all epics that match the search' do
expect(epics(search: 'awesome')).to contain_exactly(epic1, epic3)
end
end
context 'by author' do
it 'returns all epics authored by the given user' do
expect(epics(author_id: user.id)).to contain_exactly(epic2)
end
end
context 'by iids' do
it 'returns all epics by the given iids' do
expect(epics(iids: [epic1.iid, epic3.iid])).to contain_exactly(epic1, epic3)
end
end
end
end
end
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