From 22eb2e4c227b060981bb37708222cdd07e825542 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Thu, 17 Jan 2019 15:35:23 +0100 Subject: Adds milestone search Adds to search ILIKE search for milestones title in: - Milestones dashboard - Group milestones page - Project milestones page --- app/controllers/dashboard/milestones_controller.rb | 2 +- app/controllers/groups/milestones_controller.rb | 2 +- app/controllers/projects/milestones_controller.rb | 2 +- app/finders/milestones_finder.rb | 9 +++++++ app/models/dashboard_group_milestone.rb | 7 ++--- app/models/global_milestone.rb | 1 + app/models/group_milestone.rb | 3 ++- app/models/milestone.rb | 13 ++++++++- app/views/dashboard/milestones/index.html.haml | 2 ++ app/views/groups/milestones/index.html.haml | 1 + app/views/projects/milestones/index.html.haml | 1 + app/views/shared/milestones/_search_form.html.haml | 8 ++++++ changelogs/unreleased/54905-milestone-search.yml | 5 ++++ lib/gitlab/sql/union.rb | 2 +- locale/gitlab.pot | 3 +++ .../dashboard/milestones_controller_spec.rb | 18 +++++++++++++ .../groups/milestones_controller_spec.rb | 31 +++++++++++++++++++--- .../projects/milestones_controller_spec.rb | 12 ++++++++- spec/finders/milestones_finder_spec.rb | 6 +++++ spec/models/global_milestone_spec.rb | 6 +++++ spec/models/milestone_spec.rb | 23 ++++++++++++++++ 21 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 app/views/shared/milestones/_search_form.html.haml create mode 100644 changelogs/unreleased/54905-milestone-search.yml diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 3802aa5f40f..9484e4d30cd 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -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 diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 868deea3f01..7ed4384089b 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -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 diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 8bc59d8a305..f6f61b6e5fb 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -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 diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index fcd54b6106e..77b55cbb838 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -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 diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb index 9bcc95e35a5..74aa04ab7d0 100644 --- a/app/models/dashboard_group_milestone.rb +++ b/app/models/dashboard_group_milestone.rb @@ -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 diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 4e82f3fed27..fd17745b035 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -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 diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index a58537de319..97cb26c6ea9 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -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) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index b21edce3aad..7ee7da99606 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -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 diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index ae0e38bf0ee..13822d36f15 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -13,6 +13,8 @@ .top-area = render 'shared/milestones_filter', counts: @milestone_states + .nav-controls + = render 'shared/milestones/search_form' .milestones %ul.content-list diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index af4fe8f2ef8..b6fb908c8f6 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -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" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 57f3c640696..ba853ef0708 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -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 diff --git a/app/views/shared/milestones/_search_form.html.haml b/app/views/shared/milestones/_search_form.html.haml new file mode 100644 index 00000000000..403a0224a85 --- /dev/null +++ b/app/views/shared/milestones/_search_form.html.haml @@ -0,0 +1,8 @@ += 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] + diff --git a/changelogs/unreleased/54905-milestone-search.yml b/changelogs/unreleased/54905-milestone-search.yml new file mode 100644 index 00000000000..88717242e7c --- /dev/null +++ b/changelogs/unreleased/54905-milestone-search.yml @@ -0,0 +1,5 @@ +--- +title: Adds milestone search +merge_request: 24265 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index d24d5116167..f05592fc3a3 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -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})") diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 795a46382a7..e64fc61ab39 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3163,6 +3163,9 @@ msgstr "" msgid "Filter by commit message" msgstr "" +msgid "Filter by milestone name" +msgstr "" + msgid "Filter by two-factor authentication" msgstr "" diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index c9ccd5f7c55..8b176e07bc8 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -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 diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 40d991a669c..043cf28514b 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 5892024e756..ac54b3c3952 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -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) diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 656d120311a..ecffbb9e197 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -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 diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index 62699df5611..f93904065c7 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -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 diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 2e436f2cc8a..af7e3d3a6c9 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -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) } -- cgit v1.2.1