Commit bc423d3e authored by Sean McGivern's avatar Sean McGivern

Allow filtering by all started milestones

parent 641eb555
......@@ -20,6 +20,7 @@ module.exports = Vue.extend({
data-toggle="dropdown"
data-show-any="true"
data-show-upcoming="true"
data-show-started="true"
data-field-name="milestone_title"
:data-milestones="milestonePath"
ref="dropdown">
......
......@@ -7,4 +7,8 @@ module.exports = [
id: -2,
title: 'Upcoming',
},
{
id: -3,
title: 'Started',
},
];
......@@ -42,6 +42,10 @@
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
value: 'upcoming',
}, {
url: 'milestone_title=%23started',
tokenKey: 'milestone',
value: 'started',
}, {
url: 'label_name[]=No+Label',
tokenKey: 'label',
......
......@@ -19,7 +19,7 @@
}
$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);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
......@@ -29,6 +29,7 @@
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
issuableId = $dropdown.data('issuable-id');
......@@ -71,6 +72,13 @@
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
......
......@@ -312,6 +312,10 @@ class IssuableFinder
params[:milestone_title] == Milestone::Upcoming.name
end
def filter_by_started_milestone?
params[:milestone_title] == Milestone::Started.name
end
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
......@@ -319,6 +323,8 @@ class IssuableFinder
elsif filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
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
items = items.with_milestone(params[:milestone_title])
items_projects = projects(items)
......
......@@ -88,11 +88,14 @@ module IssuablesHelper
end
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
if milestone_title == Milestone::Upcoming.name
milestone_title = Milestone::Upcoming.title
title =
case milestone_title
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
def issuable_meta(issuable, project, text)
......
......@@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include InternalId
......
......@@ -25,7 +25,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" } })
.filter-item.inline.milestone-filter
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, board: board
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, board: board, show_started: true
.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[]" }
......
......@@ -9,7 +9,7 @@
- if selected.present? || params[:milestone_title].present?
= 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",
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
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
......
......@@ -73,6 +73,9 @@
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link
Upcoming
%li.filter-dropdown-item{ 'data-value' => 'started' }
%button.btn.btn-link
Started
%li.divider
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
......
......@@ -21,7 +21,7 @@
= 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) }
.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
- has_labels = @labels && @labels.any?
= 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:
......@@ -11,3 +11,18 @@ 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.
![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
expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_filtered_search_input_empty
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
describe 'input has existing content' do
......
......@@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
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!(: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
expect_filtered_search_input_empty
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
skip('to be tested, issue #26546')
end
......
......@@ -117,6 +117,41 @@ describe IssuesFinder do
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
let(:params) { { label_name: label.title } }
......
......@@ -39,9 +39,10 @@ describe('Milestone select component', () => {
it('sets default data', () => {
expect(vm.loading).toBe(false);
expect(vm.milestones.length).toBe(0);
expect(vm.extraMilestones.length).toBe(2);
expect(vm.extraMilestones.length).toBe(3);
expect(vm.extraMilestones[0].title).toBe('Any Milestone');
expect(vm.extraMilestones[1].title).toBe('Upcoming');
expect(vm.extraMilestones[2].title).toBe('Started');
});
});
......@@ -61,9 +62,9 @@ describe('Milestone select component', () => {
it('renders the milestone list', () => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelectorAll('.board-milestone-list li').length).toBe(4);
expect(vm.$el.querySelectorAll('.board-milestone-list li').length).toBe(5);
expect(
vm.$el.querySelectorAll('.board-milestone-list li')[3].textContent,
vm.$el.querySelectorAll('.board-milestone-list li')[4].textContent,
).toContain('test');
});
......@@ -85,9 +86,18 @@ describe('Milestone select component', () => {
});
});
it('selects fetched milestone', () => {
it('selects started milestone', () => {
vm.$el.querySelectorAll('.board-milestone-list a')[2].click();
expect(selectMilestoneSpy).toHaveBeenCalledWith({
id: -3,
title: 'Started',
});
});
it('selects fetched milestone', () => {
vm.$el.querySelectorAll('.board-milestone-list a')[3].click();
expect(selectMilestoneSpy).toHaveBeenCalledWith({
id: 1,
title: 'test',
......
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