Merge branch 'search-ui-update' into 'master'

Search ui update

Closes #2537 

See merge request !3751
This commit is contained in:
Robert Speicher 2016-04-26 15:12:16 +00:00
commit 31c10e0362
16 changed files with 265 additions and 149 deletions

View file

@ -4,6 +4,7 @@ v 8.8.0 (unreleased)
- Remove future dates from contribution calendar graph. - Remove future dates from contribution calendar graph.
- Fix error when visiting commit builds page before build was updated - Fix error when visiting commit builds page before build was updated
- Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project
- Updated search UI
v 8.7.1 (unreleased) v 8.7.1 (unreleased)
- Throttle the update of `project.last_activity_at` to 1 minute. !3848 - Throttle the update of `project.last_activity_at` to 1 minute. !3848

View file

@ -108,6 +108,8 @@ class Dispatcher
new BuildArtifacts() new BuildArtifacts()
when 'projects:group_links:index' when 'projects:group_links:index'
new GroupsSelect() new GroupsSelect()
when 'search:show'
new Search()
switch path.first() switch path.first()
when 'admin' when 'admin'

View file

@ -0,0 +1,75 @@
class @Search
constructor: ->
$groupDropdown = $('.js-search-group-dropdown')
$projectDropdown = $('.js-search-project-dropdown')
@eventListeners()
$groupDropdown.glDropdown(
selectable: true
filterable: true
fieldName: 'group_id'
data: (term, callback) ->
Api.groups term, null, (data) ->
data.unshift(
name: 'Any'
)
data.splice 1, 0, 'divider'
callback(data)
id: (obj) ->
obj.id
text: (obj) ->
obj.name
toggleLabel: (obj) ->
"#{$groupDropdown.data('default-label')} #{obj.name}"
clicked: =>
@submitSearch()
)
$projectDropdown.glDropdown(
selectable: true
filterable: true
fieldName: 'project_id'
data: (term, callback) ->
Api.projects term, 'id', (data) ->
data.unshift(
name_with_namespace: 'Any'
)
data.splice 1, 0, 'divider'
callback(data)
id: (obj) ->
obj.id
text: (obj) ->
obj.name_with_namespace
toggleLabel: (obj) ->
"#{$projectDropdown.data('default-label')} #{obj.name_with_namespace}"
clicked: =>
@submitSearch()
)
eventListeners: ->
$(document)
.off 'keyup', '.js-search-input'
.on 'keyup', '.js-search-input', @searchKeyUp
$(document)
.off 'click', '.js-search-clear'
.on 'click', '.js-search-clear', @clearSearchField
submitSearch: ->
$('.js-search-form').submit()
searchKeyUp: ->
$input = $(@)
if $input.val() is ''
$('.js-search-clear').addClass 'hidden'
else
$('.js-search-clear').removeClass 'hidden'
clearSearchField: ->
$('.js-search-input')
.val ''
.trigger 'keyup'
.focus()

View file

@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px } .prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px } .prepend-top-20 { margin-top: 20px }
.prepend-left-5 { margin-left: 5px }
.prepend-left-10 { margin-left: 10px } .prepend-left-10 { margin-left: 10px }
.prepend-left-default { margin-left: $gl-padding; } .prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px } .prepend-left-20 { margin-left: 20px }

View file

