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
def group_milestones
groups = GroupsFinder.new(current_user, all_available: false).execute
DashboardGroupMilestone.build_collection(groups)
DashboardGroupMilestone.build_collection(groups, params)
end
# 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
end
def search_params
params.permit(:state).merge(group_ids: group.id)
params.permit(:state, :search_title).merge(group_ids: group.id)
end
end
......@@ -147,6 +147,6 @@ class Projects::MilestonesController < Projects::ApplicationController
groups = project_group.self_and_ancestors.select(:id)
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
......@@ -22,6 +22,7 @@ class MilestonesFinder
items = Milestone.all
items = by_groups_and_projects(items)
items = by_title(items)
items = by_search_title(items)
items = by_state(items)
order(items)
......@@ -43,6 +44,14 @@ class MilestonesFinder
end
# 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)
Milestone.filter_by_state(items, params[:state])
end
......
......@@ -11,11 +11,12 @@ class DashboardGroupMilestone < GlobalMilestone
@group_name = milestone.group.full_name
end
def self.build_collection(groups)
Milestone.of_groups(groups.select(:id))
def self.build_collection(groups, params)
milestones = Milestone.of_groups(groups.select(:id))
.reorder_by_due_date_asc
.order_by_name_asc
.active
.map { |m| new(m) }
milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
milestones.map { |m| new(m) }
end
end
......@@ -27,6 +27,7 @@ class GlobalMilestone
items = Milestone.of_projects(projects)
.reorder_by_due_date_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) }
end
......
......@@ -5,9 +5,10 @@ class GroupMilestone < GlobalMilestone
def self.build_collection(group, projects, params)
params =
{ state: params[:state] }
{ state: params[:state], search_title: params[:search_title] }
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])
grouped_milestones = child_milestones.group_by(&:title)
......
......@@ -77,7 +77,7 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
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.
#
......@@ -88,6 +88,17 @@ class Milestone < ActiveRecord::Base
fuzzy_search(query, [:title, :description])
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)
case state
when 'closed' then milestones.closed
......
......@@ -13,6 +13,8 @@
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
= render 'shared/milestones/search_form'
.milestones
%ul.content-list
......
......@@ -4,6 +4,7 @@
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success"
......
......@@ -6,6 +6,7 @@
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- 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
......
= 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
#
# 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
#
# Project.where("id IN (#{sql})")
......
......@@ -3199,6 +3199,9 @@ msgstr ""
msgid "Filter by commit message"
msgstr ""
msgid "Filter by milestone name"
msgstr ""
msgid "Filter by two-factor authentication"
msgstr ""
......
......@@ -56,6 +56,24 @@ describe Dashboard::MilestonesController do
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
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
get :index
......
......@@ -32,10 +32,35 @@ describe Groups::MilestonesController do
end
describe '#index' do
it 'shows group milestones page' do
get :index, params: { group_id: group.to_param }
describe 'as HTML' do
render_views
expect(response).to have_gitlab_http_status(200)
it 'shows group milestones page' do
milestone
get :index, params: { group_id: group.to_param }
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
context 'as JSON' do
......
......@@ -42,10 +42,11 @@ describe Projects::MilestonesController do
describe "#index" do
context "as html" do
def render_index(project:, page:)
def render_index(project:, page:, search_title: '')
get :index, params: {
namespace_id: project.namespace.id,
project_id: project.id,
search_title: search_title,
page: page
}
end
......@@ -59,6 +60,15 @@ describe Projects::MilestonesController do
expect(milestones.where(project_id: nil)).to be_empty
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
allow(Milestone).to receive(:default_per_page).and_return(2)
create_list(:milestone, 5, project: project)
......
......@@ -69,6 +69,12 @@ describe MilestonesFinder do
expect(result.to_a).to contain_exactly(milestone_1)
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
describe '#find_by' do
......
......@@ -91,6 +91,12 @@ describe GlobalMilestone 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]
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
context 'when adding new milestones' do
......
......@@ -240,6 +240,29 @@ describe Milestone do
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
let(:project) { 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