Commit 6c8079d4 authored by Adam Hegyi's avatar Adam Hegyi

Merge branch '20444-limit-full-path-search' into 'master'

Limit full path search to certain places only

See merge request gitlab-org/gitlab!21910
parents e64f2227 6ca84a86
...@@ -64,6 +64,7 @@ export default { ...@@ -64,6 +64,7 @@ export default {
this.groupId, this.groupId,
term, term,
{ {
search_namespaces: true,
with_issues_enabled: true, with_issues_enabled: true,
with_shared: false, with_shared: false,
include_subgroups: true, include_subgroups: true,
......
...@@ -54,6 +54,7 @@ const projectSelect = () => { ...@@ -54,6 +54,7 @@ const projectSelect = () => {
this.groupId, this.groupId,
query.term, query.term,
{ {
search_namespaces: true,
with_issues_enabled: this.withIssuesEnabled, with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled, with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared, with_shared: this.withShared,
......
...@@ -25,7 +25,7 @@ module Autocomplete ...@@ -25,7 +25,7 @@ module Autocomplete
def execute def execute
current_user current_user
.projects_where_can_admin_issues .projects_where_can_admin_issues
.optionally_search(search) .optionally_search(search, include_namespace: true)
.excluding_project(project_id) .excluding_project(project_id)
.eager_load_namespace_and_owner .eager_load_namespace_and_owner
.sorted_by_name_asc_limited(LIMIT) .sorted_by_name_asc_limited(LIMIT)
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
# tags: string[] # tags: string[]
# personal: boolean # personal: boolean
# search: string # search: string
# search_namespaces: boolean
# non_archived: boolean # non_archived: boolean
# archived: 'only' or boolean # archived: 'only' or boolean
# min_access_level: integer # min_access_level: integer
...@@ -171,7 +172,7 @@ class ProjectsFinder < UnionFinder ...@@ -171,7 +172,7 @@ class ProjectsFinder < UnionFinder
def by_search(items) def by_search(items)
params[:search] ||= params[:name] params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
end end
def by_deleted_status(items) def by_deleted_status(items)
......
...@@ -12,8 +12,8 @@ module OptionallySearch ...@@ -12,8 +12,8 @@ module OptionallySearch
end end
# Optionally limits a result set to those matching the given search query. # Optionally limits a result set to those matching the given search query.
def optionally_search(query = nil) def optionally_search(query = nil, **options)
query.present? ? search(query) : all query.present? ? search(query, **options) : all
end end
end end
end end
...@@ -591,9 +591,9 @@ class Project < ApplicationRecord ...@@ -591,9 +591,9 @@ class Project < ApplicationRecord
# case-insensitive. # case-insensitive.
# #
# query - The search query as a String. # query - The search query as a String.
def search(query) def search(query, include_namespace: false)
if Feature.enabled?(:project_search_by_full_path, default_enabled: true) if include_namespace && Feature.enabled?(:project_search_by_full_path, default_enabled: true)
joins(:route).fuzzy_search(query, [Route.arel_table[:path], :name, :description]) joins(:route).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name], :description])
else else
fuzzy_search(query, [:path, :name, :description]) fuzzy_search(query, [:path, :name, :description])
end end
......
---
title: Only enable searching of projects by full path / name on certain dropdowns
merge_request: 21910
author:
type: changed
...@@ -46,6 +46,7 @@ GET /projects ...@@ -46,6 +46,7 @@ GET /projects
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria | | `search` | string | no | Return list of projects matching the search criteria |
| `search_namespaces` | boolean | no | Include ancestor namespaces when matching search criteria. Default is `false` |
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | | `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
| `owned` | boolean | no | Limit by projects explicitly owned by the current user | | `owned` | boolean | no | Limit by projects explicitly owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of | | `membership` | boolean | no | Limit by projects that the current user is a member of |
......
...@@ -505,6 +505,7 @@ module API ...@@ -505,6 +505,7 @@ module API
finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility] finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
finder_params[:archived] = archived_param unless params[:archived].nil? finder_params[:archived] = archived_param unless params[:archived].nil?
finder_params[:search] = params[:search] if params[:search] finder_params[:search] = params[:search] if params[:search]
finder_params[:search_namespaces] = true if params[:search_namespaces].present?
finder_params[:user] = params.delete(:user) if params[:user] finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes] finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level] finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
......
...@@ -63,6 +63,7 @@ module API ...@@ -63,6 +63,7 @@ module API
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility' desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of projects matching the search criteria' optional :search, type: String, desc: 'Return list of projects matching the search criteria'
optional :search_namespaces, type: Boolean, desc: "Include ancestor namespaces when matching search criteria"
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status' optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of' optional :membership, type: Boolean, default: false, desc: 'Limit by projects that the current user is a member of'
......
...@@ -100,6 +100,8 @@ describe 'Group issues page' do ...@@ -100,6 +100,8 @@ describe 'Group issues page' do
find('.empty-state .js-lazy-loaded') find('.empty-state .js-lazy-loaded')
find('.new-project-item-link').click find('.new-project-item-link').click
find('.select2-input').set(group.name)
page.within('.select2-results') do page.within('.select2-results') do
expect(page).to have_content(project.full_name) expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_issues_disabled.full_name) expect(page).not_to have_content(project_with_issues_disabled.full_name)
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Autocomplete::MoveToProjectFinder do describe Autocomplete::MoveToProjectFinder do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:no_access_project) { create(:project) } let(:no_access_project) { create(:project) }
let(:guest_project) { create(:project) } let(:guest_project) { create(:project) }
...@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do ...@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do
expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a)
.to eq([wadus_project]) .to eq([wadus_project])
end end
it 'allows searching by parent namespace' do
group = create(:group)
other_project = create(:project, group: group)
other_project.add_maintainer(user)
expect(described_class.new(user, project_id: project.id, search: group.name).execute.to_a)
.to contain_exactly(other_project)
end
end end
end end
end end
...@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
include AdminModeHelper include AdminModeHelper
describe '#execute' do describe '#execute' do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
let!(:private_project) do let_it_be(:private_project) do
create(:project, :private, name: 'A', path: 'A') create(:project, :private, name: 'A', path: 'A')
end end
let!(:internal_project) do let_it_be(:internal_project) do
create(:project, :internal, group: group, name: 'B', path: 'B') create(:project, :internal, group: group, name: 'B', path: 'B')
end end
let!(:public_project) do let_it_be(:public_project) do
create(:project, :public, group: group, name: 'C', path: 'C') create(:project, :public, group: group, name: 'C', path: 'C')
end end
let!(:shared_project) do let_it_be(:shared_project) do
create(:project, :private, name: 'D', path: 'D') create(:project, :private, name: 'D', path: 'D')
end end
...@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
describe 'filter by group name' do
let(:params) { { name: group.name, search_namespaces: true } }
it { is_expected.to eq([public_project, internal_project]) }
end
describe 'filter by archived' do describe 'filter by archived' do
let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') } let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') }
......
...@@ -22,12 +22,22 @@ describe OptionallySearch do ...@@ -22,12 +22,22 @@ describe OptionallySearch do
it 'delegates to the search method' do it 'delegates to the search method' do
expect(model) expect(model)
.to receive(:search) .to receive(:search)
.with('foo') .with('foo', {})
model.optionally_search('foo') model.optionally_search('foo')
end end
end end
context 'when an option is provided' do
it 'delegates to the search method' do
expect(model)
.to receive(:search)
.with('foo', some_option: true)
model.optionally_search('foo', some_option: true)
end
end
context 'when no query is given' do context 'when no query is given' do
it 'returns the current relation' do it 'returns the current relation' do
expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation)
......
...@@ -1757,7 +1757,7 @@ describe Project do ...@@ -1757,7 +1757,7 @@ describe Project do
expect(described_class.search(project.path.upcase)).to eq([project]) expect(described_class.search(project.path.upcase)).to eq([project])
end end
context 'by full path' do context 'when include_namespace is true' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
...@@ -1767,11 +1767,11 @@ describe Project do ...@@ -1767,11 +1767,11 @@ describe Project do
end end
it 'returns projects that match the group path' do it 'returns projects that match the group path' do
expect(described_class.search(group.path)).to eq([project]) expect(described_class.search(group.path, include_namespace: true)).to eq([project])
end end
it 'returns projects that match the full path' do it 'returns projects that match the full path' do
expect(described_class.search(project.full_path)).to eq([project]) expect(described_class.search(project.full_path, include_namespace: true)).to eq([project])
end end
end end
...@@ -1781,11 +1781,11 @@ describe Project do ...@@ -1781,11 +1781,11 @@ describe Project do
end end
it 'returns no results when searching by group path' do it 'returns no results when searching by group path' do
expect(described_class.search(group.path)).to be_empty expect(described_class.search(group.path, include_namespace: true)).to be_empty
end end
it 'returns no results when searching by full path' do it 'returns no results when searching by full path' do
expect(described_class.search(project.full_path)).to be_empty expect(described_class.search(project.full_path, include_namespace: true)).to be_empty
end end
end end
end end
......
...@@ -362,6 +362,21 @@ describe API::Projects do ...@@ -362,6 +362,21 @@ describe API::Projects do
end end
end end
context 'and using search and search_namespaces is true' do
let(:group) { create(:group) }
let!(:project_in_group) { create(:project, group: group) }
before do
group.add_guest(user)
end
it_behaves_like 'projects response' do
let(:filter) { { search: group.name, search_namespaces: true } }
let(:current_user) { user }
let(:projects) { [project_in_group] }
end
end
context 'and using id_after' do context 'and using id_after' do
it_behaves_like 'projects response' do it_behaves_like 'projects response' do
let(:filter) { { id_after: project2.id } } let(:filter) { { id_after: project2.id } }
......
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