Commit 15160d97 authored by Jacob Schatz's avatar Jacob Schatz Committed by Robert Speicher

Merge branch 'category-search-dropdown' into 'master'

Show categorised search queries in the search autocomplete

Fixes #5885 

It works in 3 categories, Dashboard, Groups and Project.

## Dashboard context
![for-dashboard](/uploads/3a59f6ec008a972495597c8f2691c385/for-dashboard.png)

## Group context
![for-group](/uploads/f7aa413d56330a1d9b2e5562f95badf7/for-group.png)

## Project context
![for-project](/uploads/dabe04cf8758a056cf7b03da001ffd91/for-project.png)


## Screencast
![category-search-dropdown](/uploads/4d9513dcd6ccb6e24adefdf65f9bc778/category-search-dropdown.gif)



See merge request !4499
parent f6a088ca
......@@ -76,6 +76,7 @@ v 8.9.0 (unreleased)
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
- Add workhorse controller and API helpers
- An indicator is now displayed at the top of the comment field for confidential issues.
- Show categorised search queries in the search autocomplete
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Improve issuables APIs performance when accessing notes !4471
- External links now open in a new tab
......
((w) ->
window.gl or= {}
window.gl.utils or= {}
w.gl or= {}
w.gl.utils or= {}
jQuery.timefor = (time, suffix, expiredLabel) ->
w.gl.utils.isInGroupsPage = ->
return '' unless time
return $('body').data('page').split(':')[0] is 'groups'
suffix or= 'remaining'
expiredLabel or= 'Past due'
jQuery.timeago.settings.allowFuture = yes
w.gl.utils.isInProjectPage = ->
{ suffixFromNow } = jQuery.timeago.settings.strings
jQuery.timeago.settings.strings.suffixFromNow = suffix
return $('body').data('page').split(':')[0] is 'projects'
timefor = $.timeago time
if timefor.indexOf('ago') > -1
timefor = expiredLabel
w.gl.utils.getProjectSlug = ->
jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
return if @isInProjectPage() then $('body').data 'project' else null
w.gl.utils.getGroupSlug = ->
return if @isInGroupsPage() then $('body').data 'group' else null
return timefor
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
......@@ -32,6 +31,7 @@
.attr 'title', newTitle
.tooltip 'fixTitle'
gl.utils.preventDisabledButtons = ->
$('.btn').click (e) ->
......@@ -40,4 +40,26 @@
e.stopImmediatePropagation()
return false
jQuery.timefor = (time, suffix, expiredLabel) ->
return '' unless time
suffix or= 'remaining'
expiredLabel or= 'Past due'
jQuery.timeago.settings.allowFuture = yes
{ suffixFromNow } = jQuery.timeago.settings.strings
jQuery.timeago.settings.strings.suffixFromNow = suffix
timefor = $.timeago time
if timefor.indexOf('ago') > -1
timefor = expiredLabel
jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
return timefor
) window
......@@ -67,8 +67,12 @@ class @SearchAutocomplete
getData: (term, callback) ->
_this = @
# Do not trigger request if input is empty
return if @searchInput.val() is ''
unless term
if contents = @getCategoryContents()
@searchInput.data('glDropdown').filter.options.callback contents
@enableAutocomplete()
return
# Prevent multiple ajax calls
return if @loadingSuggestions
......@@ -122,6 +126,37 @@ class @SearchAutocomplete
).always ->
_this.loadingSuggestions = false
getCategoryContents: ->
userId = gon.current_user_id
{ utils, projectOptions, groupOptions, dashboardOptions } = gl
if utils.isInGroupsPage() and groupOptions
options = groupOptions[utils.getGroupSlug()]
else if utils.isInProjectPage() and projectOptions
options = projectOptions[utils.getProjectSlug()]
else if dashboardOptions
options = dashboardOptions
{ issuesPath, mrPath, name } = options
items = [
{ header: "#{name}" }
{ text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
{ text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
'separator'
{ text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
{ text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
]
items.splice 0, 1 unless name
return items
serializeState: ->
{
# Search Criteria
......@@ -209,6 +244,12 @@ class @SearchAutocomplete
@isFocused = true
@wrap.addClass('search-active')
@getData() if @getValue() is ''
getValue: -> return @searchInput.val()
onClearInputClick: (e) =>
e.preventDefault()
@searchInput.val('').focus()
......@@ -229,6 +270,10 @@ class @SearchAutocomplete
@locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge')
hasLocationBadge: -> return @wrap.is '.has-location-badge'
restoreOriginalState: ->
inputs = Object.keys @originalState
......@@ -257,13 +302,14 @@ class @SearchAutocomplete
@getElement("##{input}").val('')
removeLocationBadge: ->
@locationBadgeEl.hide()
# Reset state
@locationBadgeEl.hide()
@resetSearchState()
@wrap.removeClass('has-location-badge')
@disableAutocomplete()
disableAutocomplete: ->
@searchInput.addClass('disabled')
......
......@@ -36,6 +36,31 @@
- else
= hidden_field_tag :search_code, true
:javascript
gl.projectOptions = gl.projectOptions || {};
gl.projectOptions["#{j(@project.path)}"] = {
issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
name: "#{j(@project.name)}"
};
- if @group and @group.path
:javascript
gl.groupOptions = gl.groupOptions || {};
gl.groupOptions["#{j(@group.path)}"] = {
name: "#{j(@group.name)}",
issuesPath: "#{issues_group_path(j(@group.path))}",
mrPath: "#{merge_requests_group_path(j(@group.path))}"
};
:javascript
gl.dashboardOptions = {
issuesPath: "#{issues_dashboard_url}",
mrPath: "#{merge_requests_dashboard_url}"
};
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
......
!!! 5
%html{ lang: "en"}
= render "layouts/head"
%body{class: "#{user_application_theme}", 'data-page' => body_data_page}
%body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
= Gon::Base.render_data
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
......
......@@ -47,4 +47,83 @@ describe "Search", feature: true do
expect(page).to have_link(snippet.title)
end
end
describe 'Right header search field', feature: true do
describe 'Search in project page' do
before do
visit namespace_project_path(project.namespace, project)
end
it 'top right search form is present' do
expect(page).to have_selector('#search')
end
it 'top right search form contains location badge' do
expect(page).to have_selector('.has-location-badge')
end
context 'clicking the search field', js: true do
it 'should show category search dropdown' do
page.find('#search').click
expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
end
end
context 'click the links in the category search dropdown', js: true do
before do
page.find('#search').click
end
it 'should take user to her issues page when issues assigned is clicked' do
find('.dropdown-menu').click_link 'Issues assigned to me'
sleep 2
expect(page).to have_selector('.issues-holder')
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should take user to her issues page when issues authored is clicked' do
find('.dropdown-menu').click_link "Issues I've created"
sleep 2
expect(page).to have_selector('.issues-holder')
expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should take user to her MR page when MR assigned is clicked' do
find('.dropdown-menu').click_link 'Merge requests assigned to me'
sleep 2
expect(page).to have_selector('.merge-requests-holder')
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should take user to her MR page when MR authored is clicked' do
find('.dropdown-menu').click_link "Merge requests I've created"
sleep 2
expect(page).to have_selector('.merge-requests-holder')
expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
end
end
context 'entering text into the search field', js: true do
before do
page.within '.search-input-wrap' do
fill_in "search", with: project.name[0..3]
end
end
it 'should not display the category search dropdown' do
expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
end
end
end
end
end
.search.search-form.has-location-badge
%form.navbar-form
.search-input-container
%div.location-badge
This project
.search-input-wrap
.dropdown
%input#search.search-input.dropdown-menu-toggle
.dropdown-menu.dropdown-select
.dropdown-content
#= require notes
#= require gl_form
window.gon = {}
window.gon or= {}
window.disableButtonIfEmptyField = -> null
describe 'Notes', ->
......
......@@ -6,7 +6,7 @@
#= require project_select
#= require project
window.gon = {}
window.gon or= {}
window.gon.api_version = 'v3'
describe 'Project Title', ->
......
#= require gl_dropdown
#= require search_autocomplete
#= require jquery
#= require lib/common_utils
#= require lib/type_utility
#= require fuzzaldrin-plus
widget = null
userId = 1
window.gon or= {}
window.gon.current_user_id = userId
dashboardIssuesPath = '/dashboard/issues'
dashboardMRsPath = '/dashboard/merge_requests'
projectIssuesPath = '/gitlab-org/gitlab-ce/issues'
projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'
groupIssuesPath = '/groups/gitlab-org/issues'
groupMRsPath = '/groups/gitlab-org/merge_requests'
projectName = 'GitLab Community Edition'
groupName = 'Gitlab Org'
# Add required attributes to body before starting the test.
# section would be dashboard|group|project
addBodyAttributes = (section = 'dashboard') ->
$body = $ 'body'
$body.removeAttr 'data-page'
$body.removeAttr 'data-project'
$body.removeAttr 'data-group'
switch section
when 'dashboard'
$body.data 'page', 'root:index'
when 'group'
$body.data 'page', 'groups:show'
$body.data 'group', 'gitlab-org'
when 'project'
$body.data 'page', 'projects:show'
$body.data 'project', 'gitlab-ce'
# Mock `gl` object in window for dashboard specific page. App code will need it.
mockDashboardOptions = ->
window.gl or= {}
window.gl.dashboardOptions =
issuesPath: dashboardIssuesPath
mrPath : dashboardMRsPath
# Mock `gl` object in window for project specific page. App code will need it.
mockProjectOptions = ->
window.gl or= {}
window.gl.projectOptions =
'gitlab-ce' :
issuesPath : projectIssuesPath
mrPath : projectMRsPath
projectName : projectName
mockGroupOptions = ->
window.gl or= {}
window.gl.groupOptions =
'gitlab-org' :
issuesPath : groupIssuesPath
mrPath : groupMRsPath
projectName : groupName
assertLinks = (list, issuesPath, mrsPath) ->
issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}"
issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}"
mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}"
mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}"
a1 = "a[href='#{issuesAssignedToMeLink}']"
a2 = "a[href='#{issuesIHaveCreatedLink}']"
a3 = "a[href='#{mrsAssignedToMeLink}']"
a4 = "a[href='#{mrsIHaveCreatedLink}']"
expect(list.find(a1).length).toBe 1
expect(list.find(a1).text()).toBe ' Issues assigned to me '
expect(list.find(a2).length).toBe 1
expect(list.find(a2).text()).toBe " Issues I've created "
expect(list.find(a3).length).toBe 1
expect(list.find(a3).text()).toBe ' Merge requests assigned to me '
expect(list.find(a4).length).toBe 1
expect(list.find(a4).text()).toBe " Merge requests I've created "
describe 'Search autocomplete dropdown', ->
fixture.preload 'search_autocomplete.html'
beforeEach ->
fixture.load 'search_autocomplete.html'
widget = new SearchAutocomplete
it 'should show Dashboard specific dropdown menu', ->
addBodyAttributes()
mockDashboardOptions()
widget.searchInput.focus()
list = widget.wrap.find('.dropdown-menu').find 'ul'
assertLinks list, dashboardIssuesPath, dashboardMRsPath
it 'should show Group specific dropdown menu', ->
addBodyAttributes 'group'
mockGroupOptions()
widget.searchInput.focus()
list = widget.wrap.find('.dropdown-menu').find 'ul'
assertLinks list, groupIssuesPath, groupMRsPath
it 'should show Project specific dropdown menu', ->
addBodyAttributes 'project'
mockProjectOptions()
widget.searchInput.focus()
list = widget.wrap.find('.dropdown-menu').find 'ul'
assertLinks list, projectIssuesPath, projectMRsPath
it 'should not show category related menu if there is text in the input', ->
addBodyAttributes 'project'
mockProjectOptions()
widget.searchInput.val 'help'
widget.searchInput.focus()
list = widget.wrap.find('.dropdown-menu').find 'ul'
link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']"
expect(list.find(link).length).toBe 0
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