Commit 00f94399 authored by Zack Cuddy's avatar Zack Cuddy Committed by Michael Kozono

Global Search - Bold Issue Search Term

This adds a method to bold the search
term on the issues results page.
parent 1f859f89
......@@ -11,6 +11,8 @@ module Projects
push_frontend_feature_flag(:ci_key_autocomplete, default_enabled: true)
end
helper_method :highlight_badge
def show
end
......@@ -50,6 +52,10 @@ module Projects
private
def highlight_badge(name, content, language = nil)
Gitlab::Highlight.highlight(name, content, language: language)
end
def update_params
params.require(:project).permit(*permitted_project_params)
end
......
......@@ -38,6 +38,7 @@ class SearchController < ApplicationController
@show_snippets = search_service.show_snippets?
@search_results = search_service.search_results
@search_objects = search_service.search_objects(preload_method)
@search_highlight = search_service.search_highlight
render_commits if @scope == 'commits'
eager_load_user_status if @scope == 'users'
......
# frozen_string_literal: true
module BlobHelper
def highlight(file_name, file_content, language: nil, plain: false)
highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language)
raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
end
def no_highlight_files
%w(credits changelog news copying copyright license authors)
end
......
......@@ -294,6 +294,13 @@ module SearchHelper
sanitize(html, tags: %w(a p ol ul li pre code))
end
def simple_search_highlight_and_truncate(text, phrase, options = {})
truncate_length = options.delete(:length) { 200 }
text = truncate(text, length: truncate_length)
phrase = phrase.split
highlight(text, phrase, options)
end
def show_user_search_tab?
return false if Feature.disabled?(:users_search, default_enabled: true)
......
......@@ -65,6 +65,10 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method))
end
def search_highlight
search_results.highlight_map(scope)
end
private
def per_page
......
......@@ -15,18 +15,18 @@
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight('.md', badge.to_markdown, language: 'markdown')
= highlight_badge('.md', badge.to_markdown, language: 'markdown')
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight('.html', badge.to_html, language: 'html')
= highlight_badge('.html', badge.to_html, language: 'html')
.row
%hr
.row
.col-md-2.text-center
AsciiDoc
.col-md-10.code.js-syntax-highlight
= highlight('.adoc', badge.to_asciidoc)
= highlight_badge('.adoc', badge.to_asciidoc)
......@@ -9,6 +9,8 @@
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issue.title
.gl-text-gray-500.gl-my-3
= sprintf(s_(' %{project_name}#%{issue_iid} &middot; opened %{issue_created} by %{author}'), { project_name: issue.project.full_name, issue_iid: issue.iid, issue_created: time_ago_with_tooltip(issue.created_at, placement: 'bottom'), author: link_to_member(@project, issue.author, avatar: false) }).html_safe
- if issue.description.present?
.description.term.col-sm-10.gl-px-0
= truncate(issue.description, length: 200)
.description.term.col-sm-10.gl-px-0
- if search_service.use_elasticsearch? && @search_highlight[issue.id]&.description.present?
= Truncato.truncate(@search_highlight[issue.id].description.first, count_tags: false, count_tail: false, max_length: 200).html_safe
- elsif issue.description.present?
= simple_search_highlight_and_truncate(issue.description, @search_term, highlighter: '<span class="gl-text-black-normal gl-font-weight-bold">\1</span>')
---
title: Global Search - Bold Issue's Search Term
merge_request: 41411
author:
type: changed
......@@ -44,7 +44,13 @@ module Elastic
memo[field.to_sym] = {}
end
{ fields: es_fields }
# Adding number_of_fragments: 0 to not split results into snippets. This way controllers can decide how to handle the highlighted data.
{
fields: es_fields,
number_of_fragments: 0,
pre_tags: ['<span class="gl-text-black-normal gl-font-weight-bold">'],
post_tags: ['</span>']
}
end
def basic_query_hash(fields, query)
......
......@@ -46,6 +46,25 @@ module Gitlab
end
end
# Pull the highlight attribute out of Elasticsearch results
# and map it to the result id
def highlight_map(scope)
results = case scope
when 'projects'
projects
when 'issues'
issues
when 'merge_requests'
merge_requests
when 'milestones'
milestones
when 'notes'
notes
end
results.to_h { |x| [x[:_source][:id], x[:highlight]] } if results.present?
end
def formatted_count(scope)
case scope
when 'projects'
......
......@@ -16,6 +16,10 @@ module Gitlab
limited_snippet_titles_count
end
def highlight_map(scope)
snippet_titles.to_h { |x| [x[:_source][:id], x[:highlight]] }
end
private
def snippet_titles
......
......@@ -12,6 +12,32 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let(:project_2) { create(:project, :public, :repository, :wiki_repo) }
let(:limit_project_ids) { [project_1.id] }
describe '#highlight_map' do
using RSpec::Parameterized::TableSyntax
let(:results) { described_class.new(user, 'hello world', limit_project_ids) }
where(:scope, :results_method, :expected) do
'projects' | :projects | { 1 => 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }
'milestones' | :milestones | { 1 => 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }
'notes' | :notes | { 1 => 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }
'issues' | :issues | { 1 => 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }
'merge_requests' | :merge_requests | { 1 => 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }
'blobs' | nil | nil
'wiki_blobs' | nil | nil
'commits' | nil | nil
'users' | nil | nil
'unknown' | nil | nil
end
with_them do
it 'returns the expected highlight map' do
expect(results).to receive(results_method).and_return([{ _source: { id: 1 }, highlight: 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }]) if results_method
expect(results.highlight_map(scope)).to eq(expected)
end
end
end
describe '#formatted_count' do
using RSpec::Parameterized::TableSyntax
......@@ -542,7 +568,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
describe 'Blobs' do
describe 'blobs' do
before do
project_1.repository.index_commits_and_blobs
......@@ -752,7 +778,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
describe 'Wikis' do
describe 'wikis' do
let(:results) { described_class.new(user, 'term', limit_project_ids) }
subject(:wiki_blobs) { results.objects('wiki_blobs') }
......@@ -839,7 +865,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
describe 'Commits' do
describe 'commits' do
before do
project_1.repository.index_commits_and_blobs
ensure_elasticsearch_index!
......@@ -875,7 +901,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
describe 'Visibility levels' do
describe 'visibility levels' do
let(:internal_project) { create(:project, :internal, :repository, :wiki_repo, description: "Internal project") }
let(:private_project1) { create(:project, :private, :repository, :wiki_repo, description: "Private project") }
let(:private_project2) { create(:project, :private, :repository, :wiki_repo, description: "Private project where I'm a member") }
......@@ -886,7 +912,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
private_project2.project_members.create(user: user, access_level: ProjectMember::DEVELOPER)
end
context 'Issues' do
context 'issues' do
it 'finds right set of issues' do
issue_1 = create :issue, project: internal_project, title: "Internal project"
create :issue, project: private_project1, title: "Private project"
......@@ -913,7 +939,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
context 'Milestones' do
context 'milestones' do
let!(:milestone_1) { create(:milestone, project: internal_project, title: "Internal project") }
let!(:milestone_2) { create(:milestone, project: private_project1, title: "Private project") }
let!(:milestone_3) { create(:milestone, project: private_project2, title: "Private project which user is member") }
......@@ -1033,7 +1059,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
context 'Projects' do
context 'projects' do
it_behaves_like 'a paginated object', 'projects'
it 'finds right set of projects' do
......@@ -1062,7 +1088,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
context 'Merge Requests' do
context 'merge requests' do
it 'finds right set of merge requests' do
merge_request_1 = create :merge_request, target_project: internal_project, source_project: internal_project, title: "Internal project"
create :merge_request, target_project: private_project1, source_project: private_project1, title: "Private project"
......@@ -1089,7 +1115,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
context 'Wikis' do
context 'wikis' do
before do
[public_project, internal_project, private_project1, private_project2].each do |project|
project.wiki.create_page('index_page', 'term')
......@@ -1116,7 +1142,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
context 'Commits' do
context 'commits' do
it 'finds right set of commits' do
[internal_project, private_project1, private_project2, public_project].each do |project|
project.repository.create_file(
......@@ -1148,7 +1174,7 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end
end
context 'Blobs' do
context 'blobs' do
it 'finds right set of blobs' do
[internal_project, private_project1, private_project2, public_project].each do |project|
project.repository.create_file(
......
......@@ -40,6 +40,13 @@ RSpec.describe Gitlab::Elastic::SnippetSearchResults, :elastic, :sidekiq_might_n
end
end
describe '#highlight_map' do
it 'returns the expected highlight map' do
expect(results).to receive(:snippet_titles).and_return([{ _source: { id: 1 }, highlight: 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' }])
expect(results.highlight_map('snippet_titles')).to eq({ 1 => 'test <span class="gl-text-black-normal gl-font-weight-bold">highlight</span>' })
end
end
context 'when user is not author' do
let(:results) { described_class.new(create(:user), 'foo', []) }
......
......@@ -116,6 +116,11 @@ module Gitlab
UsersFinder.new(current_user, search: query).execute
end
# highlighting is only performed by Elasticsearch backed results
def highlight_map(scope)
{}
end
private
def projects
......
......@@ -17,10 +17,10 @@ RSpec.describe 'list of badges' do
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
expect(page).to have_content 'AsciiDoc'
expect(page).to have_css('.highlight', count: 3)
expect(page).to have_css('.js-syntax-highlight', count: 3)
expect(page).to have_xpath("//img[@alt='pipeline status']")
page.within('.highlight', match: :first) do
page.within('.js-syntax-highlight', match: :first) do
expect(page).to have_content 'badges/master/pipeline.svg'
end
end
......@@ -32,10 +32,10 @@ RSpec.describe 'list of badges' do
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
expect(page).to have_content 'AsciiDoc'
expect(page).to have_css('.highlight', count: 3)
expect(page).to have_css('.js-syntax-highlight', count: 3)
expect(page).to have_xpath("//img[@alt='coverage report']")
page.within('.highlight', match: :first) do
page.within('.js-syntax-highlight', match: :first) do
expect(page).to have_content 'badges/master/coverage.svg'
end
end
......
......@@ -5,16 +5,6 @@ require 'spec_helper'
RSpec.describe BlobHelper do
include TreeHelper
describe '#highlight' do
it 'wraps highlighted content' do
expect(helper.highlight('test.rb', '52')).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="ruby"><span class="mi">52</span></span></code></pre>])
end
it 'handles plain version' do
expect(helper.highlight('test.rb', '52', plain: true)).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="">52</span></code></pre>])
end
end
describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { File.read(input_svg_path) }
......
......@@ -58,6 +58,25 @@ RSpec.describe Gitlab::SearchResults do
end
end
describe '#highlight_map' do
using RSpec::Parameterized::TableSyntax
where(:scope, :expected) do
'projects' | {}
'issues' | {}
'merge_requests' | {}
'milestones' | {}
'users' | {}
'unknown' | {}
end
with_them do
it 'returns the expected highlight_map' do
expect(results.highlight_map(scope)).to eq(expected)
end
end
end
describe '#formatted_limited_count' do
using RSpec::Parameterized::TableSyntax
......
......@@ -21,6 +21,12 @@ RSpec.describe Gitlab::SnippetSearchResults do
end
end
describe '#highlight_map' do
it 'returns the expected highlight map' do
expect(results.highlight_map('snippet_titles')).to eq({})
end
end
describe '#objects' do
it 'uses page and per_page to paginate results' do
snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo')
......
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