Commit 9bdf58b6 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch...

Merge branch '19445-native-group-milestone-page-needs-same-info-as-project-milestones' into 'master'

Add tabs in group milestone page

See merge request gitlab-org/gitlab!18818
parents eab4efd0 414ad018
......@@ -53,12 +53,10 @@ module MilestoneActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def milestone_redirect_path
if @project
project_milestone_path(@project, @milestone)
elsif @group
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
if @milestone.global_milestone?
url_for(action: :show, title: @milestone.title)
else
dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
url_for(action: :show)
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
......
......@@ -4,6 +4,18 @@ module MilestonesHelper
include EntityDateHelper
include Gitlab::Utils::StrongMemoize
def milestone_status_string(milestone)
if milestone.closed?
_('Closed')
elsif milestone.expired?
_('Past due')
elsif milestone.upcoming?
_('Upcoming')
else
_('Open')
end
end
def milestones_filter_path(opts = {})
if @project
project_milestones_path(@project, opts)
......@@ -213,33 +225,19 @@ module MilestonesHelper
end
end
def milestone_merge_request_tab_path(milestone)
if @project
merge_requests_project_milestone_path(@project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
def milestone_tab_path(milestone, tab)
if milestone.global_milestone?
url_for(action: tab, title: milestone.title, format: :json)
else
merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
url_for(action: tab, format: :json)
end
end
def milestone_participants_tab_path(milestone)
if @project
participants_project_milestone_path(@project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
def update_milestone_path(milestone, params = {})
if milestone.project_milestone?
project_milestone_path(milestone.project, milestone, milestone: params)
else
participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
end
end
def milestone_labels_tab_path(milestone)
if @project
labels_project_milestone_path(@project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
else
labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
group_milestone_route(milestone, params)
end
end
......@@ -264,6 +262,14 @@ module MilestonesHelper
milestone_path(milestone.milestone, params)
end
def edit_milestone_path(milestone)
if milestone.group_milestone?
edit_group_milestone_path(milestone.group, milestone)
elsif milestone.project_milestone?
edit_project_milestone_path(milestone.project, milestone)
end
end
def can_admin_project_milestones?
strong_memoize(:can_admin_project_milestones) do
can?(current_user, :admin_milestone, @project)
......
......@@ -101,6 +101,10 @@ module Milestoneish
false
end
def global_milestone?
false
end
def total_issue_time_spent
@total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
end
......
......@@ -18,4 +18,8 @@ class DashboardGroupMilestone < GlobalMilestone
milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
Milestone.filter_by_state(milestones, params[:state]).map { |m| new(m) }
end
def dashboard_milestone?
true
end
end
......@@ -100,4 +100,8 @@ class GlobalMilestone
def labels
@labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
end
def global_milestone?
true
end
end
......@@ -330,6 +330,6 @@ class Milestone < ApplicationRecord
end
def issues_finder_params
{ project_id: project_id }
{ project_id: project_id, group_id: group_id }.compact
end
end
= render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.legacy_group_milestone?
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
......@@ -3,57 +3,8 @@
- page_title @milestone.title, _('Milestones')
- page_description @milestone.description
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
= _('Closed')
- elsif @milestone.expired?
= _('Past due')
- elsif @milestone.upcoming?
= _('Upcoming')
- else
= _('Open')
.header-text-content
%span.identifier
%strong
= _('Milestone')
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
= link_to edit_project_milestone_path(@project, @milestone), class: 'btn btn-grouped btn-nr' do
= _('Edit')
- if @project.group
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal',
milestone_title: @milestone.title,
group_name: @project.group.name,
url: promote_project_milestone_path(@milestone.project, @milestone),
container: 'body' },
disabled: true,
type: 'button' }
= _('Promote')
#promote-milestone-modal
- if @milestone.active?
= link_to _('Close milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: 'btn btn-close btn-nr btn-grouped'
- else
= link_to _('Reopen milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: 'btn btn-reopen btn-nr btn-grouped'
= render 'shared/milestones/delete_button'
%a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: '#' }
= icon('angle-double-left')
.detail-page-description.milestone-detail
%h2.title.qa-milestone-title
= markdown_field(@milestone, :title)
%div
- if @milestone.description.present?
.description.md
= markdown_field(@milestone, :description)
= render 'shared/milestones/header', milestone: @milestone
= render 'shared/milestones/description', milestone: @milestone
= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
......
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- if milestone.try(:description).present?
%div
.description.md
= markdown_field(milestone, :description)
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(milestone) }
= milestone_status_string(milestone)
.header-text-content
%span.identifier
%strong
= _('Milestone')
- if milestone.due_date || milestone.start_date
= milestone_date_range(milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @group || @project)
- unless milestone.legacy_group_milestone?
= link_to _('Edit'), edit_milestone_path(milestone), class: 'btn btn-grouped'
- if milestone.project_milestone? && milestone.project.group
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal',
milestone_title: milestone.title,
group_name: milestone.project.group.name,
url: promote_project_milestone_path(milestone.project, milestone),
container: 'body' },
disabled: true,
type: 'button' }
= _('Promote')
#promote-milestone-modal
- if milestone.active?
= link_to _('Close milestone'), update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'btn btn-grouped btn-close'
- else
= link_to _('Reopen milestone'), update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'btn btn-grouped btn-reopen'
- unless milestone.legacy_group_milestone?
= render 'shared/milestones/delete_button'
%button.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ type: 'button' }
= icon('angle-double-left')
- issues_accessible = milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
- if issues_accessible
%li.nav-item
= link_to '#tab-issues', class: 'nav-link active', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
%span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
%li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
%span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
- else
%li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
%span.badge.badge-pill= milestone.merge_requests.size
%li.nav-item
= link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants
= link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do
= _('Issues')
%span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
%li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do
= _('Merge Requests')
%span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
%li.nav-item
= link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do
= _('Participants')
%span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
%li.nav-item
= link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels
= link_to '#tab-labels', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'labels') } do
= _('Labels')
%span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
- issues = milestone.sorted_issues(current_user)
......@@ -32,16 +24,11 @@
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
.tab-content.milestone-content
- if issues_accessible
.tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
-# loaded async
= render "shared/milestones/tab_loading"
- else
.tab-pane.active#tab-merge-requests
-# loaded async
= render "shared/milestones/tab_loading"
.tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
-# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants
-# loaded async
= render "shared/milestones/tab_loading"
......
......@@ -4,54 +4,15 @@
- group = local_assigns[:group]
- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header.milestone-page-header
.status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
- if milestone.closed?
Closed
- elsif milestone.expired?
Expired
- else
Open
.header-text-content
%span.identifier
Milestone #{milestone.title}
- if milestone.due_date || milestone.start_date
%span.creator
&nbsp;&middot;
= milestone_date_range(milestone)
.milestone-buttons
- if group
- if can?(current_user, :admin_milestone, group)
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
- if milestone.active?
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- else
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- unless is_dynamic_milestone
= render 'shared/milestones/delete_button'
%a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
= render 'shared/milestones/header', milestone: milestone
= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- if milestone.group_milestone? && milestone.description.present?
%div
.description.md
= markdown_field(milestone, :description)
= render 'shared/milestones/description', milestone: milestone
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
%span
= _('All issues for this milestone are closed.')
= group ? _('You may close the milestone now.') : _('Navigate to the project to close the milestone.')
= render_if_exists 'shared/milestones/burndown', milestone: milestone, project: @project
......@@ -77,10 +38,3 @@
Open
%td
= milestone.expires_at
- elsif milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
or
= link_to 'Merge Requests', merge_requests_group_path(@group, milestone_title: milestone.title)
in this milestone
---
title: Add issues, MRs, participants, and labels tabs in group milestone page
merge_request: 18818
author:
type: added
......@@ -103,30 +103,18 @@ When filtering by milestone, in addition to choosing a specific project mileston
## Milestone view
Not all features in the project milestone view are available in the group milestone view. This table summarizes the differences:
| Feature | Project milestone view | Group milestone view |
|--------------------------------------|:----------------------:|:--------------------:|
| Title and description | ✓ | ✓ |
| Issues assigned to milestone | ✓ | |
| Merge requests assigned to milestone | ✓ | |
| Participants and labels used | ✓ | |
| Percentage complete | ✓ | ✓ |
| Start date and due date | ✓ | ✓ |
| Total issue time spent | ✓ | ✓ |
| Total issue weight | ✓ | |
| Burndown chart **[STARTER}** | ✓ | ✓ |
The milestone view shows the title and description.
### Project milestone features
These features are only available for project milestones and not group milestones.
There are also tabs below these that show the following:
- Issues assigned to the milestone are displayed in three columns: Unstarted issues, ongoing issues, and completed issues.
- Merge requests assigned to the milestone are displayed in four columns: Work in progress merge requests, waiting for merge, rejected, and closed.
- Participants and labels that are used in issues and merge requests that have the milestone assigned are displayed.
- [Burndown chart](#project-burndown-charts-starter).
- Issues
Shows all issues assigned to the milestone. These are displayed in three columns: Unstarted issues, ongoing issues, and completed issues.
- Merge requests
Shows all merge requests assigned to the milestone. These are displayed in four columns: Work in progress merge requests, waiting for merge, rejected, and closed.
- Participants
Shows all assignees of issues assigned to the milestone.
- Labels
Shows all labels that are used in issues assigned to the milestone.
### Project Burndown Charts **(STARTER)**
......@@ -144,9 +132,8 @@ The milestone sidebar on the milestone view shows the following:
- Percentage complete, which is calculated as number of closed issues divided by total number of issues.
- The start date and due date.
- The total time spent on all issues that have the milestone assigned.
For project milestones only, the milestone sidebar shows the total issue weight of all issues that have the milestone assigned.
- The total time spent on all issues assigned to the milestone.
- The total issue weight of all issues assigned to the milestone.
![Project milestone page](img/milestones_project_milestone_page.png)
......
......@@ -1396,6 +1396,9 @@ msgstr ""
msgid "All groups and projects"
msgstr ""
msgid "All issues for this milestone are closed."
msgstr ""
msgid "All issues for this milestone are closed. You may close this milestone now."
msgstr ""
......@@ -10941,6 +10944,9 @@ msgstr ""
msgid "Naming, visibility"
msgstr ""
msgid "Navigate to the project to close the milestone."
msgstr ""
msgid "Nav|Help"
msgstr ""
......@@ -11782,6 +11788,9 @@ msgstr ""
msgid "Part of merge request changes"
msgstr ""
msgid "Participants"
msgstr ""
msgid "Passed"
msgstr ""
......@@ -19497,6 +19506,9 @@ msgstr ""
msgid "You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>."
msgstr ""
msgid "You may close the milestone now."
msgstr ""
msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
......
......@@ -3,9 +3,9 @@
require 'spec_helper'
describe 'Group milestones' do
let(:group) { create(:group) }
let!(:project) { create(:project_empty_repo, group: group) }
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project_empty_repo, group: group) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
around do |example|
Timecop.freeze { example.run }
......@@ -71,9 +71,9 @@ describe 'Group milestones' do
end
context 'when milestones exists' do
let!(:other_project) { create(:project_empty_repo, group: group) }
let_it_be(:other_project) { create(:project_empty_repo, group: group) }
let!(:active_project_milestone1) do
let_it_be(:active_project_milestone1) do
create(
:milestone,
project: project,
......@@ -83,12 +83,12 @@ describe 'Group milestones' do
description: 'Lorem Ipsum is simply dummy text'
)
end
let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.1') }
let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
let!(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') }
let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
let!(:issue) do
let_it_be(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.1') }
let_it_be(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
let_it_be(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
let_it_be(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') }
let_it_be(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
let_it_be(:issue) do
create :issue, project: project, assignees: [user], author: user, milestone: active_project_milestone1
end
......@@ -143,38 +143,111 @@ describe 'Group milestones' do
expect(page).to have_content('Issues 1 Open: 1 Closed: 0')
expect(page).to have_link(issue.title, href: project_issue_path(issue.project, issue))
end
end
end
describe 'milestone tabs', :js do
context 'for a legacy group milestone' do
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:issue) { create(:labeled_issue, project: project, milestone: milestone, labels: [label], assignees: [create(:user)]) }
let_it_be(:mr) { create(:merge_request, source_project: project, milestone: milestone) }
before do
visit group_milestone_path(group, milestone.title, title: milestone.title)
end
it 'renders the issues tab' do
within('#tab-issues') do
expect(page).to have_content issue.title
end
end
it 'renders the merge requests tab' do
within('.js-milestone-tabs') do
click_link('Merge Requests')
end
describe 'labels' do
before do
create(:label, project: project, title: 'bug') do |label|
issue.labels << label
end
within('#tab-merge-requests') do
expect(page).to have_content mr.title
end
end
it 'renders the participants tab' do
within('.js-milestone-tabs') do
click_link('Participants')
end
create(:label, project: project, title: 'feature') do |label|
issue.labels << label
end
within('#tab-participants') do
expect(page).to have_content issue.assignees.first.name
end
end
it 'renders labels' do
click_link 'v1.0'
it 'renders the labels tab' do
within('.js-milestone-tabs') do
click_link('Labels')
end
page.within('#tab-issues') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
within('#tab-labels') do
expect(page).to have_content label.title
end
end
end
context 'for a group milestone' do
let_it_be(:other_project) { create(:project_empty_repo, group: group) }
let_it_be(:milestone) { create(:milestone, group: group) }
it 'renders labels list', :js do
click_link 'v1.0'
let_it_be(:project_label) { create(:label, project: project) }
let_it_be(:other_project_label) { create(:label, project: other_project) }
page.within('.content .nav-links') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
let_it_be(:project_issue) { create(:labeled_issue, project: project, milestone: milestone, labels: [project_label], assignees: [create(:user)]) }
let_it_be(:other_project_issue) { create(:labeled_issue, project: other_project, milestone: milestone, labels: [other_project_label], assignees: [create(:user)]) }
let_it_be(:project_mr) { create(:merge_request, source_project: project, milestone: milestone) }
let_it_be(:other_project_mr) { create(:merge_request, source_project: other_project, milestone: milestone) }
before do
visit group_milestone_path(group, milestone)
end
it 'renders the issues tab' do
within('#tab-issues') do
expect(page).to have_content project_issue.title
expect(page).to have_content other_project_issue.title
end
end
it 'renders the merge requests tab' do
within('.js-milestone-tabs') do
click_link('Merge Requests')
end
within('#tab-merge-requests') do
expect(page).to have_content project_mr.title
expect(page).to have_content other_project_mr.title
end
end
it 'renders the participants tab' do
within('.js-milestone-tabs') do
click_link('Participants')
end
within('#tab-participants') do
expect(page).to have_content project_issue.assignees.first.name
expect(page).to have_content other_project_issue.assignees.first.name
end
end
it 'renders the labels tab' do
within('.js-milestone-tabs') do
click_link('Labels')
end
page.within('#tab-labels') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
within('#tab-labels') do
expect(page).to have_content project_label.title
expect(page).to have_content other_project_label.title
end
end
end
......
......@@ -51,15 +51,16 @@ describe 'Project milestone' do
context 'when project has disabled issues' do
before do
create(:issue, project: project, milestone: milestone)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
visit project_milestone_path(project, milestone)
end
it 'hides issues tab' do
it 'does not show any issues under the issues tab' do
within('#content-body') do
expect(page).not_to have_link 'Issues', href: '#tab-issues'
expect(page).to have_selector '.nav-links li a.active', count: 1
expect(find('.nav-links li a.active')).to have_content 'Merge Requests'
expect(find('.nav-links li a.active')).to have_content 'Issues'
expect(page).not_to have_selector '.issuable-row'
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