From 03dabc522e937f091ad30cc40034ffd6d19c7011 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 10 Mar 2017 19:01:05 +0000 Subject: Allow filtering by all started milestones --- .../boards/components/modal/filters/milestone.js | 1 + .../filtered_search/filtered_search_token_keys.js | 4 +++ app/assets/javascripts/milestone_select.js | 10 ++++++- app/finders/issuable_finder.rb | 6 ++++ app/helpers/issuables_helper.rb | 11 ++++--- app/models/milestone.rb | 1 + app/views/shared/issuable/_filter.html.haml | 2 +- .../shared/issuable/_milestone_dropdown.html.haml | 2 +- app/views/shared/issuable/_search_bar.html.haml | 3 ++ app/views/shared/issuable/form/_metadata.html.haml | 2 +- .../unreleased/better-priority-sorting-2.yml | 4 +++ doc/workflow/milestones.md | 19 ++++++++++-- .../filtered_search/dropdown_milestone_spec.rb | 8 +++++ .../issues/filtered_search/filter_issues_spec.rb | 11 +++++-- spec/finders/issues_finder_spec.rb | 35 ++++++++++++++++++++++ 15 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/better-priority-sorting-2.yml diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js b/app/assets/javascripts/boards/components/modal/filters/milestone.js index d555599d300..436aa981c59 100644 --- a/app/assets/javascripts/boards/components/modal/filters/milestone.js +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js @@ -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"> diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index e6b53cd4b55..6d5df86f2a5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -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', diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 4c4f94cb9f3..02ff6f5682c 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -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'); } diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2fca012252e..f7ebb1807d7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -310,6 +310,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? @@ -317,6 +321,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) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index aad83731b87..a777db2826b 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -90,11 +90,14 @@ module IssuablesHelper end def milestone_dropdown_label(milestone_title, default_label = "Milestone") - if milestone_title == Milestone::Upcoming.name - milestone_title = Milestone::Upcoming.title - end + 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 to_url_reference(issuable) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7331000a9f2..c0deb59ec4c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -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 diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f0bad69a989..847a86e2e68 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -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" } }) .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 = 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[]" } diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 415361f8fbf..f0d50828e2a 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -6,7 +6,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 diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f1730b1791c..ace6fbaef2b 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -68,6 +68,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 diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 7a21f19ded4..9dbfedb84f1 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -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}" diff --git a/changelogs/unreleased/better-priority-sorting-2.yml b/changelogs/unreleased/better-priority-sorting-2.yml new file mode 100644 index 00000000000..ca0d14718dc --- /dev/null +++ b/changelogs/unreleased/better-priority-sorting-2.yml @@ -0,0 +1,4 @@ +--- +title: Allow filtering by all started milestones +merge_request: +author: diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md index dff36899aec..37afe553e55 100644 --- a/doc/workflow/milestones.md +++ b/doc/workflow/milestones.md @@ -1,13 +1,28 @@ # 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. ![milestone form](milestones/form.png) ## 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. ![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. diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 85ffffe4b6d..ce96a420699 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -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 diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index f079a9627e4..f463312bf57 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -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 diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 2a008427478..ee52dc65175 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -101,6 +101,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 } } -- cgit v1.2.1