Commit 31c10e03 authored by Robert Speicher's avatar Robert Speicher

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

Search ui update

Closes #2537 

See merge request !3751
parents 110bb651 97c1a1ef
......@@ -4,6 +4,7 @@ v 8.8.0 (unreleased)
- Remove future dates from contribution calendar graph.
- 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
- Updated search UI
v 8.7.1 (unreleased)
- Throttle the update of `project.last_activity_at` to 1 minute. !3848
......
......@@ -108,6 +108,8 @@ class Dispatcher
new BuildArtifacts()
when 'projects:group_links:index'
new GroupsSelect()
when 'search:show'
new Search()
switch path.first()
when 'admin'
......
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()
......@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px }
.prepend-left-5 { margin-left: 5px }
.prepend-left-10 { margin-left: 10px }
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px }
......
......@@ -42,7 +42,7 @@
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border-radius: $dropdown-border-radius;
border-radius: $border-radius-base;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
......@@ -80,7 +80,7 @@
padding: 10px 0;
background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color;
border-radius: $dropdown-border-radius;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
......
......@@ -183,7 +183,6 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
/*
* Dropdowns
*/
$dropdown-border-radius: 2px;
$dropdown-width: 300px;
$dropdown-bg: #fff;
$dropdown-link-color: #555;
......
......@@ -10,17 +10,6 @@
}
}
.search-holder {
max-width: 600px;
margin: 0 auto;
margin-bottom: 20px;
input {
border-color: #bbb;
font-weight: bold;
}
}
.search {
margin-right: 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;
}
}
......@@ -8,8 +8,6 @@ class SearchController < ApplicationController
def show
return if params[:search].nil? || params[:search].blank?
@search_term = params[:search]
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@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)
end
@search_term = params[:search]
@scope = params[:scope]
@show_snippets = params[:snippets].eql? 'true'
......@@ -44,7 +44,7 @@ class SearchController < ApplicationController
Search::GlobalService.new(current_user, params).execute
end
@objects = @search_results.objects(@scope, params[:page])
@search_objects = @search_results.objects(@scope, params[:page])
end
def autocomplete
......
......@@ -19,6 +19,16 @@ module SearchHelper
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
# Autocomplete results for various settings pages
......
......@@ -2,97 +2,70 @@
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
= icon('code fw')
%span
Code
%span.badge
= @search_results.blobs_count
Code
%span.badge
= @search_results.blobs_count
%li{class: ("active" if @scope == 'issues')}
= link_to search_filter_path(scope: 'issues') do
= icon('exclamation-circle fw')
%span
Issues
%span.badge
= @search_results.issues_count
Issues
%span.badge
= @search_results.issues_count
%li{class: ("active" if @scope == 'merge_requests')}
= link_to search_filter_path(scope: 'merge_requests') do
= icon('tasks fw')
%span
Merge requests
%span.badge
= @search_results.merge_requests_count
Merge requests
%span.badge
= @search_results.merge_requests_count
%li{class: ("active" if @scope == 'milestones')}
= link_to search_filter_path(scope: 'milestones') do
= icon('clock-o fw')
%span
Milestones
%span.badge
= @search_results.milestones_count
Milestones
%span.badge
= @search_results.milestones_count
%li{class: ("active" if @scope == 'notes')}
= link_to search_filter_path(scope: 'notes') do
= icon('comments fw')
%span
Comments
%span.badge
= @search_results.notes_count
Comments
%span.badge
= @search_results.notes_count
%li{class: ("active" if @scope == 'wiki_blobs')}
= link_to search_filter_path(scope: 'wiki_blobs') do
= icon('book fw')
%span
Wiki
%span.badge
= @search_results.wiki_blobs_count
Wiki
%span.badge
= @search_results.wiki_blobs_count
%li{class: ("active" if @scope == 'commits')}
= link_to search_filter_path(scope: 'commits') do
= icon('history fw')
%span
Commits
%span.badge
= @search_results.commits_count
Commits
%span.badge
= @search_results.commits_count
- elsif @show_snippets
%li{class: ("active" if @scope == 'snippet_blobs')}
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
= icon('code fw')
%span
Snippet Contents
%span.badge
= @search_results.snippet_blobs_count
Snippet Contents
%span.badge
= @search_results.snippet_blobs_count
%li{class: ("active" if @scope == 'snippet_titles')}
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
= icon('book fw')
%span
Titles and Filenames
%span.badge
= @search_results.snippet_titles_count
Titles and Filenames
%span.badge
= @search_results.snippet_titles_count
- else
%li{class: ("active" if @scope == 'projects')}
= link_to search_filter_path(scope: 'projects') do
= icon('bookmark fw')
%span
Projects
%span.badge
= @search_results.projects_count
Projects
%span.badge
= @search_results.projects_count
%li{class: ("active" if @scope == 'issues')}
= link_to search_filter_path(scope: 'issues') do
= icon('exclamation-circle fw')
%span
Issues
%span.badge
= @search_results.issues_count
Issues
%span.badge
= @search_results.issues_count
%li{class: ("active" if @scope == 'merge_requests')}
= link_to search_filter_path(scope: 'merge_requests') do
= icon('tasks fw')
%span
Merge requests
%span.badge
= @search_results.merge_requests_count
Merge requests
%span.badge
= @search_results.merge_requests_count
%li{class: ("active" if @scope == 'milestones')}
= link_to search_filter_path(scope: 'milestones') do
= icon('clock-o fw')
%span
Milestones
%span.badge
= @search_results.milestones_count
Milestones
%span.badge
= @search_results.milestones_count
.dropdown.inline
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
%span.light Group:
- if @group.present?
%strong= @group.name
- else
Any
%b.caret
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Filter results by group
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.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
- if params[:group_id].present?
= hidden_field_tag :group_id, params[:group_id]
- 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?
= @group.name
- else
Any
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
= dropdown_title("Filter results by group")
= dropdown_filter("Search groups")
= dropdown_content
= dropdown_loading
.dropdown.inline.prepend-left-10.project-filter
%button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
%span.light Project:
- if @project.present?
%strong= @project.name_with_namespace
- else
Any
%b.caret
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Filter results by project
%button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
= icon('times')
.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
.dropdown.project-filter
%button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } }
%span.dropdown-toggle-text
Project:
- if @project.present?
= @project.name_with_namespace
- else
Any
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
= dropdown_title("Filter results by project")
= dropdown_filter("Search projects")
= dropdown_content
= dropdown_loading
= form_tag search_path, method: :get do |f|
= hidden_field_tag :project_id, params[:project_id]
= hidden_field_tag :group_id, params[:group_id]
= form_tag search_path, method: :get, class: 'js-search-form' do |f|
= hidden_field_tag :snippets, params[:snippets]
= hidden_field_tag :scope, params[:scope]
.search-holder.clearfix
.input-group
= 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
%span.input-group-btn
= button_tag 'Search', class: "btn btn-primary"
.search-holder
.search-field-holder
= 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
= icon("search", class: "search-icon")
%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'
%br
= render 'filter' if current_user
= button_tag "Search", class: "btn btn-success btn-search"
- if @search_results.empty?
- if @search_objects.empty?
= render partial: "search/results/empty"
- else
.gray-content-block
Search results for
%code
= @search_term
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
......@@ -15,12 +13,9 @@
.search-results
- if @scope == 'projects'
.term
= render 'shared/projects/list', projects: @objects
= render 'shared/projects/list', projects: @search_objects
- else
= render partial: "search/results/#{@scope.singularize}", collection: @objects
= render partial: "search/results/#{@scope.singularize}", collection: @search_objects
- if @scope != 'projects'
= paginate @objects, theme: 'gitlab'
:javascript
$(".search-results .term").highlight("#{escape_javascript(params[:search])}");
= paginate(@search_objects, theme: 'gitlab')
......@@ -7,7 +7,7 @@
- if issue.description.present?
.description.term
= 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
#{issue.project.name_with_namespace}
- if issue.closed?
......
......@@ -30,11 +30,13 @@ Feature: Search
Then I should see "Foo" 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
When I click project "Shop" link
And I search for "rspec"
Then I should see code results for project "Shop"
@javascript
Scenario: I should see project issues
And project has issues
When I click project "Shop" link
......@@ -43,6 +45,7 @@ Feature: Search
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
@javascript
Scenario: I should see project merge requests
And project has merge requests
When I click project "Shop" link
......@@ -51,6 +54,7 @@ Feature: Search
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
@javascript
Scenario: I should see project milestones
And project has milestones
When I click project "Shop" link
......@@ -59,6 +63,7 @@ Feature: Search
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
@javascript
Scenario: I should see Wiki blobs
And project has Wiki content
When I click project "Shop" link
......
......@@ -35,6 +35,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps
end
step 'I click project "Shop" link' do
click_button 'Project'
page.within '.project-filter' do
click_link project.name_with_namespace
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