Commit 03dabc52 authored by Sean McGivern's avatar Sean McGivern

Allow filtering by all started milestones

parent 101fddfa
...@@ -20,6 +20,7 @@ module.exports = Vue.extend({ ...@@ -20,6 +20,7 @@ module.exports = Vue.extend({
data-toggle="dropdown" data-toggle="dropdown"
data-show-any="true" data-show-any="true"
data-show-upcoming="true" data-show-upcoming="true"
data-show-started="true"
data-field-name="milestone_title" data-field-name="milestone_title"
:data-milestones="milestonePath" :data-milestones="milestonePath"
ref="dropdown"> ref="dropdown">
......
...@@ -42,6 +42,10 @@ ...@@ -42,6 +42,10 @@
url: 'milestone_title=%23upcoming', url: 'milestone_title=%23upcoming',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'upcoming', value: 'upcoming',
}, {
url: 'milestone_title=%23started',
tokenKey: 'milestone',
value: 'started',
}, { }, {
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
} }
$els.each(function(i, dropdown) { $els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
projectId = $dropdown.data('project-id'); projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones'); milestonesUrl = $dropdown.data('milestones');
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
showAny = $dropdown.data('show-any'); showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming'); showUpcoming = $dropdown.data('show-upcoming');
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id'); useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
issuableId = $dropdown.data('issuable-id'); issuableId = $dropdown.data('issuable-id');
...@@ -71,6 +72,13 @@ ...@@ -71,6 +72,13 @@
title: 'Upcoming' title: 'Upcoming'
}); });
} }
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) { if (extraOptions.length) {
extraOptions.push('divider'); extraOptions.push('divider');
} }
......
...@@ -310,6 +310,10 @@ class IssuableFinder ...@@ -310,6 +310,10 @@ class IssuableFinder
params[:milestone_title] == Milestone::Upcoming.name params[:milestone_title] == Milestone::Upcoming.name
end end
def filter_by_started_milestone?
params[:milestone_title] == Milestone::Started.name
end
def by_milestone(items) def by_milestone(items)
if milestones? if milestones?
if filter_by_no_milestone? if filter_by_no_milestone?
...@@ -317,6 +321,8 @@ class IssuableFinder ...@@ -317,6 +321,8 @@ class IssuableFinder
elsif filter_by_upcoming_milestone? elsif filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids) items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
elsif filter_by_started_milestone?
items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
else else
items = items.with_milestone(params[:milestone_title]) items = items.with_milestone(params[:milestone_title])
items_projects = projects(items) items_projects = projects(items)
......
...@@ -90,11 +90,14 @@ module IssuablesHelper ...@@ -90,11 +90,14 @@ module IssuablesHelper
end end
def milestone_dropdown_label(milestone_title, default_label = "Milestone") def milestone_dropdown_label(milestone_title, default_label = "Milestone")
if milestone_title == Milestone::Upcoming.name title =
milestone_title = Milestone::Upcoming.title case milestone_title
end when Milestone::Upcoming.name then Milestone::Upcoming.title
when Milestone::Started.name then Milestone::Started.title
else milestone_title.presence
end
h(milestone_title.presence || default_label) h(title || default_label)
end end
def to_url_reference(issuable) def to_url_reference(issuable)
......
...@@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField include CacheMarkdownField
include InternalId include InternalId
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter .filter-item.inline.milestone-filter
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- if selected.present? || params[:milestone_title].present? - if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project - if project
%ul.dropdown-footer-list %ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project - if can? current_user, :admin_milestone, project
......
...@@ -68,6 +68,9 @@ ...@@ -68,6 +68,9 @@
%li.filter-dropdown-item{ data: { value: 'upcoming' } } %li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link %button.btn.btn-link
Upcoming Upcoming
%li.filter-dropdown-item{ 'data-value' => 'started' }
%button.btn.btn-link
Started
%li.divider %li.divider
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) } .col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder .issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group .form-group
- has_labels = @labels && @labels.any? - has_labels = @labels && @labels.any?
= form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
......
---
title: Allow filtering by all started milestones
merge_request:
author:
# Milestones # Milestones
Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date. Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project. A common use is keeping track of an upcoming software version. Milestones are created per-project.
![milestone form](milestones/form.png) ![milestone form](milestones/form.png)
## Groups and milestones ## Groups and milestones
You can create a milestone for several projects in the same group simultaneously. You can create a milestone for several projects in the same group simultaneously.
On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects. On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
![group milestone form](milestones/group_form.png) ![group milestone form](milestones/group_form.png)
## Special milestone filters
In addition to the milestones that exist in the project or group, there are some
special options available when filtering by milestone:
* **No Milestone** - only show issues or merge requests without a milestone.
* **Upcoming** - show issues or merge request that belong to the next open
milestone with a due date, by project. (For example: if project A has
milestone v1 due in three days, and project B has milestone v2 due in a week,
then this will show issues or merge requests from milestone v1 in project A
and milestone v2 in project B.)
* **Started** - show issues or merge requests from any milestone with a start
date less than today. Note that this can return results from several
milestones in the same project.
...@@ -202,6 +202,14 @@ describe 'Dropdown milestone', :feature, :js do ...@@ -202,6 +202,14 @@ describe 'Dropdown milestone', :feature, :js do
expect_tokens([{ name: 'milestone', value: 'upcoming' }]) expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
it 'selects `started milestones`' do
click_static_milestone('Started')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect_tokens([{ name: 'milestone', value: 'started' }])
expect_filtered_search_input_empty
end
end end
describe 'input has existing content' do describe 'input has existing content' do
......
...@@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do ...@@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do
let!(:project) { create(:project, group: group) } let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:user2) { create(:user) } let!(:user2) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
let!(:milestone) { create(:milestone, title: "8", project: project) } let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
...@@ -505,6 +504,14 @@ describe 'Filter issues', js: true, feature: true do ...@@ -505,6 +504,14 @@ describe 'Filter issues', js: true, feature: true do
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
it 'filters issues by started milestones' do
input_filtered_search("milestone:started")
expect_tokens([{ name: 'milestone', value: 'started' }])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
it 'filters issues by invalid milestones' do it 'filters issues by invalid milestones' do
skip('to be tested, issue #26546') skip('to be tested, issue #26546')
end end
......
...@@ -101,6 +101,41 @@ describe IssuesFinder do ...@@ -101,6 +101,41 @@ describe IssuesFinder do
end end
end end
context 'filtering by started milestone' do
let(:params) { { milestone_title: Milestone::Started.name } }
let(:project_no_started_milestones) { create(:empty_project, :public) }
let(:project_started_1_and_2) { create(:empty_project, :public) }
let(:project_started_8) { create(:empty_project, :public) }
let(:yesterday) { Date.today - 1.day }
let(:tomorrow) { Date.today + 1.day }
let(:two_days_ago) { Date.today - 2.days }
let(:milestones) do
[
create(:milestone, project: project_no_started_milestones, start_date: tomorrow),
create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
create(:milestone, project: project_started_8, title: '7.0'),
create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
]
end
before do
milestones.each do |milestone|
create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
end
end
it 'returns issues in the started milestones for each project' do
expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.0', '2.0', '8.0')
expect(issues.map { |issue| issue.milestone.start_date }).to contain_exactly(two_days_ago, yesterday, yesterday)
end
end
context 'filtering by label' do context 'filtering by label' do
let(:params) { { label_name: label.title } } let(:params) { { label_name: label.title } }
......
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