Commit dfa31d5f authored by Sean McGivern's avatar Sean McGivern

Merge branch '54905-milestone-search' into 'master'

Resolve "Milestone search"

Closes #54905

See merge request gitlab-org/gitlab-ce!24265
parents 04c9ab31 22eb2e4c
...@@ -27,7 +27,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController ...@@ -27,7 +27,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def group_milestones def group_milestones
groups = GroupsFinder.new(current_user, all_available: false).execute groups = GroupsFinder.new(current_user, all_available: false).execute
DashboardGroupMilestone.build_collection(groups) DashboardGroupMilestone.build_collection(groups, params)
end end
# See [#39545](https://gitlab.com/gitlab-org/gitlab-ce/issues/39545) for info about the deprecation of dynamic milestones # See [#39545](https://gitlab.com/gitlab-org/gitlab-ce/issues/39545) for info about the deprecation of dynamic milestones
......
...@@ -115,6 +115,6 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -115,6 +115,6 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def search_params def search_params
params.permit(:state).merge(group_ids: group.id) params.permit(:state, :search_title).merge(group_ids: group.id)
end end
end end
...@@ -147,6 +147,6 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -147,6 +147,6 @@ class Projects::MilestonesController < Projects::ApplicationController
groups = project_group.self_and_ancestors.select(:id) groups = project_group.self_and_ancestors.select(:id)
end end
params.permit(:state).merge(project_ids: @project.id, group_ids: groups) params.permit(:state, :search_title).merge(project_ids: @project.id, group_ids: groups)
end end
end end
...@@ -22,6 +22,7 @@ class MilestonesFinder ...@@ -22,6 +22,7 @@ class MilestonesFinder
items = Milestone.all items = Milestone.all
items = by_groups_and_projects(items) items = by_groups_and_projects(items)
items = by_title(items) items = by_title(items)
items = by_search_title(items)
items = by_state(items) items = by_state(items)
order(items) order(items)
...@@ -43,6 +44,14 @@ class MilestonesFinder ...@@ -43,6 +44,14 @@ class MilestonesFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def by_search_title(items)
if params[:search_title].present?
items.search_title(params[:search_title])
else
items
end
end
def by_state(items) def by_state(items)
Milestone.filter_by_state(items, params[:state]) Milestone.filter_by_state(items, params[:state])
end end
......
...@@ -11,11 +11,12 @@ class DashboardGroupMilestone < GlobalMilestone ...@@ -11,11 +11,12 @@ class DashboardGroupMilestone < GlobalMilestone
@group_name = milestone.group.full_name @group_name = milestone.group.full_name
end end
def self.build_collection(groups) def self.build_collection(groups, params)
Milestone.of_groups(groups.select(:id)) milestones = Milestone.of_groups(groups.select(:id))
.reorder_by_due_date_asc .reorder_by_due_date_asc
.order_by_name_asc .order_by_name_asc
.active .active
.map { |m| new(m) } milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
milestones.map { |m| new(m) }
end end
end end
...@@ -27,6 +27,7 @@ class GlobalMilestone ...@@ -27,6 +27,7 @@ class GlobalMilestone
items = Milestone.of_projects(projects) items = Milestone.of_projects(projects)
.reorder_by_due_date_asc .reorder_by_due_date_asc
.order_by_name_asc .order_by_name_asc
items = items.search_title(params[:search_title]) if params[:search_title].present?
Milestone.filter_by_state(items, params[:state]).map { |m| new(m) } Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
end end
......
...@@ -5,9 +5,10 @@ class GroupMilestone < GlobalMilestone ...@@ -5,9 +5,10 @@ class GroupMilestone < GlobalMilestone
def self.build_collection(group, projects, params) def self.build_collection(group, projects, params)
params = params =
{ state: params[:state] } { state: params[:state], search_title: params[:search_title] }
project_milestones = Milestone.of_projects(projects) project_milestones = Milestone.of_projects(projects)
project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present?
child_milestones = Milestone.filter_by_state(project_milestones, params[:state]) child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
grouped_milestones = child_milestones.group_by(&:title) grouped_milestones = child_milestones.group_by(&:title)
......
...@@ -77,7 +77,7 @@ class Milestone < ActiveRecord::Base ...@@ -77,7 +77,7 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title alias_attribute :name, :title
class << self class << self
# Searches for milestones matching the given query. # Searches for milestones with a matching title or description.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
# #
...@@ -88,6 +88,17 @@ class Milestone < ActiveRecord::Base ...@@ -88,6 +88,17 @@ class Milestone < ActiveRecord::Base
fuzzy_search(query, [:title, :description]) fuzzy_search(query, [:title, :description])
end end
# Searches for milestones with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search_title(query)
fuzzy_search(query, [:title])
end
def filter_by_state(milestones, state) def filter_by_state(milestones, state)
case state case state
when 'closed' then milestones.closed when 'closed' then milestones.closed
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
.top-area .top-area
= render 'shared/milestones_filter', counts: @milestone_states = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
= render 'shared/milestones/search_form'
.milestones .milestones
%ul.content-list %ul.content-list
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
= render 'shared/milestones_filter', counts: @milestone_states = render 'shared/milestones_filter', counts: @milestone_states
.nav-controls .nav-controls
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group) - if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success" = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success"
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls .nav-controls
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown' = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
= link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do = link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do
......
= form_tag request.path, method: :get do |f|
= search_field_tag :search_title, params[:search_title],
placeholder: _('Filter by milestone name'),
class: 'form-control input-short',
spellcheck: false
= hidden_field_tag :state, params[:state]
= hidden_field_tag :sort, params[:sort]
---
title: Adds milestone search
merge_request: 24265
author: Jacopo Beschi @jacopo-beschi
type: added
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
# #
# Example usage: # Example usage:
# #
# union = Gitlab::SQL::Union.new(user.personal_projects, user.projects) # union = Gitlab::SQL::Union.new([user.personal_projects, user.projects])
# sql = union.to_sql # sql = union.to_sql
# #
# Project.where("id IN (#{sql})") # Project.where("id IN (#{sql})")
......
...@@ -3199,6 +3199,9 @@ msgstr "" ...@@ -3199,6 +3199,9 @@ msgstr ""
msgid "Filter by commit message" msgid "Filter by commit message"
msgstr "" msgstr ""
msgid "Filter by milestone name"
msgstr ""
msgid "Filter by two-factor authentication" msgid "Filter by two-factor authentication"
msgstr "" msgstr ""
......
...@@ -56,6 +56,24 @@ describe Dashboard::MilestonesController do ...@@ -56,6 +56,24 @@ describe Dashboard::MilestonesController do
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
it 'searches legacy project milestones by title when search_title is given' do
project_milestone = create(:milestone, title: 'Project milestone title', project: project)
get :index, params: { search_title: 'Project mil' }
expect(response.body).to include(project_milestone.title)
expect(response.body).not_to include(group_milestone.title)
end
it 'searches group milestones by title when search_title is given' do
group_milestone = create(:milestone, title: 'Group milestone title', group: group)
get :index, params: { search_title: 'Group mil' }
expect(response.body).to include(group_milestone.title)
expect(response.body).not_to include(project_milestone.title)
end
it 'should contain group and project milestones to which the user belongs to' do it 'should contain group and project milestones to which the user belongs to' do
get :index get :index
......
...@@ -32,10 +32,35 @@ describe Groups::MilestonesController do ...@@ -32,10 +32,35 @@ describe Groups::MilestonesController do
end end
describe '#index' do describe '#index' do
describe 'as HTML' do
render_views
it 'shows group milestones page' do it 'shows group milestones page' do
milestone
get :index, params: { group_id: group.to_param } get :index, params: { group_id: group.to_param }
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.body).to include(milestone.title)
end
it 'searches legacy milestones by title when search_title is given' do
project_milestone = create(:milestone, project: project, title: 'Project milestone title')
get :index, params: { group_id: group.to_param, search_title: 'Project mil' }
expect(response.body).to include(project_milestone.title)
expect(response.body).not_to include(milestone.title)
end
it 'searches group milestones by title when search_title is given' do
group_milestone = create(:milestone, title: 'Group milestone title', group: group)
get :index, params: { group_id: group.to_param, search_title: 'Group mil' }
expect(response.body).to include(group_milestone.title)
expect(response.body).not_to include(milestone.title)
end
end end
context 'as JSON' do context 'as JSON' do
......
...@@ -42,10 +42,11 @@ describe Projects::MilestonesController do ...@@ -42,10 +42,11 @@ describe Projects::MilestonesController do
describe "#index" do describe "#index" do
context "as html" do context "as html" do
def render_index(project:, page:) def render_index(project:, page:, search_title: '')
get :index, params: { get :index, params: {
namespace_id: project.namespace.id, namespace_id: project.namespace.id,
project_id: project.id, project_id: project.id,
search_title: search_title,
page: page page: page
} }
end end
...@@ -59,6 +60,15 @@ describe Projects::MilestonesController do ...@@ -59,6 +60,15 @@ describe Projects::MilestonesController do
expect(milestones.where(project_id: nil)).to be_empty expect(milestones.where(project_id: nil)).to be_empty
end end
it 'searches milestones by title when search_title is given' do
milestone1 = create(:milestone, title: 'Project milestone title', project: project)
render_index project: project, page: 1, search_title: 'Project mile'
milestones = assigns(:milestones)
expect(milestones).to eq([milestone1])
end
it 'renders paginated milestones without missing or duplicates' do it 'renders paginated milestones without missing or duplicates' do
allow(Milestone).to receive(:default_per_page).and_return(2) allow(Milestone).to receive(:default_per_page).and_return(2)
create_list(:milestone, 5, project: project) create_list(:milestone, 5, project: project)
......
...@@ -69,6 +69,12 @@ describe MilestonesFinder do ...@@ -69,6 +69,12 @@ describe MilestonesFinder do
expect(result.to_a).to contain_exactly(milestone_1) expect(result.to_a).to contain_exactly(milestone_1)
end end
it 'filters by search_title' do
result = described_class.new(params.merge(search_title: 'one t')).execute
expect(result.to_a).to contain_exactly(milestone_1)
end
end end
describe '#find_by' do describe '#find_by' do
......
...@@ -91,6 +91,12 @@ describe GlobalMilestone do ...@@ -91,6 +91,12 @@ describe GlobalMilestone do
it 'sorts collection by due date' do it 'sorts collection by due date' do
expect(global_milestones.map(&:due_date)).to eq [milestone1_due_date, milestone1_due_date, milestone1_due_date, nil, nil, nil] expect(global_milestones.map(&:due_date)).to eq [milestone1_due_date, milestone1_due_date, milestone1_due_date, nil, nil, nil]
end end
it 'filters milestones by search_title when params[:search_title] is present' do
global_milestones = described_class.build_collection(projects, { search_title: 'v1.2' })
expect(global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'Milestone v1.2', 'Milestone v1.2'])
end
end end
context 'when adding new milestones' do context 'when adding new milestones' do
......
...@@ -240,6 +240,29 @@ describe Milestone do ...@@ -240,6 +240,29 @@ describe Milestone do
end end
end end
describe '#search_title' do
let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
it 'returns milestones with a matching title' do
expect(described_class.search_title(milestone.title)) .to eq([milestone])
end
it 'returns milestones with a partially matching title' do
expect(described_class.search_title(milestone.title[0..2])).to eq([milestone])
end
it 'returns milestones with a matching title regardless of the casing' do
expect(described_class.search_title(milestone.title.upcase))
.to eq([milestone])
end
it 'searches only on the title and ignores milestones with a matching description' do
create(:milestone, title: 'bar', description: 'foo')
expect(described_class.search_title(milestone.title)) .to eq([milestone])
end
end
describe '#for_projects_and_groups' do describe '#for_projects_and_groups' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:project_other) { create(:project) } let(:project_other) { create(:project) }
......
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