@ -42,7 +42,7 @@
font-size: 15px; font-size: 15px;
text-align: left; text-align: left;
border: 1px solid $dropdown-toggle-border-color; border: 1px solid $dropdown-toggle-border-color;
border-radius: $dropdown-border-radius; border-radius: $border-radius-base;
outline: 0; outline: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -80,7 +80,7 @@
padding: 10px 0; padding: 10px 0;
background-color: $dropdown-bg; background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $dropdown-border-radius; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading { &.is-loading {

View file

@ -183,7 +183,6 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
/* /*
* Dropdowns * Dropdowns
*/ */
$dropdown-border-radius: 2px;
$dropdown-width: 300px; $dropdown-width: 300px;
$dropdown-bg: #fff; $dropdown-bg: #fff;
$dropdown-link-color: #555; $dropdown-link-color: #555;

View file

@ -10,17 +10,6 @@
} }
} }
.search-holder {
max-width: 600px;
margin: 0 auto;
margin-bottom: 20px;
input {
border-color: #bbb;
font-weight: bold;
}
}
.search { .search {
margin-right: 10px; margin-right: 10px;
margin-left: 10px; margin-left: 10px;
@ -163,3 +152,81 @@
} }
} }
} }
.search-holder {
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.search-field-holder {
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
margin-right: 0;
@media (min-width: $screen-sm-min) {
margin-right: 5px;
}
}
.search-icon {
position: absolute;
left: 10px;
top: 10px;
color: $gray-darkest;
pointer-events: none;
}
.search-text-input {
padding-left: $gl-padding + 15px;
padding-right: $gl-padding + 15px;
}
.btn-search {
width: 100%;
margin-top: 5px;
@media (min-width: $screen-sm-min) {
width: auto;
margin-top: 0;
margin-left: 5px;
}
}
.dropdown {
@media (min-width: $screen-sm-min) {
margin-left: 5px;
margin-right: 5px;
}
}
.dropdown-menu-toggle {
width: 100%;
margin-top: 5px;
@media (min-width: $screen-sm-min) {
width: 160px;
margin-top: 0;
}
}
}
.search-clear {
position: absolute;
right: 10px;
top: 10px;
padding: 0;
color: $gray-darkest;
line-height: 0;
background: none;
border: 0;
&:hover,
&:focus {
color: $gl-link-color;
outline: none;
}
}

View file

@ -8,8 +8,6 @@ class SearchController < ApplicationController
def show def show
return if params[:search].nil? || params[:search].blank? return if params[:search].nil? || params[:search].blank?
@search_term = params[:search]
if params[:project_id].present? if params[:project_id].present?
@project = Project.find_by(id: params[:project_id]) @project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project) @project = nil unless can?(current_user, :download_code, @project)
@ -20,6 +18,8 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group) @group = nil unless can?(current_user, :read_group, @group)
end end
@search_term = params[:search]
@scope = params[:scope] @scope = params[:scope]
@show_snippets = params[:snippets].eql? 'true' @show_snippets = params[:snippets].eql? 'true'
@ -44,7 +44,7 @@ class SearchController < ApplicationController
Search::GlobalService.new(current_user, params).execute Search::GlobalService.new(current_user, params).execute
end end
@objects = @search_results.objects(@scope, params[:page]) @search_objects = @search_results.objects(@scope, params[:page])
end end
def autocomplete def autocomplete

View file

@ -19,6 +19,16 @@ module SearchHelper
end end
end end
def search_entries_info(collection, scope, term)
return unless collection.count > 0
from = collection.offset_value + 1
to = collection.offset_value + collection.length
count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end
private private
# Autocomplete results for various settings pages # Autocomplete results for various settings pages

View file

