summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee8
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee103
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee279
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss60
-rw-r--r--app/assets/stylesheets/framework/forms.scss34
-rw-r--r--app/assets/stylesheets/framework/header.scss20
-rw-r--r--app/assets/stylesheets/framework/variables.scss18
-rw-r--r--app/assets/stylesheets/pages/search.scss142
-rw-r--r--app/helpers/search_helper.rb57
-rw-r--r--app/views/layouts/_search.html.haml40
-rw-r--r--features/steps/dashboard/issues.rb9
11 files changed, 613 insertions, 157 deletions
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index f5e1ca9860d..70fd6f50e9c 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -146,15 +146,11 @@ class Dispatcher
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
-
# If we haven't installed a custom shortcut handler, install the default one
if not shortcut_handler
new Shortcuts()
initSearch: ->
- opts = $('.search-autocomplete-opts')
- path = opts.data('autocomplete-path')
- project_id = opts.data('autocomplete-project-id')
- project_ref = opts.data('autocomplete-project-ref')
- new SearchAutocomplete(path, project_id, project_ref)
+ # Only when search form is present
+ new SearchAutocomplete() if $('.search').length
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 2b56ab2e6de..85dcd493e53 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -3,6 +3,10 @@ class GitLabDropdownFilter
HAS_VALUE_CLASS = "has-value"
constructor: (@input, @options) ->
+ {
+ @filterInputBlur = true
+ } = @options
+
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
@@ -33,7 +37,7 @@ class GitLabDropdownFilter
blur_field = @shouldBlur e.keyCode
search_text = @input.val()
- if blur_field
+ if blur_field and @filterInputBlur
@input.blur()
if @options.remote
@@ -93,27 +97,48 @@ class GitLabDropdown
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field'
+
constructor: (@el, @options) ->
- self = @
@dropdown = $(@el).parent()
+
+ # Set Defaults
+ {
+ # If no input is passed create a default one
+ @filterInput = @getElement(FILTER_INPUT)
+ @highlight = false
+ @filterInputBlur = true
+ @enterCallback = true
+ } = @options
+
+ self = @
+
+ # If selector was passed
+ if _.isString(@filterInput)
+ @filterInput = @getElement(@filterInput)
+
search_fields = if @options.search then @options.search.fields else [];
if @options.data
- # Remote data
- @remote = new GitLabDropdownRemote @options.data, {
- dataType: @options.dataType,
- beforeSend: @toggleLoading.bind(@)
- success: (data) =>
- @fullData = data
+ # If data is an array
+ if _.isArray @options.data
+ @fullData = @options.data
+ @parseData @options.data
+ else
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
- @parseData @fullData
- }
+ @parseData @fullData
+ }
- # Init filiterable
+ # Init filterable
if @options.filterable
- @input = @dropdown.find('.dropdown-input .dropdown-input-field')
-
- @filter = new GitLabDropdownFilter @input,
+ @filter = new GitLabDropdownFilter @filterInput,
+ filterInputBlur: @filterInputBlur
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
@@ -123,7 +148,8 @@ class GitLabDropdown
@parseData data
@highlightRow 1
enterCallback: =>
- @selectFirstRow()
+ if @enterCallback
+ @selectFirstRow()
# Event listeners
@@ -151,6 +177,10 @@ class GitLabDropdown
if self.options.clicked
self.options.clicked.call(@,e)
+ # Finds an element inside wrapper element
+ getElement: (selector) ->
+ @dropdown.find selector
+
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
@@ -194,7 +224,7 @@ class GitLabDropdown
@remote.execute()
if @options.filterable
- @dropdown.find(".dropdown-input-field").focus()
+ @filterInput.focus()
hidden: (e) =>
if @options.filterable
@@ -234,13 +264,19 @@ class GitLabDropdown
renderItem: (data) ->
html = ""
+ # Divider
return "<li class='divider'></li>" if data is "divider"
+ # Separator is a full-width divider
+ return "<li class='separator'></li>" if data is "separator"
+
+ # Header
+ return "<li class='dropdown-header'>#{data.header}</li>" if data.header?
+
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
- selected = if @options.isSelected then @options.isSelected(data) else false
if not selected
value = if @options.id then @options.id(data) else data.id
fieldName = @options.fieldName
@@ -248,13 +284,26 @@ class GitLabDropdown
if field.length
selected = true
- url = if @options.url then @options.url(data) else "#"
- text = if @options.text then @options.text(data) else ""
+ # Set URL
+ if @options.url?
+ url = @options.url(data)
+ else
+ url = if data.url? then data.url else '#'
+
+ # Set Text
+ if @options.text?
+ text = @options.text(data)
+ else
+ text = if data.text? then data.text else ''
+
cssClass = "";
if selected
cssClass = "is-active"
+ if @highlight
+ text = @highlightTextMatches(text, @filterInput.val())
+
html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>"
html += text
@@ -263,20 +312,26 @@ class GitLabDropdown
return html
+ highlightTextMatches: (text, term) ->
+ occurrences = fuzzaldrinPlus.match(text, term)
+ text.split('').map((character, i) ->
+ if i in occurrences then "<b>#{character}</b>" else character
+ ).join('')
+
noResults: ->
html = "<li>"
- html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
+ html += "<a class='dropdown-menu-empty-link is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
highlightRow: (index) ->
- if @input.val() isnt ""
+ if @filterInput.val() isnt ""
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
- $(selector).addClass 'is-focused'
+ @getElement(selector).addClass 'is-focused'
rowClicked: (el) ->
fieldName = @options.fieldName
@@ -328,4 +383,6 @@ class GitLabDropdown
$.fn.glDropdown = (opts) ->
return @.each ->
- new GitLabDropdown @, opts
+ if (!$.data @, 'glDropdown')
+ $.data(@, 'glDropdown', new GitLabDropdown @, opts)
+
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index c1801365266..030655491bf 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -1,11 +1,270 @@
class @SearchAutocomplete
- constructor: (search_autocomplete_path, project_id, project_ref) ->
- project_id = '' unless project_id
- project_ref = '' unless project_ref
- query = "?project_id=" + project_id + "&project_ref=" + project_ref
-
- $("#search").autocomplete
- source: search_autocomplete_path + query
- minLength: 1
- select: (event, ui) ->
- location.href = ui.item.url
+
+ KEYCODE =
+ ESCAPE: 27
+ BACKSPACE: 8
+ ENTER: 13
+
+ constructor: (opts = {}) ->
+ {
+ @wrap = $('.search')
+
+ @optsEl = @wrap.find('.search-autocomplete-opts')
+ @autocompletePath = @optsEl.data('autocomplete-path')
+ @projectId = @optsEl.data('autocomplete-project-id') || ''
+ @projectRef = @optsEl.data('autocomplete-project-ref') || ''
+
+ } = opts
+
+ # Dropdown Element
+ @dropdown = @wrap.find('.dropdown')
+ @dropdownContent = @dropdown.find('.dropdown-content')
+
+ @locationBadgeEl = @getElement('.search-location-badge')
+ @locationText = @getElement('.location-text')
+ @scopeInputEl = @getElement('#scope')
+ @searchInput = @getElement('.search-input')
+ @projectInputEl = @getElement('#search_project_id')
+ @groupInputEl = @getElement('#group_id')
+ @searchCodeInputEl = @getElement('#search_code')
+ @repositoryInputEl = @getElement('#repository_ref')
+ @clearInput = @getElement('.js-clear-input')
+
+ @saveOriginalState()
+
+ # Only when user is logged in
+ @createAutocomplete() if gon.current_user_id
+
+ @searchInput.addClass('disabled')
+
+ @saveTextLength()
+
+ @bindEvents()
+
+ # Finds an element inside wrapper element
+ getElement: (selector) ->
+ @wrap.find(selector)
+
+ saveOriginalState: ->
+ @originalState = @serializeState()
+
+ saveTextLength: ->
+ @lastTextLength = @searchInput.val().length
+
+ createAutocomplete: ->
+ @searchInput.glDropdown
+ filterInputBlur: false
+ filterable: true
+ filterRemote: true
+ highlight: true
+ enterCallback: false
+ filterInput: 'input#search'
+ search:
+ fields: ['text']
+ data: @getData.bind(@)
+
+ getData: (term, callback) ->
+ _this = @
+
+ # Do not trigger request if input is empty
+ return if @searchInput.val() is ''
+
+ # Prevent multiple ajax calls
+ return if @loadingSuggestions
+
+ @loadingSuggestions = true
+
+ jqXHR = $.get(@autocompletePath, {
+ project_id: @projectId
+ project_ref: @projectRef
+ term: term
+ }, (response) ->
+ # Hide dropdown menu if no suggestions returns
+ if !response.length
+ _this.disableAutocomplete()
+ return
+
+ data = []
+
+ # List results
+ firstCategory = true
+ for suggestion in response
+
+ # Add group header before list each group
+ if lastCategory isnt suggestion.category
+ data.push 'separator' if !firstCategory
+
+ firstCategory = false if firstCategory
+
+ data.push
+ header: suggestion.category
+
+ lastCategory = suggestion.category
+
+ data.push
+ text: suggestion.label
+ url: suggestion.url
+
+ # Add option to proceed with the search
+ if data.length
+ data.push('separator')
+ data.push
+ text: "Result name contains \"#{term}\""
+ url: "/search?\
+ search=#{term}\
+ &project_id=#{_this.projectInputEl.val()}\
+ &group_id=#{_this.groupInputEl.val()}"
+
+ callback(data)
+ ).always ->
+ _this.loadingSuggestions = false
+
+ serializeState: ->
+ {
+ # Search Criteria
+ search_project_id: @projectInputEl.val()
+ group_id: @groupInputEl.val()
+ search_code: @searchCodeInputEl.val()
+ repository_ref: @repositoryInputEl.val()
+ scope: @scopeInputEl.val()
+
+ # Location badge
+ _location: @locationText.text()
+ }
+
+ bindEvents: ->
+ @searchInput.on 'keydown', @onSearchInputKeyDown
+ @searchInput.on 'keyup', @onSearchInputKeyUp
+ @searchInput.on 'click', @onSearchInputClick
+ @searchInput.on 'focus', @onSearchInputFocus
+ @searchInput.on 'blur', @onSearchInputBlur
+ @clearInput.on 'click', @onRemoveLocationClick
+
+ enableAutocomplete: ->
+ # No need to enable anything if user is not logged in
+ return if !gon.current_user_id
+
+ _this = @
+ @loadingSuggestions = false
+
+ @dropdown.addClass('open')
+ @searchInput.removeClass('disabled')
+
+ onSearchInputKeyDown: =>
+ # Saves last length of the entered text
+ @saveTextLength()
+
+ onSearchInputKeyUp: (e) =>
+ switch e.keyCode
+ when KEYCODE.BACKSPACE
+ # when trying to remove the location badge
+ if @lastTextLength is 0 and @badgePresent()
+ @removeLocationBadge()
+
+ # When removing the last character and no badge is present
+ if @lastTextLength is 1
+ @disableAutocomplete()
+
+ # When removing any character from existin value
+ if @lastTextLength > 1
+ @enableAutocomplete()
+
+ when KEYCODE.ESCAPE
+ @restoreOriginalState()
+
+ else
+ # Handle the case when deleting the input value other than backspace
+ # e.g. Pressing ctrl + backspace or ctrl + x
+ if @searchInput.val() is ''
+ @disableAutocomplete()
+ else
+ # We should display the menu only when input is not empty
+ @enableAutocomplete()
+
+ # Avoid falsy value to be returned
+ return
+
+ onSearchInputClick: (e) =>
+ # Prevents closing the dropdown menu
+ e.stopImmediatePropagation()
+
+ onSearchInputFocus: =>
+ @wrap.addClass('search-active')
+
+ onRemoveLocationClick: (e) =>
+ e.preventDefault()
+ @removeLocationBadge()
+ @searchInput.val('').focus()
+ @skipBlurEvent = true
+
+ onSearchInputBlur: (e) =>
+ @skipBlurEvent = false
+
+ # We should wait to make sure we are not clearing the input instead
+ setTimeout( =>
+ return if @skipBlurEvent
+
+ @wrap.removeClass('search-active')
+
+ # If input is blank then restore state
+ if @searchInput.val() is ''
+ @restoreOriginalState()
+ , 150)
+
+ addLocationBadge: (item) ->
+ category = if item.category? then "#{item.category}: " else ''
+ value = if item.value? then item.value else ''
+
+ html = "<span class='location-badge'>
+ <i class='location-text'>#{category}#{value}</i>
+ </span>"
+ @locationBadgeEl.html(html)
+ @wrap.addClass('has-location-badge')
+
+ restoreOriginalState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+ @getElement("##{input}").val(@originalState[input])
+
+
+ if @originalState._location is ''
+ @locationBadgeEl.empty()
+ else
+ @addLocationBadge(
+ value: @originalState._location
+ )
+
+ @dropdown.removeClass 'open'
+
+ badgePresent: ->
+ @locationBadgeEl.children().length
+
+ resetSearchState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+
+ # _location isnt a input
+ break if input is '_location'
+
+ @getElement("##{input}").val('')
+
+ removeLocationBadge: ->
+ @locationBadgeEl.empty()
+
+ # Reset state
+ @resetSearchState()
+
+ @wrap.removeClass('has-location-badge')
+
+ disableAutocomplete: ->
+ @searchInput.addClass('disabled')
+ @dropdown.removeClass('open')
+ @restoreMenu()
+
+ restoreMenu: ->
+ html = "<ul>
+ <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
+ </ul>"
+ @dropdownContent.html(html)
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 6c870ed927e..82dc1acbd01 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -42,7 +42,7 @@
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
- border-radius: 2px;
+ border-radius: $dropdown-border-radius;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
@@ -75,12 +75,12 @@
width: 240px;
margin-top: 2px;
margin-bottom: 0;
- padding: 10px;
- font-size: 14px;
+ font-size: 15px;
font-weight: normal;
+ padding: 10px 0;
background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color;
- border-radius: $border-radius-base;
+ border-radius: $dropdown-border-radius;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
@@ -101,9 +101,17 @@
li {
text-align: left;
list-style: none;
+ padding: 0 10px;
}
.divider {
+ height: 1px;
+ margin: 8px 10px;
+ padding: 0;
+ background-color: $dropdown-divider-color;
+ }
+
+ .separator {
width: 100%;
height: 1px;
margin-top: 8px;
@@ -141,6 +149,17 @@
line-height: 16px;
}
}
+
+ .dropdown-header {
+ color: $dropdown-header-color;
+ font-size: 13px;
+ line-height: 22px;
+ padding: 0 10px 10px;
+ }
+
+ .separator + .dropdown-header {
+ padding-top: 2px;
+ }
}
.dropdown-menu-paging {
@@ -158,6 +177,10 @@
.dropdown-menu-back {
display: block;
}
+
+ .dropdown-content {
+ padding: 0 10px;
+ }
}
}
@@ -193,7 +216,7 @@
}
.dropdown-select {
- width: 300px;
+ width: $dropdown-width;
}
.dropdown-menu-align-right {
@@ -222,20 +245,11 @@
}
}
-.dropdown-header {
- padding-left: 5px;
- padding-right: 5px;
- color: $dropdown-header-color;
- font-size: 13px;
- line-height: 22px;
-}
.dropdown-title {
position: relative;
- margin-bottom: 10px;
- padding-left: 30px;
- padding-right: 30px;
- padding-bottom: 10px;
+ padding: 0 0 15px;
+ margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
text-align: center;
@@ -261,21 +275,26 @@
}
.dropdown-menu-close {
- right: 0;
+ right: 7px;
+ width: 20px;
+ height: 20px;
+ top: -1px;
}
.dropdown-menu-back {
- left: 0;
+ left: 7px;
+ top: 2px;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
+ padding: 0 10px;
.fa {
position: absolute;
top: 10px;
- right: 10px;
+ right: 20px;
color: #c7c7c7;
font-size: 12px;
pointer-events: none;
@@ -285,6 +304,9 @@
display: none;
cursor: pointer;
pointer-events: all;
+ right: 22px;
+ top: 9px;
+ font-size: 14px;
}
&.has-value {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 4cb4129b71b..91b6451e68a 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -6,40 +6,6 @@ input {
border-radius: $border-radius-base;
}
-input[type='search'] {
- background-color: white;
- padding-left: 10px;
-}
-
-input[type='search'].search-input {
- background-repeat: no-repeat;
- background-position: 10px;
- background-size: 16px;
- background-position-x: 30%;
- padding-left: 10px;
- background-color: $gray-light;
-
- &.search-input[value=""] {
- background-image: url('data:image/png;base64,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');
- }
-
- &.search-input::-webkit-input-placeholder {
- text-align: center;
- }
-
- &.search-input:-moz-placeholder { /* Firefox 18- */
- text-align: center;
- }
-
- &.search-input::-moz-placeholder { /* Firefox 19+ */
- text-align: center;
- }
-
- &.search-input:-ms-input-placeholder {
- text-align: center;
- }
-}
-
input[type='text'].danger {
background: #f2dede!important;
border-color: #d66;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 6a68bb5c115..a450167b04f 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -117,26 +117,6 @@ header {
}
}
- .search {
- margin-right: 10px;
- margin-left: 10px;
- margin-top: ($header-height - 36) / 2;
-
- form {
- margin: 0;
- padding: 0;
- }
-
- .search-input {
- width: 220px;
-
- &:focus {
- @include box-shadow(none);
- outline: none;
- }
- }
- }
-
.impersonation i {
color: $red-normal;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 61e0dd4d672..bb49ae396c7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -66,7 +66,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958;
-$border-radius-default: 3px;
+$border-radius-default: 2px;
$btn-transparent-color: #8f8f8f;
$ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px;
@@ -166,6 +166,8 @@ $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;
$dropdown-link-hover-bg: $row-hover;
@@ -177,7 +179,7 @@ $dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
$dropdown-input-focus-border: rgb(58, 171, 240);
-$dropdown-input-focus-shadow: rgba(#000, .2);
+$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
@@ -193,3 +195,15 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
$award-emoji-menu-bg: #fff;
$award-emoji-menu-border: #f1f2f4;
$award-emoji-new-btn-icon-color: #dcdcdc;
+
+/*
+ * Search Box
+ */
+$search-input-border-color: $dropdown-input-focus-border;
+$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
+$search-input-width: $dropdown-width;
+$location-badge-color: #aaa;
+$location-badge-bg: $gray-normal;
+$location-icon-color: #e7e9ed;
+$location-active-color: $gl-text-color;
+$location-active-bg: $search-input-border-color;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index b6e45024644..3c74d25beb0 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -21,3 +21,145 @@
}
}
+.search {
+ margin-right: 10px;
+ margin-left: 10px;
+ margin-top: ($header-height - 35) / 2;
+
+ form {
+ @extend .form-control;
+ margin: 0;
+ padding: 4px;
+ width: $search-input-width;
+ line-height: 24px;
+ }
+
+ .location-text {
+ font-style: normal;
+ }
+
+ .search-input {
+ border: none;
+ font-size: 14px;
+ outline: none;
+ padding: 0;
+ margin-left: 5px;
+ line-height: 25px;
+ width: 98%;
+ }
+
+ .location-badge {
+ line-height: 25px;
+ padding: 0 5px;
+ border-radius: $border-radius-default;
+ font-size: 14px;
+ font-style: normal;
+ color: $location-badge-color;
+ display: inline-block;
+ background-color: $location-badge-bg;
+ vertical-align: top;
+ }
+
+ .search-input-container {
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ }
+
+ .search-location-badge, .search-input-wrap {
+ // Fallback if flexbox is not supported
+ display: inline-block;
+ }
+
+ .search-input-wrap {
+ width: 100%;
+
+ .search-icon, .clear-icon {
+ position: absolute;
+ right: 5px;
+ top: 0;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ .search-icon {
+ @extend .fa-search;
+ @include transition(color .15s);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ }
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ }
+
+ // Rewrite position. Dropdown menu should be relative to .search-input-container
+ .dropdown {
+ position: static;
+ }
+
+ .dropdown-header {
+ text-transform: uppercase;
+ font-size: 11px;
+ }
+
+ // Custom dropdown positioning
+ .dropdown-menu {
+ top: 30px;
+ left: -5px;
+ padding: 0;
+
+ ul {
+ padding: 10px 0;
+ }
+ }
+
+ .dropdown-content {
+ max-height: 350px;
+ }
+ }
+
+ &.search-active {
+ form {
+ @extend .form-control:focus;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $search-input-focus-shadow-color;
+ }
+
+ .location-badge {
+ @include transition(all .15s);
+ background-color: $location-active-bg;
+ color: $white-light;
+ }
+
+ .search-input-wrap {
+ i {
+ color: $location-active-color;
+ }
+ }
+
+ &.has-location-badge {
+ .search-icon {
+ display: none;
+ }
+
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+ }
+
+ &.has-location-badge {
+ .search-input-wrap {
+ width: 78%;
+ }
+ }
+}
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 494dad0b41e..8a97a74ad73 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,4 +1,5 @@
module SearchHelper
+
def search_autocomplete_opts(term)
return unless current_user
@@ -23,45 +24,44 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { label: "Profile settings", url: profile_path },
- { label: "SSH Keys", url: profile_keys_path },
- { label: "Dashboard", url: root_path },
- { label: "Admin Section", url: admin_root_path },
+ { category: "Settings", label: "Profile settings", url: profile_path },
+ { category: "Settings", label: "SSH Keys", url: profile_keys_path },
+ { category: "Settings", label: "Dashboard", url: root_path },
+ { category: "Settings", label: "Admin Section", url: admin_root_path },
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
- { label: "help: API Help", url: help_page_path("api", "README") },
- { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") },
- { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") },
- { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") },
- { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
- { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
- { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
- { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
+ { category: "Help", label: "API Help", url: help_page_path("api", "README") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
+ { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
+ { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
+ { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
+ { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
+ { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
]
end
# Autocomplete results for the current project, if it's defined
def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref
- prefix = search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref
[
- { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) },
- { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
- { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
- { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
- { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
+ { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
[]
@@ -72,7 +72,9 @@ module SearchHelper
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group|
{
- label: "group: #{search_result_sanitize(group.name)}",
+ category: "Groups",
+ id: group.id,
+ label: "#{search_result_sanitize(group.name)}",
url: group_path(group)
}
end
@@ -83,7 +85,10 @@ module SearchHelper
current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p|
{
- label: "project: #{search_result_sanitize(p.name_with_namespace)}",
+ category: "Projects",
+ id: p.id,
+ value: "#{search_result_sanitize(p.name)}",
+ label: "#{search_result_sanitize(p.name_with_namespace)}",
url: namespace_project_path(p.namespace, p)
}
end
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 54af2c3063c..9d4ab9847a8 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,10 +1,33 @@
-.search
- = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
- = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
+- if controller.controller_path =~ /^groups/
+ - label = 'This group'
+- if controller.controller_path =~ /^projects/
+ - label = 'This project'
+
+.search.search-form{class: "#{'has-location-badge' if label.present?}"}
+ = form_tag search_path, method: :get, class: 'navbar-form' do |f|
+ .search-input-container
+ .search-location-badge
+ - if label.present?
+ %span.location-badge
+ %i.location-text
+ = label
+ .search-input-wrap
+ .dropdown{ data: {url: search_autocomplete_path } }
+ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
+ .dropdown-menu.dropdown-select
+ = dropdown_content do
+ %ul
+ %li
+ %a.is-focused.dropdown-menu-empty-link
+ Loading...
+ = dropdown_loading
+ %i.search-icon
+ %i.clear-icon.js-clear-input
+
= hidden_field_tag :group_id, @group.try(:id)
- - if @project && @project.persisted?
- = hidden_field_tag :project_id, @project.id
+ = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
+ - if @project && @project.persisted?
- if current_controller?(:issues)
= hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests)
@@ -21,10 +44,3 @@
= hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
-
-:javascript
- $('.search-input').on('keyup', function(e) {
- if (e.keyCode == 27) {
- $('.search-input').blur();
- }
- });
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 93aa77589be..e21af72a777 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "All" link' do
- find('.js-author-search').click
- find('.dropdown-content a', match: :first).click
-
- find('.js-assignee-search').click
- find('.dropdown-content a', match: :first).click
+ find(".js-author-search").click
+ find(".dropdown-menu-author li a", match: :first).click
+ find(".js-assignee-search").click
+ find(".dropdown-menu-assignee li a", match: :first).click
end
def should_see(issue)