Commit b36a8642 authored by Simon Knox's avatar Simon Knox

Merge branch 'psi-empty-milestones' into 'master'

More helpful empty states for milestones

See merge request gitlab-org/gitlab!81190
parents 4c89b987 e3ebb7c4
...@@ -26,6 +26,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -26,6 +26,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestone_states = Milestone.states_count(@project)
# We need to show group milestones in the JSON response # We need to show group milestones in the JSON response
# so that people can filter by and assign group milestones, # so that people can filter by and assign group milestones,
# but we don't need to show them on the project milestones page itself. # but we don't need to show them on the project milestones page itself.
......
...@@ -63,21 +63,6 @@ module TimeboxesHelper ...@@ -63,21 +63,6 @@ module TimeboxesHelper
issues.size issues.size
end end
# Returns count of milestones for different states
# Uses explicit hash keys as the 'opened' state URL params differs from the db value
# and we need to add the total
# rubocop: disable CodeReuse/ActiveRecord
def milestone_counts(milestones)
counts = milestones.reorder(nil).group(:state).count
{
opened: counts['active'] || 0,
closed: counts['closed'] || 0,
all: counts.values.sum || 0
}
end
# rubocop: enable CodeReuse/ActiveRecord
def milestone_progress_tooltip_text(milestone) def milestone_progress_tooltip_text(milestone)
has_issues = milestone.total_issues_count > 0 has_issues = milestone.total_issues_count > 0
......
...@@ -12,16 +12,29 @@ ...@@ -12,16 +12,29 @@
path: '-/milestones/new', label: 'New milestone', path: '-/milestones/new', label: 'New milestone',
include_groups: true, type: :milestones include_groups: true, type: :milestones
.top-area - if @milestone_states.any? { |name, count| count > 0 }
= render 'shared/milestones_filter', counts: @milestone_states .top-area
.nav-controls = render 'shared/milestones_filter', counts: @milestone_states
= render 'shared/milestones/search_form' .nav-controls
= render 'shared/milestones/search_form'
- if @milestones.blank? - if @milestones.blank?
= render 'shared/empty_states/milestones' = render 'shared/empty_states/milestones_tab', active_tab: params[:state] do
- if current_user
.page-title-controls
= render 'shared/new_project_item_select',
path: '-/milestones/new', label: 'New milestone',
include_groups: true, type: :milestones
- else
.milestones
%ul.content-list
- @milestones.each do |milestone|
= render 'milestone', milestone: milestone
= paginate @milestones, theme: 'gitlab'
- else - else
.milestones = render 'shared/empty_states/milestones' do
%ul.content-list - if current_user
- @milestones.each do |milestone| .page-title-controls
= render 'milestone', milestone: milestone = render 'shared/new_project_item_select',
= paginate @milestones, theme: 'gitlab' path: '-/milestones/new', label: 'New milestone',
include_groups: true, type: :milestones
- page_title _("Milestones") - page_title _("Milestones")
- add_page_specific_style 'page_bundles/milestone' - add_page_specific_style 'page_bundles/milestone'
.top-area - if @milestone_states.any? { |name, count| count > 0 }
= render 'shared/milestones_filter', counts: @milestone_states .top-area
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls .nav-controls
= render 'shared/milestones/search_form' = render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group) - if can?(current_user, :admin_milestone, @group)
= link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" } = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" }
- if @milestones.blank? - if @milestones.blank?
= render 'shared/empty_states/milestones' = render 'shared/empty_states/milestones_tab', learn_more_path: help_page_path('user/group/milestones') do
- if can?(current_user, :admin_milestone, @group)
.text-center
= link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" }
- else
.milestones
%ul.content-list
- @milestones.each do |milestone|
- if milestone.project_milestone?
= render 'projects/milestones/milestone', milestone: milestone
- else
= render 'milestone', milestone: milestone
= paginate @milestones, theme: "gitlab"
- else - else
.milestones = render 'shared/empty_states/milestones', learn_more_path: help_page_path('user/group/milestones') do
%ul.content-list - if can?(current_user, :admin_milestone, @group)
- @milestones.each do |milestone| .text-center
- if milestone.project_milestone? = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" }
= render 'projects/milestones/milestone', milestone: milestone
- else
= render 'milestone', milestone: milestone
= paginate @milestones, theme: "gitlab"
- page_title _('Milestones') - page_title _('Milestones')
- add_page_specific_style 'page_bundles/milestone' - add_page_specific_style 'page_bundles/milestone'
.top-area - if @milestone_states.any? { |name, count| count > 0 }
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) .top-area
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls .nav-controls
= render 'shared/milestones/search_form' = render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
= link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do = link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do
= _('New milestone') = _('New milestone')
- if @milestones.blank? - if @milestones.blank?
= render 'shared/empty_states/milestones' = render 'shared/empty_states/milestones_tab' do
- else - if can?(current_user, :admin_milestone, @project)
.milestones .text-center
#js-delete-milestone-modal = link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do
#promote-milestone-modal = _('New milestone')
- else
.milestones
#js-delete-milestone-modal
#promote-milestone-modal
%ul.content-list %ul.content-list
= render @milestones = render @milestones
= paginate @milestones, theme: 'gitlab' = paginate @milestones, theme: 'gitlab'
- else
= render 'shared/empty_states/milestones' do
- if can?(current_user, :admin_milestone, @project)
.text-center
= link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do
= _('New milestone')
- learn_more_path = local_assigns.fetch(:learn_more_path, help_page_path('user/project/milestones/index'))
- learn_more_link = link_to _('Learn more.'), learn_more_path
.row.empty-state .row.empty-state
.col-12 .col-12
.svg-content .svg-content
= image_tag 'illustrations/milestone_burndown_chart.svg' = image_tag 'illustrations/milestone_burndown_chart.svg'
.col-12 .col-12
.text-content .text-content
%h4.text-center= _('No milestones to show') %h4= s_('Milestones|Use milestones to track issues and merge requests over a fixed period of time')
%p.state-description
= s_('Milestones|Organize issues and merge requests into a cohesive group, and set an optional start and due dates. %{learn_more_link}').html_safe % { learn_more_link: learn_more_link }
= yield
- learn_more_path = local_assigns.fetch(:learn_more_path, help_page_path('user/project/milestones/index'))
- learn_more_link = link_to _('Learn more.'), learn_more_path
- closed_tab_selected = params[:state] == 'closed'
.row.empty-state
.col-12
.svg-content
= image_tag 'illustrations/milestone_burndown_chart.svg'
.col-12
.text-content
- if closed_tab_selected
%h4.text-center= s_('Milestones|There are no closed milestones')
- else
%h4.text-center= s_('Milestones|There are no open milestones')
%p.state-description
= s_('Milestones|Create a milestone to better track your issues and merge requests. %{learn_more_link}').html_safe % { learn_more_link: learn_more_link }
= yield
...@@ -24091,6 +24091,9 @@ msgstr "" ...@@ -24091,6 +24091,9 @@ msgstr ""
msgid "Milestones|Completed Issues (closed)" msgid "Milestones|Completed Issues (closed)"
msgstr "" msgstr ""
msgid "Milestones|Create a milestone to better track your issues and merge requests. %{learn_more_link}"
msgstr ""
msgid "Milestones|Delete milestone" msgid "Milestones|Delete milestone"
msgstr "" msgstr ""
...@@ -24109,6 +24112,9 @@ msgstr "" ...@@ -24109,6 +24112,9 @@ msgstr ""
msgid "Milestones|Ongoing Issues (open and assigned)" msgid "Milestones|Ongoing Issues (open and assigned)"
msgstr "" msgstr ""
msgid "Milestones|Organize issues and merge requests into a cohesive group, and set an optional start and due dates. %{learn_more_link}"
msgstr ""
msgid "Milestones|Project Milestone" msgid "Milestones|Project Milestone"
msgstr "" msgstr ""
...@@ -24127,12 +24133,21 @@ msgstr "" ...@@ -24127,12 +24133,21 @@ msgstr ""
msgid "Milestones|Reopen Milestone" msgid "Milestones|Reopen Milestone"
msgstr "" msgstr ""
msgid "Milestones|There are no closed milestones"
msgstr ""
msgid "Milestones|There are no open milestones"
msgstr ""
msgid "Milestones|This action cannot be reversed." msgid "Milestones|This action cannot be reversed."
msgstr "" msgstr ""
msgid "Milestones|Unstarted Issues (open and unassigned)" msgid "Milestones|Unstarted Issues (open and unassigned)"
msgstr "" msgstr ""
msgid "Milestones|Use milestones to track issues and merge requests over a fixed period of time"
msgstr ""
msgid "Minimum capacity to be available before we schedule more mirrors preemptively." msgid "Minimum capacity to be available before we schedule more mirrors preemptively."
msgstr "" msgstr ""
...@@ -25208,9 +25223,6 @@ msgstr "" ...@@ -25208,9 +25223,6 @@ msgstr ""
msgid "No milestone" msgid "No milestone"
msgstr "" msgstr ""
msgid "No milestones to show"
msgstr ""
msgid "No namespace" msgid "No namespace"
msgstr "" msgstr ""
......
...@@ -66,7 +66,7 @@ RSpec.describe 'Group milestones' do ...@@ -66,7 +66,7 @@ RSpec.describe 'Group milestones' do
context 'when no milestones' do context 'when no milestones' do
it 'renders no milestones text' do it 'renders no milestones text' do
visit group_milestones_path(group) visit group_milestones_path(group)
expect(page).to have_content('No milestones to show') expect(page).to have_content('Use milestones to track issues and merge requests')
end end
end end
......
...@@ -21,7 +21,7 @@ RSpec.describe "User deletes milestone", :js do ...@@ -21,7 +21,7 @@ RSpec.describe "User deletes milestone", :js do
click_button("Delete") click_button("Delete")
click_button("Delete milestone") click_button("Delete milestone")
expect(page).to have_content("No milestones to show") expect(page).to have_content("Use milestones to track issues and merge requests over a fixed period of time")
visit(activity_project_path(project)) visit(activity_project_path(project))
......
...@@ -24,34 +24,6 @@ RSpec.describe TimeboxesHelper do ...@@ -24,34 +24,6 @@ RSpec.describe TimeboxesHelper do
end end
end end
describe '#milestone_counts' do
let(:project) { create(:project) }
let(:counts) { helper.milestone_counts(project.milestones) }
context 'when there are milestones' do
it 'returns the correct counts' do
create_list(:active_milestone, 2, project: project)
create(:closed_milestone, project: project)
expect(counts).to eq(opened: 2, closed: 1, all: 3)
end
end
context 'when there are only milestones of one type' do
it 'returns the correct counts' do
create_list(:active_milestone, 2, project: project)
expect(counts).to eq(opened: 2, closed: 0, all: 2)
end
end
context 'when there are no milestones' do
it 'returns the correct counts' do
expect(counts).to eq(opened: 0, closed: 0, all: 0)
end
end
end
describe "#group_milestone_route" do describe "#group_milestone_route" do
let(:group) { build_stubbed(:group) } let(:group) { build_stubbed(:group) }
let(:subgroup) { build_stubbed(:group, parent: group, name: "Test Subgrp") } let(:subgroup) { build_stubbed(:group, parent: group, name: "Test Subgrp") }
......
# frozen_string_literal: true
RSpec.shared_examples 'milestone empty states' do
include Devise::Test::ControllerHelpers
let_it_be(:user) { build(:user) }
let(:empty_state) { 'Use milestones to track issues and merge requests over a fixed period of time' }
before do
assign(:projects, [])
allow(view).to receive(:current_user).and_return(user)
end
context 'with no milestones' do
before do
assign(:milestones, [])
assign(:milestone_states, { opened: 0, closed: 0, all: 0 })
render
end
it 'shows empty state' do
expect(rendered).to have_content(empty_state)
end
it 'does not show tabs or searchbar' do
expect(rendered).not_to have_link('Open')
expect(rendered).not_to have_link('Closed')
expect(rendered).not_to have_link('All')
end
end
context 'with no open milestones' do
before do
allow(view).to receive(:milestone_path).and_return("/milestones/1")
assign(:milestones, [])
assign(:milestone_states, { opened: 0, closed: 1, all: 1 })
end
it 'shows tabs and searchbar', :aggregate_failures do
render
expect(rendered).not_to have_content(empty_state)
expect(rendered).to have_link('Open')
expect(rendered).to have_link('Closed')
expect(rendered).to have_link('All')
end
it 'shows empty state' do
render
expect(rendered).to have_content('There are no open milestones')
end
end
context 'with no closed milestones' do
before do
allow(view).to receive(:milestone_path).and_return("/milestones/1")
allow(view).to receive(:params).and_return(state: 'closed')
assign(:milestones, [])
assign(:milestone_states, { opened: 1, closed: 0, all: 1 })
end
it 'shows tabs and searchbar', :aggregate_failures do
render
expect(rendered).not_to have_content(empty_state)
expect(rendered).to have_link('Open')
expect(rendered).to have_link('Closed')
expect(rendered).to have_link('All')
end
it 'shows empty state on closed milestones' do
render
expect(rendered).to have_content('There are no closed milestones')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'dashboard/milestones/index.html.haml' do
it_behaves_like 'milestone empty states'
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/milestones/index.html.haml' do
it_behaves_like 'milestone empty states'
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/milestones/index.html.haml' do
it_behaves_like 'milestone empty states'
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