@ -2,50 +2,36 @@
- if @project - if @project
%li{class: ("active" if @scope == 'blobs')} %li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do = link_to search_filter_path(scope: 'blobs') do
= icon('code fw')
%span
Code Code
%span.badge %span.badge
= @search_results.blobs_count = @search_results.blobs_count
%li{class: ("active" if @scope == 'issues')} %li{class: ("active" if @scope == 'issues')}
= link_to search_filter_path(scope: 'issues') do = link_to search_filter_path(scope: 'issues') do
= icon('exclamation-circle fw')
%span
Issues Issues
%span.badge %span.badge
= @search_results.issues_count = @search_results.issues_count
%li{class: ("active" if @scope == 'merge_requests')} %li{class: ("active" if @scope == 'merge_requests')}
= link_to search_filter_path(scope: 'merge_requests') do = link_to search_filter_path(scope: 'merge_requests') do
= icon('tasks fw')
%span
Merge requests Merge requests
%span.badge %span.badge
= @search_results.merge_requests_count = @search_results.merge_requests_count
%li{class: ("active" if @scope == 'milestones')} %li{class: ("active" if @scope == 'milestones')}
= link_to search_filter_path(scope: 'milestones') do = link_to search_filter_path(scope: 'milestones') do
= icon('clock-o fw')
%span
Milestones Milestones
%span.badge %span.badge
= @search_results.milestones_count = @search_results.milestones_count
%li{class: ("active" if @scope == 'notes')} %li{class: ("active" if @scope == 'notes')}
= link_to search_filter_path(scope: 'notes') do = link_to search_filter_path(scope: 'notes') do
= icon('comments fw')
%span
Comments Comments
%span.badge %span.badge
= @search_results.notes_count = @search_results.notes_count
%li{class: ("active" if @scope == 'wiki_blobs')} %li{class: ("active" if @scope == 'wiki_blobs')}
= link_to search_filter_path(scope: 'wiki_blobs') do = link_to search_filter_path(scope: 'wiki_blobs') do
= icon('book fw')
%span
Wiki Wiki
%span.badge %span.badge
= @search_results.wiki_blobs_count = @search_results.wiki_blobs_count
%li{class: ("active" if @scope == 'commits')} %li{class: ("active" if @scope == 'commits')}
= link_to search_filter_path(scope: 'commits') do = link_to search_filter_path(scope: 'commits') do
= icon('history fw')
%span
Commits Commits
%span.badge %span.badge
= @search_results.commits_count = @search_results.commits_count
@ -53,15 +39,11 @@
- elsif @show_snippets - elsif @show_snippets
%li{class: ("active" if @scope == 'snippet_blobs')} %li{class: ("active" if @scope == 'snippet_blobs')}
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
= icon('code fw')
%span
Snippet Contents Snippet Contents
%span.badge %span.badge
= @search_results.snippet_blobs_count = @search_results.snippet_blobs_count
%li{class: ("active" if @scope == 'snippet_titles')} %li{class: ("active" if @scope == 'snippet_titles')}
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
= icon('book fw')
%span
Titles and Filenames Titles and Filenames
%span.badge %span.badge
= @search_results.snippet_titles_count = @search_results.snippet_titles_count
@ -69,30 +51,21 @@
- else - else
%li{class: ("active" if @scope == 'projects')} %li{class: ("active" if @scope == 'projects')}
= link_to search_filter_path(scope: 'projects') do = link_to search_filter_path(scope: 'projects') do
= icon('bookmark fw')
%span
Projects Projects
%span.badge %span.badge
= @search_results.projects_count = @search_results.projects_count
%li{class: ("active" if @scope == 'issues')} %li{class: ("active" if @scope == 'issues')}
= link_to search_filter_path(scope: 'issues') do = link_to search_filter_path(scope: 'issues') do
= icon('exclamation-circle fw')
%span
Issues Issues
%span.badge %span.badge
= @search_results.issues_count = @search_results.issues_count
%li{class: ("active" if @scope == 'merge_requests')} %li{class: ("active" if @scope == 'merge_requests')}
= link_to search_filter_path(scope: 'merge_requests') do = link_to search_filter_path(scope: 'merge_requests') do
= icon('tasks fw')
%span
Merge requests Merge requests
%span.badge %span.badge
= @search_results.merge_requests_count = @search_results.merge_requests_count
%li{class: ("active" if @scope == 'milestones')} %li{class: ("active" if @scope == 'milestones')}
= link_to search_filter_path(scope: 'milestones') do = link_to search_filter_path(scope: 'milestones') do
= icon('clock-o fw')
%span
Milestones Milestones
%span.badge %span.badge
= @search_results.milestones_count = @search_results.milestones_count

View file

@ -1,47 +1,33 @@
.dropdown.inline - if params[:group_id].present?
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} = hidden_field_tag :group_id, params[:group_id]
%span.light Group: - if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
.dropdown
%button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } }
%span.dropdown-toggle-text
Group:
- if @group.present? - if @group.present?
%strong= @group.name = @group.name
- else - else
Any Any
%b.caret = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
.dropdown-title = dropdown_title("Filter results by group")
%span Filter results by group = dropdown_filter("Search groups")
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} = dropdown_content
= icon('times') = dropdown_loading
.dropdown-content
%ul
%li
= link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do
Any
%li.divider
- current_user.authorized_groups.sort_by(&:name).each do |group|
%li
= link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do
= group.name
.dropdown.inline.prepend-left-10.project-filter .dropdown.project-filter
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
%span.light Project: %span.dropdown-toggle-text
Project:
- if @project.present? - if @project.present?
%strong= @project.name_with_namespace = @project.name_with_namespace
- else - else
Any Any
%b.caret = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
.dropdown-title = dropdown_title("Filter results by project")
%span Filter results by project = dropdown_filter("Search projects")
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} = dropdown_content
= icon('times') = dropdown_loading
.dropdown-content
%ul
%li
= link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do
Any
%li.divider
- current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project|
%li
= link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do
= project.name_with_namespace

