Commit d95e56f05e577a260444681e42a58109834d8284
Exists in
spb-stable
and in
3 other branches
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
Showing
9 changed files
with
75 additions
and
42 deletions
Show diff stats
CHANGELOG
... | ... | @@ -16,6 +16,7 @@ v 6.5.0 |
16 | 16 | - Use jquery timeago plugin |
17 | 17 | - Fix 500 error for rdoc files |
18 | 18 | - Ability to customize merge commit message (sponsored by Say Media) |
19 | + - Search autocomplete via ajax | |
19 | 20 | |
20 | 21 | v6.4.3 |
21 | 22 | - Don't use unicorn worker killer if PhusionPassenger is defined | ... | ... |
app/assets/javascripts/dispatcher.js.coffee
... | ... | @@ -47,5 +47,9 @@ class Dispatcher |
47 | 47 | |
48 | 48 | |
49 | 49 | initSearch: -> |
50 | - autocomplete_json = $('.search-autocomplete-json').data('autocomplete-opts') | |
51 | - new SearchAutocomplete(autocomplete_json) | |
50 | + opts = $('.search-autocomplete-opts') | |
51 | + path = opts.data('autocomplete-path') | |
52 | + project_id = opts.data('autocomplete-project-id') | |
53 | + project_ref = opts.data('autocomplete-project-ref') | |
54 | + | |
55 | + new SearchAutocomplete(path, project_id, project_ref) | ... | ... |
app/assets/javascripts/search_autocomplete.js.coffee
1 | 1 | class SearchAutocomplete |
2 | - constructor: (json) -> | |
2 | + constructor: (search_autocomplete_path, project_id, project_ref) -> | |
3 | + project_id = '' unless project_id | |
4 | + project_ref = '' unless project_ref | |
5 | + query = "?project_id=" + project_id + "&project_ref=" + project_ref | |
6 | + | |
3 | 7 | $("#search").autocomplete |
4 | - source: json | |
8 | + source: search_autocomplete_path + query | |
9 | + minLength: 1 | |
5 | 10 | select: (event, ui) -> |
6 | 11 | location.href = ui.item.url |
7 | 12 | ... | ... |
app/controllers/search_controller.rb
1 | 1 | class SearchController < ApplicationController |
2 | + include SearchHelper | |
3 | + | |
2 | 4 | def show |
3 | 5 | @project = Project.find_by_id(params[:project_id]) if params[:project_id].present? |
4 | 6 | @group = Group.find_by_id(params[:group_id]) if params[:group_id].present? |
... | ... | @@ -10,4 +12,12 @@ class SearchController < ApplicationController |
10 | 12 | @search_results = Search::GlobalService.new(current_user, params).execute |
11 | 13 | end |
12 | 14 | end |
15 | + | |
16 | + def autocomplete | |
17 | + term = params[:term] | |
18 | + @project = Project.find(params[:project_id]) if params[:project_id].present? | |
19 | + @ref = params[:project_ref] if params[:project_ref].present? | |
20 | + | |
21 | + render json: search_autocomplete_opts(term).to_json | |
22 | + end | |
13 | 23 | end | ... | ... |
app/helpers/search_helper.rb
1 | 1 | module SearchHelper |
2 | - def search_autocomplete_source | |
2 | + def search_autocomplete_opts(term) | |
3 | 3 | return unless current_user |
4 | + | |
5 | + resources_results = [ | |
6 | + groups_autocomplete(term), | |
7 | + projects_autocomplete(term), | |
8 | + public_projects_autocomplete(term), | |
9 | + ].flatten | |
10 | + | |
11 | + generic_results = project_autocomplete + default_autocomplete + help_autocomplete | |
12 | + generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") } | |
13 | + | |
4 | 14 | [ |
5 | - groups_autocomplete, | |
6 | - projects_autocomplete, | |
7 | - public_projects_autocomplete, | |
8 | - default_autocomplete, | |
9 | - project_autocomplete, | |
10 | - help_autocomplete | |
15 | + resources_results, | |
16 | + generic_results | |
11 | 17 | ].flatten.uniq do |item| |
12 | 18 | item[:label] |
13 | - end.to_json | |
19 | + end | |
14 | 20 | end |
15 | 21 | |
16 | 22 | private |
... | ... | @@ -43,7 +49,7 @@ module SearchHelper |
43 | 49 | # Autocomplete results for the current project, if it's defined |
44 | 50 | def project_autocomplete |
45 | 51 | if @project && @project.repository.exists? && @project.repository.root_ref |
46 | - prefix = simple_sanitize(@project.name_with_namespace) | |
52 | + prefix = search_result_sanitize(@project.name_with_namespace) | |
47 | 53 | ref = @ref || @project.repository.root_ref |
48 | 54 | |
49 | 55 | [ |
... | ... | @@ -65,23 +71,36 @@ module SearchHelper |
65 | 71 | end |
66 | 72 | |
67 | 73 | # Autocomplete results for the current user's groups |
68 | - def groups_autocomplete | |
69 | - current_user.authorized_groups.map do |group| | |
70 | - { label: "group: #{simple_sanitize(group.name)}", url: group_path(group) } | |
74 | + def groups_autocomplete(term, limit = 5) | |
75 | + current_user.authorized_groups.search(term).limit(limit).map do |group| | |
76 | + { | |
77 | + label: "group: #{search_result_sanitize(group.name)}", | |
78 | + url: group_path(group) | |
79 | + } | |
71 | 80 | end |
72 | 81 | end |
73 | 82 | |
74 | 83 | # Autocomplete results for the current user's projects |
75 | - def projects_autocomplete | |
76 | - current_user.authorized_projects.non_archived.map do |p| | |
77 | - { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) } | |
84 | + def projects_autocomplete(term, limit = 5) | |
85 | + current_user.authorized_projects.search_by_title(term).non_archived.limit(limit).map do |p| | |
86 | + { | |
87 | + label: "project: #{search_result_sanitize(p.name_with_namespace)}", | |
88 | + url: project_path(p) | |
89 | + } | |
78 | 90 | end |
79 | 91 | end |
80 | 92 | |
81 | 93 | # Autocomplete results for the current user's projects |
82 | - def public_projects_autocomplete | |
83 | - Project.public_or_internal_only(current_user).non_archived.map do |p| | |
84 | - { label: "project: #{simple_sanitize(p.name_with_namespace)}", url: project_path(p) } | |
94 | + def public_projects_autocomplete(term, limit = 5) | |
95 | + Project.public_or_internal_only(current_user).search_by_title(term).non_archived.limit(limit).map do |p| | |
96 | + { | |
97 | + label: "project: #{search_result_sanitize(p.name_with_namespace)}", | |
98 | + url: project_path(p) | |
99 | + } | |
85 | 100 | end |
86 | 101 | end |
102 | + | |
103 | + def search_result_sanitize(str) | |
104 | + Sanitize.clean(str) | |
105 | + end | |
87 | 106 | end | ... | ... |
app/models/project.rb
... | ... | @@ -138,6 +138,10 @@ class Project < ActiveRecord::Base |
138 | 138 | 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}%") |
139 | 139 | end |
140 | 140 | |
141 | + def search_by_title query | |
142 | + where("projects.archived = ?", false).where("LOWER(projects.name) LIKE :query", query: "%#{query.downcase}%") | |
143 | + end | |
144 | + | |
141 | 145 | def find_with_namespace(id) |
142 | 146 | if id.include?("/") |
143 | 147 | id = id.split("/") | ... | ... |
app/views/layouts/_search.html.haml
... | ... | @@ -7,4 +7,4 @@ |
7 | 7 | = hidden_field_tag :search_code, true |
8 | 8 | = hidden_field_tag :repository_ref, @ref |
9 | 9 | = submit_tag 'Go' if ENV['RAILS_ENV'] == 'test' |
10 | - .search-autocomplete-json.hide{:'data-autocomplete-opts' => search_autocomplete_source } | |
10 | + .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } | ... | ... |
config/routes.rb
spec/helpers/search_helper_spec.rb
... | ... | @@ -13,52 +13,41 @@ describe SearchHelper do |
13 | 13 | end |
14 | 14 | |
15 | 15 | it "it returns nil" do |
16 | - search_autocomplete_source.should be_nil | |
16 | + search_autocomplete_opts("q").should be_nil | |
17 | 17 | end |
18 | 18 | end |
19 | 19 | |
20 | 20 | context "with a user" do |
21 | 21 | let(:user) { create(:user) } |
22 | - let(:result) { JSON.parse(search_autocomplete_source) } | |
23 | 22 | |
24 | 23 | before do |
25 | 24 | allow(self).to receive(:current_user).and_return(user) |
26 | 25 | end |
27 | 26 | |
28 | 27 | it "includes Help sections" do |
29 | - result.select { |h| h['label'] =~ /^help:/ }.length.should == 9 | |
28 | + search_autocomplete_opts("hel").size.should == 9 | |
30 | 29 | end |
31 | 30 | |
32 | 31 | it "includes default sections" do |
33 | - result.count { |h| h['label'] =~ /^(My|Admin)\s/ }.should == 4 | |
32 | + search_autocomplete_opts("adm").size.should == 1 | |
34 | 33 | end |
35 | 34 | |
36 | 35 | it "includes the user's groups" do |
37 | 36 | create(:group).add_owner(user) |
38 | - result.count { |h| h['label'] =~ /^group:/ }.should == 1 | |
37 | + search_autocomplete_opts("gro").size.should == 1 | |
39 | 38 | end |
40 | 39 | |
41 | 40 | it "includes the user's projects" do |
42 | - create(:project, namespace: create(:namespace, owner: user)) | |
43 | - result.count { |h| h['label'] =~ /^project:/ }.should == 1 | |
41 | + project = create(:project, namespace: create(:namespace, owner: user)) | |
42 | + search_autocomplete_opts(project.name).size.should == 1 | |
44 | 43 | end |
45 | 44 | |
46 | 45 | context "with a current project" do |
47 | 46 | before { @project = create(:project_with_code) } |
48 | 47 | |
49 | 48 | it "includes project-specific sections" do |
50 | - result.count { |h| h['label'] =~ /^#{@project.name_with_namespace} - / }.should == 11 | |
51 | - end | |
52 | - | |
53 | - it "uses @ref in urls if defined" do | |
54 | - @ref = "foo_bar" | |
55 | - result.count { |h| h['url'] == project_tree_path(@project, @ref) }.should == 1 | |
56 | - end | |
57 | - end | |
58 | - | |
59 | - context "with no current project" do | |
60 | - it "does not include project-specific sections" do | |
61 | - result.count { |h| h['label'] =~ /Files$/ }.should == 0 | |
49 | + search_autocomplete_opts("Files").size.should == 1 | |
50 | + search_autocomplete_opts("Commits").size.should == 1 | |
62 | 51 | end |
63 | 52 | end |
64 | 53 | end | ... | ... |