Add project milestone link to dashboard milestones
One of the steps to deprecate dashboard milestones. Links do dashboard milestone are replaced with links for each project milestone
This commit is contained in:
parent
f7ac8041f7
commit
e9ef02096b
20 changed files with 246 additions and 175 deletions
|
@ -45,9 +45,4 @@
|
||||||
&.status-box-upcoming {
|
&.status-box-upcoming {
|
||||||
background: $gl-text-color-secondary;
|
background: $gl-text-color-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.status-box-milestone {
|
|
||||||
color: $gl-text-color;
|
|
||||||
background: $gray-darker;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
$status-box-line-height: 26px;
|
||||||
|
|
||||||
.issues-sortable-list .str-truncated {
|
.issues-sortable-list .str-truncated {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
@ -38,6 +40,7 @@
|
||||||
font-size: $tooltip-font-size;
|
font-size: $tooltip-font-size;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-right: $gl-padding-4;
|
margin-right: $gl-padding-4;
|
||||||
|
line-height: $status-box-line-height;
|
||||||
|
|
||||||
@include media-breakpoint-down(xs) {
|
@include media-breakpoint-down(xs) {
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
|
|
|
@ -43,14 +43,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||||
def update
|
def update
|
||||||
# Keep this compatible with legacy group milestones where we have to update
|
# Keep this compatible with legacy group milestones where we have to update
|
||||||
# all projects milestones states at once.
|
# all projects milestones states at once.
|
||||||
if @milestone.legacy_group_milestone?
|
milestones, update_params = get_milestones_for_update
|
||||||
update_params = milestone_params.select { |key| key == "state_event" }
|
|
||||||
milestones = @milestone.milestones
|
|
||||||
else
|
|
||||||
update_params = milestone_params
|
|
||||||
milestones = [@milestone]
|
|
||||||
end
|
|
||||||
|
|
||||||
milestones.each do |milestone|
|
milestones.each do |milestone|
|
||||||
Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
|
Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
|
||||||
end
|
end
|
||||||
|
@ -71,6 +64,14 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def get_milestones_for_update
|
||||||
|
if @milestone.legacy_group_milestone?
|
||||||
|
[@milestone.milestones, legacy_milestone_params]
|
||||||
|
else
|
||||||
|
[[@milestone], milestone_params]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def authorize_admin_milestones!
|
def authorize_admin_milestones!
|
||||||
return render_404 unless can?(current_user, :admin_milestone, group)
|
return render_404 unless can?(current_user, :admin_milestone, group)
|
||||||
end
|
end
|
||||||
|
@ -79,6 +80,10 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||||
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
|
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def legacy_milestone_params
|
||||||
|
params.require(:milestone).permit(:state_event)
|
||||||
|
end
|
||||||
|
|
||||||
def milestone_path
|
def milestone_path
|
||||||
if @milestone.legacy_group_milestone?
|
if @milestone.legacy_group_milestone?
|
||||||
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
|
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
|
||||||
|
|
|
@ -237,12 +237,15 @@ module MilestonesHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_or_dashboard_milestone_path(milestone)
|
def group_or_project_milestone_path(milestone)
|
||||||
|
params =
|
||||||
if milestone.group_milestone?
|
if milestone.group_milestone?
|
||||||
group_milestone_path(milestone.group, milestone.iid, milestone: { title: milestone.title })
|
{ milestone: { title: milestone.title } }
|
||||||
else
|
else
|
||||||
dashboard_milestone_path(milestone.safe_title, title: milestone.title)
|
{ title: milestone.title }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
milestone_path(milestone.milestone, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_admin_project_milestones?
|
def can_admin_project_milestones?
|
||||||
|
|
|
@ -42,7 +42,7 @@ module Milestoneish
|
||||||
def issues_visible_to_user(user)
|
def issues_visible_to_user(user)
|
||||||
memoize_per_user(user, :issues_visible_to_user) do
|
memoize_per_user(user, :issues_visible_to_user) do
|
||||||
IssuesFinder.new(user, issues_finder_params)
|
IssuesFinder.new(user, issues_finder_params)
|
||||||
.execute.preload(:assignees).where(milestone_id: milestoneish_ids)
|
.execute.preload(:assignees).where(milestone_id: milestoneish_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ class DashboardGroupMilestone < GlobalMilestone
|
||||||
|
|
||||||
override :initialize
|
override :initialize
|
||||||
def initialize(milestone)
|
def initialize(milestone)
|
||||||
super(milestone.title, Array(milestone))
|
super
|
||||||
|
|
||||||
@group_name = milestone.group.full_name
|
@group_name = milestone.group.full_name
|
||||||
end
|
end
|
||||||
|
@ -19,22 +19,4 @@ class DashboardGroupMilestone < GlobalMilestone
|
||||||
.active
|
.active
|
||||||
.map { |m| new(m) }
|
.map { |m| new(m) }
|
||||||
end
|
end
|
||||||
|
|
||||||
override :group_milestone?
|
|
||||||
def group_milestone?
|
|
||||||
@first_milestone.group_milestone?
|
|
||||||
end
|
|
||||||
|
|
||||||
override :milestoneish_ids
|
|
||||||
def milestoneish_ids
|
|
||||||
milestones.map(&:id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def group
|
|
||||||
@first_milestone.group
|
|
||||||
end
|
|
||||||
|
|
||||||
def iid
|
|
||||||
@first_milestone.iid
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DashboardMilestone < GlobalMilestone
|
class DashboardMilestone < GlobalMilestone
|
||||||
def issues_finder_params
|
attr_reader :project_name
|
||||||
{ authorized_only: true }
|
|
||||||
|
def initialize(milestone)
|
||||||
|
super
|
||||||
|
|
||||||
|
@project_name = milestone.project.full_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def dashboard_milestone?
|
def project_milestone?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,69 +3,78 @@
|
||||||
class GlobalMilestone
|
class GlobalMilestone
|
||||||
include Milestoneish
|
include Milestoneish
|
||||||
|
|
||||||
EPOCH = DateTime.parse('1970-01-01')
|
|
||||||
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
|
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
|
||||||
|
|
||||||
attr_accessor :title, :milestones
|
attr_reader :milestone
|
||||||
alias_attribute :name, :title
|
alias_attribute :name, :title
|
||||||
|
|
||||||
|
delegate :title, :state, :due_date, :start_date, :participants, :project, :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, :milestoneish_id, to: :milestone
|
||||||
|
|
||||||
|
def to_hash
|
||||||
|
{
|
||||||
|
name: title,
|
||||||
|
title: title,
|
||||||
|
group_name: group&.full_name,
|
||||||
|
project_name: project&.full_name
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def for_display
|
def for_display
|
||||||
@first_milestone
|
@milestone
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.build_collection(projects, params)
|
def self.build_collection(projects, params)
|
||||||
params =
|
items = Milestone.of_projects(projects)
|
||||||
{ project_ids: projects.map(&:id), state: params[:state] }
|
.reorder_by_due_date_asc
|
||||||
|
.order_by_name_asc
|
||||||
|
|
||||||
child_milestones = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
|
Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
|
||||||
|
|
||||||
milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
|
|
||||||
milestones_relation = Milestone.where(id: grouped.map(&:id))
|
|
||||||
new(title, milestones_relation)
|
|
||||||
end
|
|
||||||
|
|
||||||
milestones.sort_by { |milestone| milestone.due_date || EPOCH }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# necessary for legacy milestones
|
||||||
def self.build(projects, title)
|
def self.build(projects, title)
|
||||||
child_milestones = Milestone.of_projects(projects).where(title: title)
|
milestones = Milestone.of_projects(projects).where(title: title)
|
||||||
return if child_milestones.blank?
|
return if milestones.blank?
|
||||||
|
|
||||||
new(title, child_milestones)
|
new(milestones.first)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.count_by_state(milestones_by_state_and_title, state)
|
def self.states_count(projects, group = nil)
|
||||||
milestones_by_state_and_title.count do |(milestone_state, _), _|
|
legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
|
||||||
milestone_state == state
|
group_milestones_count = group_milestones_states_count(group)
|
||||||
end
|
|
||||||
end
|
|
||||||
private_class_method :count_by_state
|
|
||||||
|
|
||||||
def initialize(title, milestones)
|
legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
|
||||||
@title = title
|
legacy_group_milestones_count + group_milestones_count
|
||||||
@name = title
|
end
|
||||||
@milestones = milestones
|
|
||||||
@first_milestone = milestones.find {|m| m.description.present? } || milestones.first
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def milestoneish_ids
|
def self.group_milestones_states_count(group)
|
||||||
milestones.select(:id)
|
return STATE_COUNT_HASH unless group
|
||||||
|
|
||||||
|
counts_by_state = Milestone.of_groups(group).count_by_state
|
||||||
|
|
||||||
|
{
|
||||||
|
opened: counts_by_state['active'] || 0,
|
||||||
|
closed: counts_by_state['closed'] || 0,
|
||||||
|
all: counts_by_state.values.sum
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def safe_title
|
def self.legacy_group_milestone_states_count(projects)
|
||||||
@title.to_slug.normalize.to_s
|
return STATE_COUNT_HASH unless projects
|
||||||
|
|
||||||
|
# We need to reorder(nil) on the projects, because the controller passes them in sorted.
|
||||||
|
relation = Milestone.of_projects(projects.reorder(nil)).count_by_state
|
||||||
|
|
||||||
|
{
|
||||||
|
opened: relation['active'] || 0,
|
||||||
|
closed: relation['closed'] || 0,
|
||||||
|
all: relation.values.sum
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def projects
|
def initialize(milestone)
|
||||||
@projects ||= Project.for_milestones(milestoneish_ids)
|
@milestone = milestone
|
||||||
end
|
|
||||||
|
|
||||||
def state
|
|
||||||
milestones.each do |milestone|
|
|
||||||
return 'active' if milestone.state != 'closed'
|
|
||||||
end
|
|
||||||
|
|
||||||
'closed'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def active?
|
def active?
|
||||||
|
@ -77,37 +86,14 @@ class GlobalMilestone
|
||||||
end
|
end
|
||||||
|
|
||||||
def issues
|
def issues
|
||||||
@issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignees, :labels)
|
@issues ||= Issue.of_milestones(milestone).includes(:project, :assignees, :labels)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_requests
|
def merge_requests
|
||||||
@merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
|
@merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignee, :labels)
|
||||||
end
|
|
||||||
|
|
||||||
def participants
|
|
||||||
@participants ||= milestones.map(&:participants).flatten.uniq
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
def labels
|
||||||
@labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
|
@labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
|
||||||
.sort_by!(&:title)
|
|
||||||
end
|
|
||||||
|
|
||||||
def due_date
|
|
||||||
return @due_date if defined?(@due_date)
|
|
||||||
|
|
||||||
@due_date =
|
|
||||||
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
|
|
||||||
@milestones.first.due_date
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_date
|
|
||||||
return @start_date if defined?(@start_date)
|
|
||||||
|
|
||||||
@start_date =
|
|
||||||
if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
|
|
||||||
@milestones.first.start_date
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,35 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
# Group Milestones are milestones that can be shared among many projects within the same group
|
# Group Milestones are milestones that can be shared among many projects within the same group
|
||||||
class GroupMilestone < GlobalMilestone
|
class GroupMilestone < GlobalMilestone
|
||||||
attr_accessor :group
|
attr_reader :group, :milestones
|
||||||
|
|
||||||
def self.build_collection(group, projects, params)
|
def self.build_collection(group, projects, params)
|
||||||
super(projects, params).each do |milestone|
|
params =
|
||||||
milestone.group = group
|
{ state: params[:state] }
|
||||||
|
|
||||||
|
project_milestones = Milestone.of_projects(projects)
|
||||||
|
child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
|
||||||
|
grouped_milestones = child_milestones.group_by(&:title)
|
||||||
|
|
||||||
|
grouped_milestones.map do |title, grouped|
|
||||||
|
new(title, grouped, group)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.build(group, projects, title)
|
def self.build(group, projects, title)
|
||||||
super(projects, title).tap do |milestone|
|
child_milestones = Milestone.of_projects(projects).where(title: title)
|
||||||
milestone&.group = group
|
return if child_milestones.blank?
|
||||||
|
|
||||||
|
new(title, child_milestones, group)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def initialize(title, milestones, group)
|
||||||
|
@milestones = milestones
|
||||||
|
@group = group
|
||||||
|
end
|
||||||
|
|
||||||
|
def milestone
|
||||||
|
@milestone ||= milestones.find { |m| m.description.present? } || milestones.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def issues_finder_params
|
def issues_finder_params
|
||||||
|
|
|
@ -94,6 +94,10 @@ class Milestone < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def count_by_state
|
||||||
|
reorder(nil).group(:state).count
|
||||||
|
end
|
||||||
|
|
||||||
def predefined?(milestone)
|
def predefined?(milestone)
|
||||||
milestone == Any ||
|
milestone == Any ||
|
||||||
milestone == None ||
|
milestone == None ||
|
||||||
|
@ -215,7 +219,7 @@ class Milestone < ActiveRecord::Base
|
||||||
self.title
|
self.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def milestoneish_ids
|
def milestoneish_id
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
= render 'shared/milestones/milestone',
|
= render 'shared/milestones/milestone',
|
||||||
milestone_path: group_or_dashboard_milestone_path(milestone),
|
milestone_path: group_or_project_milestone_path(milestone),
|
||||||
issues_path: issues_dashboard_path(milestone_title: milestone.title),
|
issues_path: issues_dashboard_path(milestone_title: milestone.title),
|
||||||
merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
|
merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
|
||||||
milestone: milestone,
|
milestone: milestone,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- dashboard = local_assigns[:dashboard]
|
- dashboard = local_assigns[:dashboard]
|
||||||
- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone)
|
- custom_dom_id = dom_id(milestone.try(:milestone) ? milestone.milestone : milestone)
|
||||||
- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
|
- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
|
||||||
|
|
||||||
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
|
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
|
||||||
|
@ -21,10 +21,12 @@
|
||||||
= milestone.group.full_name
|
= milestone.group.full_name
|
||||||
- if milestone.legacy_group_milestone?
|
- if milestone.legacy_group_milestone?
|
||||||
.projects
|
.projects
|
||||||
- milestone.milestones.each do |milestone|
|
- link_to milestone_path(milestone.milestone) do
|
||||||
= link_to milestone_path(milestone) do
|
|
||||||
%span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
|
%span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
|
||||||
= dashboard ? milestone.project.full_name : milestone.project.name
|
= dashboard ? milestone.project.full_name : milestone.project.name
|
||||||
|
- if milestone.project
|
||||||
|
.label-badge.label-badge-gray.d-inline-block
|
||||||
|
= milestone.project.full_name
|
||||||
|
|
||||||
.col-sm-4.milestone-progress
|
.col-sm-4.milestone-progress
|
||||||
= milestone_progress_bar(milestone)
|
= milestone_progress_bar(milestone)
|
||||||
|
@ -58,5 +60,5 @@
|
||||||
- else
|
- else
|
||||||
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
|
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
|
||||||
- if dashboard
|
- if dashboard
|
||||||
.status-box.status-box-milestone
|
.label-badge.label-badge-gray
|
||||||
= milestone_type
|
= milestone_type
|
||||||
|
|
|
@ -62,20 +62,19 @@
|
||||||
%th Open issues
|
%th Open issues
|
||||||
%th State
|
%th State
|
||||||
%th Due date
|
%th Due date
|
||||||
- milestone.milestones.each do |ms|
|
|
||||||
%tr
|
%tr
|
||||||
%td
|
%td
|
||||||
- project_name = group ? ms.project.name : ms.project.full_name
|
- project_name = group ? milestone.project.name : milestone.project.full_name
|
||||||
= link_to project_name, project_milestone_path(ms.project, ms)
|
= link_to project_name, milestone_path(milestone.milestone)
|
||||||
%td
|
%td
|
||||||
= ms.issues_visible_to_user(current_user).opened.count
|
= milestone.milestone.issues_visible_to_user(current_user).opened.count
|
||||||
%td
|
%td
|
||||||
- if ms.closed?
|
- if milestone.closed?
|
||||||
Closed
|
Closed
|
||||||
- else
|
- else
|
||||||
Open
|
Open
|
||||||
%td
|
%td
|
||||||
= ms.expires_at
|
= milestone.expires_at
|
||||||
- elsif milestone.group_milestone?
|
- elsif milestone.group_milestone?
|
||||||
%br
|
%br
|
||||||
View
|
View
|
||||||
|
|
5
changelogs/unreleased/ccr-49289_milestone_link.yml
Normal file
5
changelogs/unreleased/ccr-49289_milestone_link.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add project milestone link
|
||||||
|
merge_request: 22552
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -52,7 +52,7 @@ describe Dashboard::MilestonesController do
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(200)
|
expect(response).to have_gitlab_http_status(200)
|
||||||
expect(json_response.size).to eq(2)
|
expect(json_response.size).to eq(2)
|
||||||
expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id])
|
expect(json_response.map { |i| i["name"] }).to match_array([group_milestone.name, project_milestone.name])
|
||||||
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
|
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ describe Groups::MilestonesController do
|
||||||
|
|
||||||
context 'when there is a title parameter' do
|
context 'when there is a title parameter' do
|
||||||
it 'searches for a legacy group milestone' do
|
it 'searches for a legacy group milestone' do
|
||||||
expect(GlobalMilestone).to receive(:build)
|
expect(GroupMilestone).to receive(:build)
|
||||||
expect(Milestone).not_to receive(:find_by_iid)
|
expect(Milestone).not_to receive(:find_by_iid)
|
||||||
|
|
||||||
get :show, params: { group_id: group.to_param, id: title, title: milestone1.safe_title }
|
get :show, params: { group_id: group.to_param, id: title, title: milestone1.safe_title }
|
||||||
|
|
|
@ -81,7 +81,7 @@ describe 'Group milestones' do
|
||||||
description: 'Lorem Ipsum is simply dummy text'
|
description: 'Lorem Ipsum is simply dummy text'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') }
|
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_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!(: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!(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') }
|
||||||
|
@ -104,7 +104,7 @@ describe 'Group milestones' do
|
||||||
legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first
|
legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first
|
||||||
|
|
||||||
expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1)
|
expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1)
|
||||||
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
|
expect(page).to have_selector("#milestone_#{legacy_milestone.milestone.id}", count: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows milestone detail and supports its edit' do
|
it 'shows milestone detail and supports its edit' do
|
||||||
|
@ -121,6 +121,7 @@ describe 'Group milestones' do
|
||||||
|
|
||||||
it 'renders milestones' do
|
it 'renders milestones' do
|
||||||
expect(page).to have_content('v1.0')
|
expect(page).to have_content('v1.0')
|
||||||
|
expect(page).to have_content('v1.1')
|
||||||
expect(page).to have_content('GL-113')
|
expect(page).to have_content('GL-113')
|
||||||
expect(page).to have_link(
|
expect(page).to have_link(
|
||||||
'1 Issue',
|
'1 Issue',
|
||||||
|
|
|
@ -42,6 +42,7 @@ describe 'Milestones sorting', :js do
|
||||||
|
|
||||||
expect(page).to have_button('Due later')
|
expect(page).to have_button('Due later')
|
||||||
|
|
||||||
|
# assert descending sorting
|
||||||
within '.milestones' do
|
within '.milestones' do
|
||||||
expect(page.all('ul.content-list > li').first.text).to include('v1.0')
|
expect(page.all('ul.content-list > li').first.text).to include('v1.0')
|
||||||
expect(page.all('ul.content-list > li')[1].text).to include('v3.0')
|
expect(page.all('ul.content-list > li')[1].text).to include('v3.0')
|
||||||
|
|
|
@ -65,56 +65,103 @@ describe GlobalMilestone do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
let!(:projects) do
|
||||||
projects = [
|
[
|
||||||
project1,
|
project1,
|
||||||
project2,
|
project2,
|
||||||
project3
|
project3
|
||||||
]
|
]
|
||||||
|
|
||||||
@global_milestones = described_class.build_collection(projects, {})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let!(:global_milestones) { described_class.build_collection(projects, {}) }
|
||||||
|
|
||||||
|
context 'when building a collection of milestones' do
|
||||||
it 'has all project milestones' do
|
it 'has all project milestones' do
|
||||||
expect(@global_milestones.count).to eq(2)
|
expect(global_milestones.count).to eq(6)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has all project milestones titles' do
|
it 'has all project milestones titles' do
|
||||||
expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
|
expect(global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'Milestone v1.2', 'Milestone v1.2', 'VD-123', 'VD-123', 'VD-123'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has all project milestones' do
|
it 'has all project milestones' do
|
||||||
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
|
expect(global_milestones.size).to eq(6)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sorts collection by due date' do
|
it 'sorts collection by due date' do
|
||||||
expect(@global_milestones.map(&:due_date)).to eq [nil, milestone1_due_date]
|
expect(global_milestones.map(&:due_date)).to eq [milestone1_due_date, milestone1_due_date, milestone1_due_date, nil, nil, nil]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when adding new milestones' do
|
||||||
|
it 'does not add more queries' do
|
||||||
|
control_count = ActiveRecord::QueryRecorder.new do
|
||||||
|
described_class.build_collection(projects, {})
|
||||||
|
end.count
|
||||||
|
|
||||||
|
create_list(:milestone, 3, project: project3)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.build_collection(projects, {})
|
||||||
|
end.not_to exceed_all_query_limit(control_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.states_count' do
|
||||||
|
context 'when the projects have milestones' do
|
||||||
|
before do
|
||||||
|
create(:closed_milestone, title: 'Active Group Milestone', project: project3)
|
||||||
|
create(:active_milestone, title: 'Active Group Milestone', project: project1)
|
||||||
|
create(:active_milestone, title: 'Active Group Milestone', project: project2)
|
||||||
|
create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
|
||||||
|
create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
|
||||||
|
create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
|
||||||
|
create(:closed_milestone, title: 'Closed Group Milestone 4', group: group)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the quantity of global milestones and group milestones in each possible state' do
|
||||||
|
expected_count = { opened: 2, closed: 5, all: 7 }
|
||||||
|
|
||||||
|
count = described_class.states_count(Project.all, group)
|
||||||
|
|
||||||
|
expect(count).to eq(expected_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the quantity of global milestones in each possible state' do
|
||||||
|
expected_count = { opened: 2, closed: 4, all: 6 }
|
||||||
|
|
||||||
|
count = described_class.states_count(Project.all)
|
||||||
|
|
||||||
|
expect(count).to eq(expected_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the projects do not have milestones' do
|
||||||
|
before do
|
||||||
|
project1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 0 as the quantity of global milestones in each state' do
|
||||||
|
expected_count = { opened: 0, closed: 0, all: 0 }
|
||||||
|
|
||||||
|
count = described_class.states_count(Project.all)
|
||||||
|
|
||||||
|
expect(count).to eq(expected_count)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#initialize' do
|
describe '#initialize' do
|
||||||
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
|
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
|
||||||
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
|
subject(:global_milestone) { described_class.new(milestone1_project1) }
|
||||||
let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
milestones =
|
|
||||||
[
|
|
||||||
milestone1_project1,
|
|
||||||
milestone1_project2,
|
|
||||||
milestone1_project3
|
|
||||||
]
|
|
||||||
milestones_relation = Milestone.where(id: milestones.map(&:id))
|
|
||||||
|
|
||||||
@global_milestone = described_class.new(milestone1_project1.title, milestones_relation)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'has exactly one group milestone' do
|
it 'has exactly one group milestone' do
|
||||||
expect(@global_milestone.title).to eq('Milestone v1.2')
|
expect(global_milestone.title).to eq('Milestone v1.2')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has all project milestones with the same title' do
|
it 'has all project milestones with the same title' do
|
||||||
expect(@global_milestone.milestones.count).to eq(3)
|
expect(global_milestone.milestone).to eq(milestone1_project1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -122,7 +169,7 @@ describe GlobalMilestone do
|
||||||
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
|
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
|
||||||
|
|
||||||
it 'strips out slashes and spaces' do
|
it 'strips out slashes and spaces' do
|
||||||
global_milestone = described_class.new(milestone.title, Milestone.where(id: milestone.id))
|
global_milestone = described_class.new(milestone)
|
||||||
|
|
||||||
expect(global_milestone.safe_title).to eq('git-test')
|
expect(global_milestone.safe_title).to eq('git-test')
|
||||||
end
|
end
|
||||||
|
@ -132,11 +179,8 @@ describe GlobalMilestone do
|
||||||
context 'when at least one milestone is active' do
|
context 'when at least one milestone is active' do
|
||||||
it 'returns active' do
|
it 'returns active' do
|
||||||
title = 'Active Group Milestone'
|
title = 'Active Group Milestone'
|
||||||
milestones = [
|
|
||||||
create(:active_milestone, title: title),
|
global_milestone = described_class.new(create(:active_milestone, title: title))
|
||||||
create(:closed_milestone, title: title)
|
|
||||||
]
|
|
||||||
global_milestone = described_class.new(title, milestones)
|
|
||||||
|
|
||||||
expect(global_milestone.state).to eq('active')
|
expect(global_milestone.state).to eq('active')
|
||||||
end
|
end
|
||||||
|
@ -145,11 +189,8 @@ describe GlobalMilestone do
|
||||||
context 'when all milestones are closed' do
|
context 'when all milestones are closed' do
|
||||||
it 'returns closed' do
|
it 'returns closed' do
|
||||||
title = 'Closed Group Milestone'
|
title = 'Closed Group Milestone'
|
||||||
milestones = [
|
|
||||||
create(:closed_milestone, title: title),
|
global_milestone = described_class.new(create(:closed_milestone, title: title))
|
||||||
create(:closed_milestone, title: title)
|
|
||||||
]
|
|
||||||
global_milestone = described_class.new(title, milestones)
|
|
||||||
|
|
||||||
expect(global_milestone.state).to eq('closed')
|
expect(global_milestone.state).to eq('closed')
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,13 +20,36 @@ describe GroupMilestone do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.build_collection' do
|
describe '.build_collection' do
|
||||||
before do
|
let(:group) { create(:group) }
|
||||||
project_milestone
|
let(:project1) { create(:project, group: group) }
|
||||||
|
let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
|
||||||
|
let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
|
||||||
|
|
||||||
|
let!(:projects) do
|
||||||
|
[
|
||||||
|
project1,
|
||||||
|
project2,
|
||||||
|
project3
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns array of milestones, each with group assigned' do
|
it 'returns array of milestones, each with group assigned' do
|
||||||
milestones = described_class.build_collection(group, [project], {})
|
milestones = described_class.build_collection(group, [project], {})
|
||||||
expect(milestones).to all(have_attributes(group: group))
|
expect(milestones).to all(have_attributes(group: group))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when adding new milestones' do
|
||||||
|
it 'does not add more queries' do
|
||||||
|
control_count = ActiveRecord::QueryRecorder.new do
|
||||||
|
described_class.build_collection(group, projects, {})
|
||||||
|
end.count
|
||||||
|
|
||||||
|
create(:milestone, title: 'This title', project: project1)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.build_collection(group, projects, {})
|
||||||
|
end.not_to exceed_all_query_limit(control_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue