Commit d95e56f0 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'improve/search_autocomplete' into 'master'

Improve: Search autocomplete

* fetch options via ajax
* only show options related to user input
* add limit to amount of options
parents 96808199 1016b547
...@@ -16,6 +16,7 @@ v 6.5.0 ...@@ -16,6 +16,7 @@ v 6.5.0
- Use jquery timeago plugin - Use jquery timeago plugin
- Fix 500 error for rdoc files - Fix 500 error for rdoc files
- Ability to customize merge commit message (sponsored by Say Media) - Ability to customize merge commit message (sponsored by Say Media)
- Search autocomplete via ajax
v6.4.3 v6.4.3
- Don't use unicorn worker killer if PhusionPassenger is defined - Don't use unicorn worker killer if PhusionPassenger is defined
......
...@@ -47,5 +47,9 @@ class Dispatcher ...@@ -47,5 +47,9 @@ class Dispatcher
initSearch: -> initSearch: ->
autocomplete_json = $('.search-autocomplete-json').data('autocomplete-opts') opts = $('.search-autocomplete-opts')
new SearchAutocomplete(autocomplete_json) path = opts.data('autocomplete-path')
project_id = opts.data('autocomplete-project-id')
project_ref = opts.data('autocomplete-project-ref')
new SearchAutocomplete(path, project_id, project_ref)
class SearchAutocomplete class SearchAutocomplete
constructor: (json) -> constructor: (search_autocomplete_path, project_id, project_ref) ->
project_id = '' unless project_id
project_ref = '' unless project_ref
query = "?project_id=" + project_id + "&project_ref=" + project_ref
$("#search").autocomplete $("#search").autocomplete
source: json source: search_autocomplete_path + query
minLength: 1
select: (event, ui) -> select: (event, ui) ->
location.href = ui.item.url location.href = ui.item.url
......
class SearchController < ApplicationController class SearchController < ApplicationController
include SearchHelper
def show def show
@project = Project.find_by_id(params[:project_id]) if params[:project_id].present? @project = Project.find_by_id(params[:project_id]) if params[:project_id].present?
@group = Group.find_by_id(params[:group_id]) if params[:group_id].present? @group = Group.find_by_id(params[:group_id]) if params[:group_id].present?
...@@ -10,4 +12,12 @@ class SearchController < ApplicationController ...@@ -10,4 +12,12 @@ class SearchController < ApplicationController
@search_results = Search::GlobalService.new(current_user, params).execute @search_results = Search::GlobalService.new(current_user, params).execute
end end
end end
def autocomplete
term = params[:term]
@project = Project.find(params[:project_id]) if params[:project_id].present?
@ref = params[:project_ref] if params[:project_ref].present?
render json: search_autocomplete_opts(term).to_json
end
end end
module SearchHelper module SearchHelper
def search_autocomplete_source def search_autocomplete_opts(term)
return unless current_user return unless current_user
resources_results = [
groups_autocomplete(term),
projects_autocomplete(term),
public_projects_autocomplete(term),
].flatten
generic_results = project_autocomplete + default_autocomplete + help_autocomplete
generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
[ [
groups_autocomplete, resources_results,
projects_autocomplete, generic_results
public_projects_autocomplete,
default_autocomplete,
project_autocomplete,
help_autocomplete
].flatten.uniq do |item| ].flatten.uniq do |item|
item[:label] item[:label]
end.to_json end
end end
private private
...@@ -43,7 +49,7 @@ module SearchHelper ...@@ -43,7 +49,7 @@ module SearchHelper
# Autocomplete results for the current project, if it's defined # Autocomplete results for the current project, if it's defined
def project_autocomplete def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref if @project && @project.repository.exists? && @project.repository.root_ref
prefix = simple_sanitize(@project.name_with_namespace) prefix = search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref ref = @ref || @project.repository.root_ref
[ [
...@@ -65,23 +71,36 @@ module SearchHelper ...@@ -65,23 +71,36 @@ module SearchHelper
end end
# Autocomplete results for the current user's groups # Autocomplete results for the current user's groups
def groups_autocomplete def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.map do |group| current_user.authorized_groups.search(term).limit(limit).map do |group|
{ label: "group: #{simple_sanitize(group.name)}", url: group_path(group) } {
label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group)
}
end end
end end
# Autocomplete results for the current user's projects # Autocomplete results for the current user's projects
def projects_autocomplete def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.non_archived.map do |p| current_user.authorized_projects.search_by_title(term).non_archived.limit(limit).map do |p|
{ label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) } {
label: "project: #{search_result_sanitize(p.name_with_namespace)}",
url: project_path(p)
}
end end
end end
# Autocomplete results for the current user's projects # Autocomplete results for the current user's projects
def public_projects_autocomplete def public_projects_autocomplete(term, limit = 5)
Project.public_or_internal_only(current_user).non_archived.map do |p| Project.public_or_internal_only(current_user).search_by_title(term).non_archived.limit(limit).map do |p|
{ label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) } {
label: "project: #{search_result_sanitize(p.name_with_namespace)}",
url: project_path(p)
}
end end
end end
def search_result_sanitize(str)
Sanitize.clean(str)
end
end end
...@@ -138,6 +138,10 @@ class Project < ActiveRecord::Base ...@@ -138,6 +138,10 @@ class Project < ActiveRecord::Base
joins(:namespace).where("projects.archived = ?", false).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%") joins(:namespace).where("projects.archived = ?", false).where("projects.name LIKE :query OR projects.path LIKE :query OR namespaces.name LIKE :query OR projects.description LIKE :query", query: "%#{query}%")
end end
def search_by_title query
where("projects.archived = ?", false).where("LOWER(projects.name) LIKE :query", query: "%#{query.downcase}%")
end
def find_with_namespace(id) def find_with_namespace(id)
if id.include?("/") if id.include?("/")
id = id.split("/") id = id.split("/")
......
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
= hidden_field_tag :search_code, true = hidden_field_tag :search_code, true
= hidden_field_tag :repository_ref, @ref = hidden_field_tag :repository_ref, @ref
= submit_tag 'Go' if ENV['RAILS_ENV'] == 'test' = submit_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-json.hide{:'data-autocomplete-opts' => search_autocomplete_source } .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
...@@ -6,6 +6,7 @@ Gitlab::Application.routes.draw do ...@@ -6,6 +6,7 @@ Gitlab::Application.routes.draw do
# Search # Search
# #
get 'search' => "search#show" get 'search' => "search#show"
get 'search/autocomplete' => "search#autocomplete", as: :search_autocomplete
# API # API
API::API.logger Rails.logger API::API.logger Rails.logger
......
...@@ -13,52 +13,41 @@ describe SearchHelper do ...@@ -13,52 +13,41 @@ describe SearchHelper do
end end
it "it returns nil" do it "it returns nil" do
search_autocomplete_source.should be_nil search_autocomplete_opts("q").should be_nil
end end
end end
context "with a user" do context "with a user" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:result) { JSON.parse(search_autocomplete_source) }
before do before do
allow(self).to receive(:current_user).and_return(user) allow(self).to receive(:current_user).and_return(user)
end end
it "includes Help sections" do it "includes Help sections" do
result.select { |h| h['label'] =~ /^help:/ }.length.should == 9 search_autocomplete_opts("hel").size.should == 9
end end
it "includes default sections" do it "includes default sections" do
result.count { |h| h['label'] =~ /^(My|Admin)\s/ }.should == 4 search_autocomplete_opts("adm").size.should == 1
end end
it "includes the user's groups" do it "includes the user's groups" do
create(:group).add_owner(user) create(:group).add_owner(user)
result.count { |h| h['label'] =~ /^group:/ }.should == 1 search_autocomplete_opts("gro").size.should == 1
end end
it "includes the user's projects" do it "includes the user's projects" do
create(:project, namespace: create(:namespace, owner: user)) project = create(:project, namespace: create(:namespace, owner: user))
result.count { |h| h['label'] =~ /^project:/ }.should == 1 search_autocomplete_opts(project.name).size.should == 1
end end
context "with a current project" do context "with a current project" do
before { @project = create(:project_with_code) } before { @project = create(:project_with_code) }
it "includes project-specific sections" do it "includes project-specific sections" do
result.count { |h| h['label'] =~ /^#{@project.name_with_namespace} - / }.should == 11 search_autocomplete_opts("Files").size.should == 1
end search_autocomplete_opts("Commits").size.should == 1
it "uses @ref in urls if defined" do
@ref = "foo_bar"
result.count { |h| h['url'] == project_tree_path(@project, @ref) }.should == 1
end
end
context "with no current project" do
it "does not include project-specific sections" do
result.count { |h| h['label'] =~ /Files$/ }.should == 0
end end
end end
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