Merge branch 'search-ui-update' into 'master'
Search ui update Closes #2537 See merge request !3751
This commit is contained in:
commit
31c10e0362
16 changed files with 265 additions and 149 deletions
|
@ -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'
|
||||
|
|
75
app/assets/javascripts/search.js.coffee
Normal file
75
app/assets/javascripts/search.js.coffee
Normal 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()
|
|
@ -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
|
||||
|
|
|
@ -1,47 +1,33 @@
|
|||
.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
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
= 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"
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
- 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
|
||||
|
|
Loading…
Reference in a new issue