View file

@ -1,14 +1,15 @@
= form_tag search_path, method: :get do |f| = form_tag search_path, method: :get, class: 'js-search-form' do |f|
= hidden_field_tag :project_id, params[:project_id]
= hidden_field_tag :group_id, params[:group_id]
= hidden_field_tag :snippets, params[:snippets] = hidden_field_tag :snippets, params[:snippets]
= hidden_field_tag :scope, params[:scope] = hidden_field_tag :scope, params[:scope]
.search-holder.clearfix .search-holder
.input-group .search-field-holder
= search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true, spellcheck: false = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false
%span.input-group-btn = icon("search", class: "search-icon")
= button_tag 'Search', class: "btn btn-primary" %button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" }
= icon("times-circle")
%span.sr-only
Clear search
- unless params[:snippets].eql? 'true' - unless params[:snippets].eql? 'true'
%br
= render 'filter' if current_user = render 'filter' if current_user
= button_tag "Search", class: "btn btn-success btn-search"

View file

@ -1,10 +1,8 @@
- if @search_results.empty? - if @search_objects.empty?
= render partial: "search/results/empty" = render partial: "search/results/empty"
- else - else
.gray-content-block .gray-content-block
Search results for = search_entries_info(@search_objects, @scope, @search_term)
%code
= @search_term
- unless @show_snippets - unless @show_snippets
- if @project - if @project
in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
@ -15,12 +13,9 @@
.search-results .search-results
- if @scope == 'projects' - if @scope == 'projects'
.term .term
= render 'shared/projects/list', projects: @objects = render 'shared/projects/list', projects: @search_objects
- else - else
= render partial: "search/results/#{@scope.singularize}", collection: @objects = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
- if @scope != 'projects' - if @scope != 'projects'
= paginate @objects, theme: 'gitlab' = paginate(@search_objects, theme: 'gitlab')
:javascript
$(".search-results .term").highlight("#{escape_javascript(params[:search])}");

View file

@ -7,7 +7,7 @@
- if issue.description.present? - if issue.description.present?
.description.term .description.term
= preserve do = preserve do
= search_md_sanitize(markdown(issue.description, { project: issue.project })) = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project }))
%span.light %span.light
#{issue.project.name_with_namespace} #{issue.project.name_with_namespace}
- if issue.closed? - if issue.closed?

View file

@ -30,11 +30,13 @@ Feature: Search
Then I should see "Foo" link in the search results Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results And I should not see "Bar" link in the search results
@javascript
Scenario: I should see project code I am looking for Scenario: I should see project code I am looking for
When I click project "Shop" link When I click project "Shop" link
And I search for "rspec" And I search for "rspec"
Then I should see code results for project "Shop" Then I should see code results for project "Shop"
@javascript
Scenario: I should see project issues Scenario: I should see project issues
And project has issues And project has issues
When I click project "Shop" link When I click project "Shop" link
@ -43,6 +45,7 @@ Feature: Search
Then I should see "Foo" link in the search results Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results And I should not see "Bar" link in the search results
@javascript
Scenario: I should see project merge requests Scenario: I should see project merge requests
And project has merge requests And project has merge requests
When I click project "Shop" link When I click project "Shop" link
@ -51,6 +54,7 @@ Feature: Search
Then I should see "Foo" link in the search results Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results And I should not see "Bar" link in the search results
@javascript
Scenario: I should see project milestones Scenario: I should see project milestones
And project has milestones And project has milestones
When I click project "Shop" link When I click project "Shop" link
@ -59,6 +63,7 @@ Feature: Search
Then I should see "Foo" link in the search results Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results And I should not see "Bar" link in the search results
@javascript
Scenario: I should see Wiki blobs Scenario: I should see Wiki blobs
And project has Wiki content And project has Wiki content
When I click project "Shop" link When I click project "Shop" link

View file

@ -35,6 +35,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps
end end
step 'I click project "Shop" link' do step 'I click project "Shop" link' do
click_button 'Project'
page.within '.project-filter' do page.within '.project-filter' do
click_link project.name_with_namespace click_link project.name_with_namespace
end end