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 | ... | ... |