Commit 403b7fe1 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'issue_3400_port' into 'master'

Location aware search

Closes #3400 #14217

Introduces suggestion grouping on the search box and adds the ability to the user to remove the location to search on.


**Notes**
- Suggestions are now grouped by category.
- Suggestions are displayed when no location is set.
  - Would be great to provide suggestions for the specified location which could be a project or group.
- A location is set from the server for projects/groups related urls.

**Default Apparence**

![](/uploads/9fad1a354fb0e4b13cfd36698c061ab4/default.gif)

**When location badge is present**

![](/uploads/ddc6379f407061e188f9f4a7a9a8c9b8/location-badge.gif)

**Suggestions**

![suggestions](/uploads/2df288e00ad496b31a1a2efc2a4a9f6d/suggestions.gif)

**Suggestions when location badge is present**

![](/uploads/f6ef09d3aed124179ab4e228b848486e/location-present-suggestions.gif)

Closes #3400 

See merge request !3212
parents 6985e7db a41f5f59
...@@ -146,15 +146,11 @@ class Dispatcher ...@@ -146,15 +146,11 @@ class Dispatcher
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one # If we haven't installed a custom shortcut handler, install the default one
if not shortcut_handler if not shortcut_handler
new Shortcuts() new Shortcuts()
initSearch: -> 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
...@@ -3,6 +3,10 @@ class GitLabDropdownFilter ...@@ -3,6 +3,10 @@ class GitLabDropdownFilter
HAS_VALUE_CLASS = "has-value" HAS_VALUE_CLASS = "has-value"
constructor: (@input, @options) -> constructor: (@input, @options) ->
{
@filterInputBlur = true
} = @options
$inputContainer = @input.parent() $inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear') $clearButton = $inputContainer.find('.js-dropdown-input-clear')
...@@ -33,7 +37,7 @@ class GitLabDropdownFilter ...@@ -33,7 +37,7 @@ class GitLabDropdownFilter
blur_field = @shouldBlur e.keyCode blur_field = @shouldBlur e.keyCode
search_text = @input.val() search_text = @input.val()
if blur_field if blur_field and @filterInputBlur
@input.blur() @input.blur()
if @options.remote if @options.remote
...@@ -93,27 +97,48 @@ class GitLabDropdown ...@@ -93,27 +97,48 @@ class GitLabDropdown
PAGE_TWO_CLASS = "is-page-two" PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active" ACTIVE_CLASS = "is-active"
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
constructor: (@el, @options) -> constructor: (@el, @options) ->
self = @
@dropdown = $(@el).parent() @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 []; search_fields = if @options.search then @options.search.fields else [];
if @options.data if @options.data
# Remote data # If data is an array
@remote = new GitLabDropdownRemote @options.data, { if _.isArray @options.data
dataType: @options.dataType, @fullData = @options.data
beforeSend: @toggleLoading.bind(@) @parseData @options.data
success: (data) => else
@fullData = data # 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 if @options.filterable
@input = @dropdown.find('.dropdown-input .dropdown-input-field') @filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur
@filter = new GitLabDropdownFilter @input,
remote: @options.filterRemote remote: @options.filterRemote
query: @options.data query: @options.data
keys: @options.search.fields keys: @options.search.fields
...@@ -123,7 +148,8 @@ class GitLabDropdown ...@@ -123,7 +148,8 @@ class GitLabDropdown
@parseData data @parseData data
@highlightRow 1 @highlightRow 1
enterCallback: => enterCallback: =>
@selectFirstRow() if @enterCallback
@selectFirstRow()
# Event listeners # Event listeners
...@@ -150,6 +176,10 @@ class GitLabDropdown ...@@ -150,6 +176,10 @@ class GitLabDropdown
if self.options.clicked if self.options.clicked
self.options.clicked(selected) self.options.clicked(selected)
# Finds an element inside wrapper element
getElement: (selector) ->
@dropdown.find selector
toggleLoading: -> toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
...@@ -193,7 +223,7 @@ class GitLabDropdown ...@@ -193,7 +223,7 @@ class GitLabDropdown
@remote.execute() @remote.execute()
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").focus() @filterInput.focus()
@dropdown.trigger('shown.gl.dropdown') @dropdown.trigger('shown.gl.dropdown')
...@@ -237,13 +267,19 @@ class GitLabDropdown ...@@ -237,13 +267,19 @@ class GitLabDropdown
renderItem: (data) -> renderItem: (data) ->
html = "" html = ""
# Divider
return "<li class='divider'></li>" if data is "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 if @options.renderRow
# Call the render function # Call the render function
html = @options.renderRow(data) html = @options.renderRow(data)
else else
selected = if @options.isSelected then @options.isSelected(data) else false
if not selected if not selected
value = if @options.id then @options.id(data) else data.id value = if @options.id then @options.id(data) else data.id
fieldName = @options.fieldName fieldName = @options.fieldName
...@@ -251,13 +287,26 @@ class GitLabDropdown ...@@ -251,13 +287,26 @@ class GitLabDropdown
if field.length if field.length
selected = true selected = true
url = if @options.url then @options.url(data) else "#" # Set URL
text = if @options.text then @options.text(data) else "" 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 = ""; cssClass = "";
if selected if selected
cssClass = "is-active" cssClass = "is-active"
if @highlight
text = @highlightTextMatches(text, @filterInput.val())
html = "<li>" html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>" html += "<a href='#{url}' class='#{cssClass}'>"
html += text html += text
...@@ -266,20 +315,26 @@ class GitLabDropdown ...@@ -266,20 +315,26 @@ class GitLabDropdown
return html 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: -> noResults: ->
html = "<li>" 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 += "No matching results."
html += "</a>" html += "</a>"
html += "</li>" html += "</li>"
highlightRow: (index) -> highlightRow: (index) ->
if @input.val() isnt "" if @filterInput.val() isnt ""
selector = '.dropdown-content li:first-child a' selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a" selector = ".dropdown-page-one .dropdown-content li:first-child a"
$(selector).addClass 'is-focused' @getElement(selector).addClass 'is-focused'
rowClicked: (el) -> rowClicked: (el) ->
fieldName = @options.fieldName fieldName = @options.fieldName
...@@ -331,4 +386,6 @@ class GitLabDropdown ...@@ -331,4 +386,6 @@ class GitLabDropdown
$.fn.glDropdown = (opts) -> $.fn.glDropdown = (opts) ->
return @.each -> return @.each ->
new GitLabDropdown @, opts if (!$.data @, 'glDropdown')
$.data(@, 'glDropdown', new GitLabDropdown @, opts)
class @SearchAutocomplete class @SearchAutocomplete
constructor: (search_autocomplete_path, project_id, project_ref) ->
project_id = '' unless project_id KEYCODE =
project_ref = '' unless project_ref ESCAPE: 27
query = "?project_id=" + project_id + "&project_ref=" + project_ref BACKSPACE: 8
ENTER: 13
$("#search").autocomplete
source: search_autocomplete_path + query constructor: (opts = {}) ->
minLength: 1 {
select: (event, ui) -> @wrap = $('.search')
location.href = ui.item.url
@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)
...@@ -42,7 +42,7 @@ ...@@ -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: 2px; border-radius: $dropdown-border-radius;
outline: 0; outline: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
...@@ -75,12 +75,12 @@ ...@@ -75,12 +75,12 @@
width: 240px; width: 240px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0; margin-bottom: 0;
padding: 10px; font-size: 15px;
font-size: 14px;
font-weight: normal; font-weight: normal;
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: $border-radius-base; border-radius: $dropdown-border-radius;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading { &.is-loading {
...@@ -101,9 +101,17 @@ ...@@ -101,9 +101,17 @@
li { li {
text-align: left; text-align: left;
list-style: none; list-style: none;
padding: 0 10px;
} }
.divider { .divider {
height: 1px;
margin: 8px 10px;
padding: 0;
background-color: $dropdown-divider-color;
}
.separator {
width: 100%; width: 100%;
height: 1px; height: 1px;
margin-top: 8px; margin-top: 8px;
...@@ -141,6 +149,17 @@ ...@@ -141,6 +149,17 @@
line-height: 16px; 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 { .dropdown-menu-paging {
...@@ -158,6 +177,10 @@ ...@@ -158,6 +177,10 @@
.dropdown-menu-back { .dropdown-menu-back {
display: block; display: block;
} }
.dropdown-content {
padding: 0 10px;
}
} }
} }
...@@ -193,7 +216,7 @@ ...@@ -193,7 +216,7 @@
} }
.dropdown-select { .dropdown-select {
width: 300px; width: $dropdown-width;
} }
.dropdown-menu-align-right { .dropdown-menu-align-right {
...@@ -222,20 +245,11 @@ ...@@ -222,20 +245,11 @@
} }
} }
.dropdown-header {
padding-left: 5px;
padding-right: 5px;
color: $dropdown-header-color;
font-size: 13px;
line-height: 22px;
}
.dropdown-title { .dropdown-title {
position: relative; position: relative;
margin-bottom: 10px; padding: 0 0 15px;
padding-left: 30px; margin: 0 10px 10px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
text-align: center; text-align: center;
...@@ -261,21 +275,26 @@ ...@@ -261,21 +275,26 @@
} }
.dropdown-menu-close { .dropdown-menu-close {
right: 0; right: 7px;
width: 20px;
height: 20px;
top: -1px;
} }
.dropdown-menu-back { .dropdown-menu-back {
left: 0; left: 7px;
top: 2px;
} }
.dropdown-input { .dropdown-input {
position: relative; position: relative;
margin-bottom: 10px; margin-bottom: 10px;
padding: 0 10px;
.fa { .fa {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 20px;
color: #c7c7c7; color: #c7c7c7;
font-size: 12px; font-size: 12px;
pointer-events: none; pointer-events: none;
...@@ -285,6 +304,9 @@ ...@@ -285,6 +304,9 @@
display: none; display: none;
cursor: pointer; cursor: pointer;
pointer-events: all; pointer-events: all;
right: 22px;
top: 9px;
font-size: 14px;
} }
&.has-value { &.has-value {
......
...@@ -6,40 +6,6 @@ input { ...@@ -6,40 +6,6 @@ input {
border-radius: $border-radius-base; 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 { input[type='text'].danger {
background: #f2dede!important; background: #f2dede!important;
border-color: #d66; border-color: #d66;
......
...@@ -117,26 +117,6 @@ header { ...@@ -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 { .impersonation i {
color: $red-normal; color: $red-normal;
} }
......
...@@ -66,7 +66,7 @@ $header-height: 58px; ...@@ -66,7 +66,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$error-exclamation-point: #e62958; $error-exclamation-point: #e62958;
$border-radius-default: 3px; $border-radius-default: 2px;
$btn-transparent-color: #8f8f8f; $btn-transparent-color: #8f8f8f;
$ssh-key-icon-color: #8f8f8f; $ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px; $ssh-key-icon-size: 18px;
...@@ -166,6 +166,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif ...@@ -166,6 +166,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
/* /*
* Dropdowns * Dropdowns
*/ */
$dropdown-border-radius: 2px;
$dropdown-width: 300px;
$dropdown-bg: #fff; $dropdown-bg: #fff;
$dropdown-link-color: #555; $dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover; $dropdown-link-hover-bg: $row-hover;
...@@ -177,7 +179,7 @@ $dropdown-header-color: #959494; ...@@ -177,7 +179,7 @@ $dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf; $dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555; $dropdown-input-color: #555;
$dropdown-input-focus-border: rgb(58, 171, 240); $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-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff; $dropdown-toggle-bg: #fff;
...@@ -193,3 +195,15 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; ...@@ -193,3 +195,15 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
$award-emoji-menu-bg: #fff; $award-emoji-menu-bg: #fff;
$award-emoji-menu-border: #f1f2f4; $award-emoji-menu-border: #f1f2f4;
$award-emoji-new-btn-icon-color: #dcdcdc; $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;
...@@ -21,3 +21,145 @@ ...@@ -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%;
}
}
}
module SearchHelper module SearchHelper
def search_autocomplete_opts(term) def search_autocomplete_opts(term)
return unless current_user return unless current_user
...@@ -23,45 +24,44 @@ module SearchHelper ...@@ -23,45 +24,44 @@ module SearchHelper
# Autocomplete results for various settings pages # Autocomplete results for various settings pages
def default_autocomplete def default_autocomplete
[ [
{ label: "Profile settings", url: profile_path }, { category: "Settings", label: "Profile settings", url: profile_path },
{ label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ label: "Dashboard", url: root_path }, { category: "Settings", label: "Dashboard", url: root_path },
{ label: "Admin Section", url: admin_root_path }, { category: "Settings", label: "Admin Section", url: admin_root_path },
] ]
end end
# Autocomplete results for internal help pages # Autocomplete results for internal help pages
def help_autocomplete def help_autocomplete
[ [
{ label: "help: API Help", url: help_page_path("api", "README") }, { category: "Help", label: "API Help", url: help_page_path("api", "README") },
{ label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
{ label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
{ label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") }, { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
] ]
end end
# Autocomplete results for the current project, if it's defined # Autocomplete results for the current project, if it's defined
def project_autocomplete def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref if @project && @project.repository.exists? && @project.repository.root_ref
prefix = search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref ref = @ref || @project.repository.root_ref
[ [
{ label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) },
{ label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
{ label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, { category: "Current Project", label: "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: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
] ]
else else
[] []
...@@ -72,7 +72,9 @@ module SearchHelper ...@@ -72,7 +72,9 @@ module SearchHelper
def groups_autocomplete(term, limit = 5) def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group| 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) url: group_path(group)
} }
end end
...@@ -83,7 +85,10 @@ module SearchHelper ...@@ -83,7 +85,10 @@ module SearchHelper
current_user.authorized_projects.search_by_title(term). current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p| 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) url: namespace_project_path(p.namespace, p)
} }
end end
......
.search - if controller.controller_path =~ /^groups/
= form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - label = 'This group'
= search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" - 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) = hidden_field_tag :group_id, @group.try(:id)
- if @project && @project.persisted? = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
= hidden_field_tag :project_id, @project.id
- if @project && @project.persisted?
- if current_controller?(:issues) - if current_controller?(:issues)
= hidden_field_tag :scope, 'issues' = hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests) - elsif current_controller?(:merge_requests)
...@@ -21,10 +44,3 @@ ...@@ -21,10 +44,3 @@
= hidden_field_tag :repository_ref, @ref = hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test' = 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 } .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();
}
});
...@@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps ...@@ -42,11 +42,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end end
step 'I click "All" link' do step 'I click "All" link' do
find('.js-author-search').click find(".js-author-search").click
find('.dropdown-content a', match: :first).click find(".dropdown-menu-author li a", match: :first).click
find(".js-assignee-search").click
find('.js-assignee-search').click find(".dropdown-menu-assignee li a", match: :first).click
find('.dropdown-content a', match: :first).click
end end
def should_see(issue) def should_see(issue)
......
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