Commit 84b3b8f6 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '213597-add-in-this-group-option-to-search-dropdown-where-not-present' into 'master'

Enable the `in this group` action in the Search dropdown

See merge request gitlab-org/gitlab!31939
parents 8fc733e6 587e2130
......@@ -31,7 +31,7 @@ export const getProjectSlug = () => {
};
export const getGroupSlug = () => {
if (isInGroupsPage()) {
if (isInProjectPage() || isInGroupsPage()) {
return $('body').data('group');
}
return null;
......
......@@ -2,7 +2,7 @@
import $ from 'jquery';
import { escape, throttle } from 'lodash';
import { s__, __ } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import {
......@@ -170,33 +170,24 @@ export class SearchAutocomplete {
},
})
.then(response => {
// Hide dropdown menu if no suggestions returns
if (!response.data.length) {
this.disableAutocomplete();
return;
}
const options = this.scopedSearchOptions(term);
const data = [];
// List results
let firstCategory = true;
let lastCategory;
let lastCategory = null;
for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
data.push({ type: 'separator' });
}
if (firstCategory) {
firstCategory = false;
}
data.push({
options.push({ type: 'separator' });
options.push({
type: 'header',
content: suggestion.category,
});
lastCategory = suggestion.category;
}
data.push({
// Add the suggestion
options.push({
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
icon: this.getAvatar(suggestion),
category: suggestion.category,
......@@ -204,39 +195,8 @@ export class SearchAutocomplete {
url: suggestion.url,
});
}
// Add option to proceed with the search
if (data.length) {
const icon = spriteIcon('search', 's16 inline-search-icon');
let template;
if (this.projectInputEl.val()) {
template = s__('SearchAutocomplete|in this project');
}
if (this.groupInputEl.val()) {
template = s__('SearchAutocomplete|in this group');
}
data.unshift({ type: 'separator' });
data.unshift({
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
url: `${gon.relative_url_root}/search?search=${term}`,
});
if (template) {
data.unshift({
icon,
text: term,
template,
url: `${
gon.relative_url_root
}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
}
callback(data);
callback(options);
this.loadingSuggestions = false;
this.highlightFirstRow();
......@@ -253,10 +213,10 @@ export class SearchAutocomplete {
// Get options
let options;
if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (isInProjectPage() && projectOptions) {
if (isInProjectPage() && projectOptions) {
options = projectOptions[getProjectSlug()];
} else if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (dashboardOptions) {
options = dashboardOptions;
}
......@@ -301,6 +261,64 @@ export class SearchAutocomplete {
return items;
}
// Add option to proceed with the search for each
// scope that is currently available, namely:
//
// - Search in this project
// - Search in this group (or project's group)
// - Search in all GitLab
scopedSearchOptions(term) {
const icon = spriteIcon('search', 's16 inline-search-icon');
const projectId = this.projectInputEl.val();
const groupId = this.groupInputEl.val();
const options = [];
if (projectId) {
const projectOptions = gl.projectOptions[getProjectSlug()];
const url = groupId
? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}`
: `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}`;
options.push({
icon,
text: term,
template: sprintf(
s__(`SearchAutocomplete|in project %{projectName}`),
{
projectName: `<i>${projectOptions.name}</i>`,
},
false,
),
url,
});
}
if (groupId) {
const groupOptions = gl.groupOptions[getGroupSlug()];
options.push({
icon,
text: term,
template: sprintf(
s__(`SearchAutocomplete|in group %{groupName}`),
{
groupName: `<i>${groupOptions.name}</i>`,
},
false,
),
url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}`,
});
}
options.push({
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
url: `${gon.relative_url_root}/search?search=${term}`,
});
return options;
}
serializeState() {
return {
// Search Criteria
......
......@@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
include Gitlab::SearchContext::ControllerConcern
include SessionlessAuthentication
include SessionsHelper
include ConfirmEmailWarning
......
......@@ -103,7 +103,7 @@ module ApplicationHelper
page: body_data_page,
page_type_id: controller.params[:id],
find_file: find_file_path,
group: "#{@group&.path}"
group: @group&.path
}.merge(project_data)
end
......@@ -113,6 +113,7 @@ module ApplicationHelper
{
project_id: @project.id,
project: @project.path,
group: @project.group&.path,
namespace_id: @project.namespace&.id
}
end
......
......@@ -104,6 +104,16 @@ module PageLayoutHelper
end
end
# This helper ensures there is always a default `Gitlab::SearchContext` available
# to all controller that use the application layout.
def search_context
strong_memoize(:search_context) do
next super if defined?(super)
Gitlab::SearchContext::Builder.new(controller.view_context).build!
end
end
def fluid_layout
current_user && current_user.layout == "fluid"
end
......
- if @group && @group.persisted? && @group.path
- group_data_attrs = { group_path: j(@group.path), name: j(@group.name), issues_path: issues_group_path(@group), mr_path: merge_requests_group_path(@group) }
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
.search.search-form{ data: { track_label: "navbar_search", track_event: "activate_form_input", track_value: "" } }
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
......@@ -27,27 +23,20 @@
= sprite_icon('search', size: 16, css_class: 'search-icon')
= sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input')
= hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
= hidden_field_tag :group_id, search_context.for_group? ? search_context.group.id : '', class: 'js-search-group-options', data: search_context.group_metadata
= hidden_field_tag :project_id, search_context.for_project? ? search_context.project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: search_context.project_metadata
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: project_data_attrs
- if search_context.for_project?
= hidden_field_tag :scope, search_context.scope
= hidden_field_tag :search_code, search_context.code_search?
- if @project && @project.persisted?
- if current_controller?(:issues)
= hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests)
= hidden_field_tag :scope, 'merge_requests'
- elsif current_controller?(:wikis)
= hidden_field_tag :scope, 'wiki_blobs'
- elsif current_controller?(:commits)
= hidden_field_tag :scope, 'commits'
- else
= hidden_field_tag :search_code, true
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
= hidden_field_tag :snippets, search_context.for_snippets?
= hidden_field_tag :repository_ref, search_context.ref
= hidden_field_tag :nav_source, 'navbar'
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test'
%noscript= button_tag 'Search'
.search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
.search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path,
:'data-autocomplete-project-id' => search_context.project.try(:id),
:'data-autocomplete-project-ref' => search_context.ref }
- if project
- search_path_url = search_path(project_id: project.id)
- elsif group
- search_path_url = search_path(group_id: group.id)
- else
- search_path_url = search_path
- has_impersonation_link = header_link?(:admin_impersonation)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
......@@ -36,7 +30,7 @@
%li.nav-item.d-none.d-lg-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item.d-inline-block.d-lg-none
= link_to search_path_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= link_to search_context.search_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search', size: 16)
- if header_link?(:issues)
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
......
---
title: Enable the `in this group` action in the Search dropdown
merge_request: 31939
author:
type: changed
# frozen_string_literal: true
module Gitlab
# Holds the contextual data used by navbar search component to
# determine the search scope, whether to search for code, or if
# a search should target snippets.
#
# Use the SearchContext::Builder to create an instance of this class
class SearchContext
attr_accessor :project, :project_metadata, :ref,
:group, :group_metadata,
:snippets,
:scope, :search_url
def initialize
@ref = nil
@project = nil
@project_metadata = {}
@group = nil
@group_metadata = {}
@snippets = []
@scope = nil
@search_url = nil
end
def for_project?
project.present? && project.persisted?
end
def for_group?
group.present? && group.persisted?
end
def for_snippets?
snippets.any?
end
def code_search?
project.present? && scope.nil?
end
class Builder
def initialize(view_context)
@view_context = view_context
@snippets = []
end
def with_snippet(snippet)
@snippets << snippet
self
end
def with_project(project)
@project = project
with_group(project&.group)
self
end
def with_group(group)
@group = group
self
end
def with_ref(ref)
@ref = ref
self
end
def build!
SearchContext.new.tap do |context|
context.project = @project
context.group = @group
context.ref = @ref
context.snippets = @snippets.dup
context.scope = search_scope
context.search_url = search_url
context.group_metadata = group_search_metadata(@group)
context.project_metadata = project_search_metadata(@project)
end
end
private
attr_accessor :view_context
def project_search_metadata(project)
return {} unless project
{
project_path: project.path,
name: project.name,
issues_path: view_context.project_issues_path(project),
mr_path: view_context.project_merge_requests_path(project),
issues_disabled: !project.issues_enabled?
}
end
def group_search_metadata(group)
return {} unless group
{
group_path: group.path,
name: group.name,
issues_path: view_context.issues_group_path(group),
mr_path: view_context.merge_requests_group_path(group)
}
end
def search_url
if @project.present?
view_context.search_path(project_id: @project.id)
elsif @group.present?
view_context.search_path(group_id: @group.id)
else
view_context.search_path
end
end
def search_scope
if view_context.current_controller?(:issues)
'issues'
elsif view_context.current_controller?(:merge_requests)
'merge_requests'
elsif view_context.current_controller?(:wikis)
'wiki_blobs'
elsif view_context.current_controller?(:commits)
'commits'
else nil
end
end
end
module ControllerConcern
extend ActiveSupport::Concern
included do
helper_method :search_context
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
#
# Introspect the current controller's assignments and
# and builds the proper SearchContext object for it.
def search_context
builder = Builder.new(view_context)
builder.with_snippet(@snippet) if @snippet.present?
@snippets.each(&builder.method(:with_snippet)) if @snippets.present?
builder.with_project(@project) if @project.present? && @project.persisted?
builder.with_group(@group) if @group.present? && @group.persisted?
builder.with_ref(@ref) if @ref.present?
builder.build!
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
......@@ -19086,10 +19086,10 @@ msgstr ""
msgid "SearchAutocomplete|in all GitLab"
msgstr ""
msgid "SearchAutocomplete|in this group"
msgid "SearchAutocomplete|in group %{groupName}"
msgstr ""
msgid "SearchAutocomplete|in this project"
msgid "SearchAutocomplete|in project %{projectName}"
msgstr ""
msgid "SearchCodeResults|in"
......
......@@ -332,8 +332,7 @@ describe Projects::IssuesController do
end
before do
allow(controller).to receive(:find_routable!)
.with(Project, project.full_path, any_args).and_return(project)
allow(controller).to receive(:find_routable!).and_return(project)
allow(project).to receive(:default_branch).and_return(master_branch)
allow_next_instance_of(Issues::RelatedBranchesService) do |service|
allow(service).to receive(:execute).and_return(related_branches)
......
......@@ -95,14 +95,6 @@ describe 'User uses header search field', :js do
expect(page).not_to have_selector('.dropdown-header', text: /#{scope_name}/i)
end
it 'hides the dropdown when there are no results' do
page.within('.search-input-wrap') do
fill_in('search', with: 'a_search_term_with_no_results')
end
expect(page).not_to have_selector('.dropdown-menu')
end
end
end
......
......@@ -278,7 +278,7 @@ describe ApplicationHelper do
page: 'application',
page_type_id: nil,
find_file: nil,
group: ''
group: nil
}
)
end
......@@ -317,7 +317,7 @@ describe ApplicationHelper do
page: 'application',
page_type_id: nil,
find_file: nil,
group: '',
group: nil,
project_id: project.id,
project: project.name,
namespace_id: project.namespace.id
......@@ -325,6 +325,25 @@ describe ApplicationHelper do
)
end
context 'when @project is owned by a group' do
let_it_be(:project) { create(:project, :repository, group: create(:group)) }
it 'includes all possible body data elements and associates the project elements with project' do
expect(helper).to receive(:can?).with(nil, :download_code, project)
expect(helper.body_data).to eq(
{
page: 'application',
page_type_id: nil,
find_file: nil,
group: project.group.name,
project_id: project.id,
project: project.name,
namespace_id: project.namespace.id
}
)
end
end
context 'when controller is issues' do
before do
stub_controller_method(:controller_path, 'projects:issues')
......@@ -342,7 +361,7 @@ describe ApplicationHelper do
page: 'projects:issues:show',
page_type_id: issue.id,
find_file: nil,
group: '',
group: nil,
project_id: issue.project.id,
project: issue.project.name,
namespace_id: issue.project.namespace.id
......
......@@ -117,4 +117,19 @@ describe PageLayoutHelper do
expect(tags).to include(%q{content="foo&quot; http-equiv=&quot;refresh"})
end
end
describe '#search_context' do
subject(:search_context) { helper.search_context }
describe 'a bare controller' do
it 'returns an empty context' do
expect(search_context).to have_attributes(project: nil,
group: nil,
snippets: [],
project_metadata: {},
group_metadata: {},
search_url: '/search')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SearchContext::Builder, type: :controller do
controller(ApplicationController) { }
subject(:builder) { described_class.new(controller.view_context) }
shared_examples "has a fluid interface" do
it { is_expected.to be_instance_of(described_class) }
end
def expected_project_metadata(project)
return {} if project.nil?
a_hash_including(project_path: project.path,
name: project.name,
issues_path: a_string_including("/issues"),
mr_path: a_string_including("/merge_requests"),
issues_disabled: !project.issues_enabled?)
end
def expected_group_metadata(group)
return {} if group.nil?
a_hash_including(group_path: group.path,
name: group.name,
issues_path: a_string_including("/issues"),
mr_path: a_string_including("/merge_requests"))
end
def expected_search_url(project, group)
if project
search_path(project_id: project.id)
elsif group
search_path(group_id: group.id)
else
search_path
end
end
def be_search_context(project: nil, group: nil, snippets: [], ref: nil)
group = project ? project.group : group
snippets.compact!
ref = ref
have_attributes(
project: project,
group: group,
ref: ref,
snippets: snippets,
project_metadata: expected_project_metadata(project),
group_metadata: expected_group_metadata(group),
search_url: expected_search_url(project, group)
)
end
describe '#with_project' do
let(:project) { create(:project) }
subject { builder.with_project(project) }
it_behaves_like "has a fluid interface"
describe '#build!' do
subject(:context) { builder.with_project(project).build! }
context 'when a project is not owned by a group' do
it { is_expected.to be_for_project }
it { is_expected.to be_search_context(project: project) }
end
context 'when a project is owned by a group' do
let(:project) { create(:project, group: create(:group)) }
it 'delegates to `#with_group`' do
expect(builder).to receive(:with_group).with(project.group)
expect(context).to be
end
it { is_expected.to be_search_context(project: project, group: project.group) }
end
end
end
describe '#with_snippet' do
context 'when there is a single snippet' do
let(:snippet) { create(:snippet) }
subject { builder.with_snippet(snippet) }
it_behaves_like "has a fluid interface"
describe '#build!' do
subject(:context) { builder.with_snippet(snippet).build! }
it { is_expected.to be_for_snippet }
it { is_expected.to be_search_context(snippets: [snippet]) }
end
end
context 'when there are multiple snippets' do
let(:snippets) { create_list(:snippet, 3) }
describe '#build!' do
subject(:context) do
snippets.each(&builder.method(:with_snippet))
builder.build!
end
it { is_expected.to be_for_snippet }
it { is_expected.to be_search_context(snippets: snippets) }
end
end
end
describe '#with_group' do
let(:group) { create(:group) }
subject { builder.with_group(group) }
it_behaves_like "has a fluid interface"
describe '#build!' do
subject(:context) { builder.with_group(group).build! }
it { is_expected.to be_for_group }
it { is_expected.to be_search_context(group: group) }
end
end
describe '#with_ref' do
let(:ref) { Gitlab::Git::EMPTY_TREE_ID }
subject { builder.with_ref(ref) }
it_behaves_like "has a fluid interface"
describe '#build!' do
subject(:context) { builder.with_ref(ref).build! }
it { is_expected.to be_search_context(ref: ref) }
end
end
describe '#build!' do
subject(:context) { builder.build! }
it { is_expected.to be_a(Gitlab::SearchContext) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SearchContext::ControllerConcern, type: :controller do
controller(ApplicationController) do
include Gitlab::SearchContext::ControllerConcern
end
let(:project) { nil }
let(:group) { nil }
let(:snippet) { nil }
let(:snippets) { [] }
let(:ref) { nil }
let(:builder) { Gitlab::SearchContext::Builder.new(controller.view_context) }
subject(:search_context) { controller.search_context }
def weak_assign(ivar, value)
return if value.nil?
controller.instance_variable_set(ivar.to_sym, value)
end
before do
weak_assign(:@project, project)
weak_assign(:@group, group)
weak_assign(:@ref, ref)
weak_assign(:@snippet, snippet)
weak_assign(:@snippets, snippets)
allow(Gitlab::SearchContext::Builder).to receive(:new).and_return(builder)
end
shared_examples 'has the proper context' do
it :aggregate_failures do
expected_group = project ? project.group : group
expected_snippets = [snippet, *snippets].compact
expect(builder).to receive(:with_project).with(project).and_call_original if project
expect(builder).to receive(:with_group).with(expected_group).and_call_original if expected_group
expect(builder).to receive(:with_ref).with(ref).and_call_original if ref
expected_snippets.each do |snippet|
expect(builder).to receive(:with_snippet).with(snippet).and_call_original
end
is_expected.to be_a(Gitlab::SearchContext)
end
end
context 'exposing @project' do
let(:project) { create(:project) }
it_behaves_like 'has the proper context'
context 'when the project is owned by a group' do
let(:project) { create(:project, group: create(:group)) }
it_behaves_like 'has the proper context'
end
end
context 'exposing @group' do
let(:group) { create(:group) }
it_behaves_like 'has the proper context'
end
context 'exposing @snippet, @snippets' do
let(:snippet) { create(:snippet) }
let(:snippets) { create_list(:snippet, 3) }
it_behaves_like 'has the proper context'
end
context 'exposing @ref' do
let(:ref) { Gitlab::Git::EMPTY_TREE_ID }
it_behaves_like 'has the proper context'
end
end
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