mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-15 15:46:08 +10:00
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  ## Group context  ## Project context  ## Screencast  See merge request !4499
This commit is contained in:
@@ -79,6 +79,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
|
||||
|
||||
@@ -1,7 +1,45 @@
|
||||
((w) ->
|
||||
|
||||
window.gl or= {}
|
||||
window.gl.utils or= {}
|
||||
w.gl or= {}
|
||||
w.gl.utils or= {}
|
||||
|
||||
w.gl.utils.isInGroupsPage = ->
|
||||
|
||||
return $('body').data('page').split(':')[0] is 'groups'
|
||||
|
||||
|
||||
w.gl.utils.isInProjectPage = ->
|
||||
|
||||
return $('body').data('page').split(':')[0] is 'projects'
|
||||
|
||||
|
||||
w.gl.utils.getProjectSlug = ->
|
||||
|
||||
return if @isInProjectPage() then $('body').data 'project' else null
|
||||
|
||||
|
||||
w.gl.utils.getGroupSlug = ->
|
||||
|
||||
return if @isInGroupsPage() then $('body').data 'group' else null
|
||||
|
||||
|
||||
|
||||
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
|
||||
|
||||
$tooltipEl
|
||||
.tooltip 'destroy'
|
||||
.attr 'title', newTitle
|
||||
.tooltip 'fixTitle'
|
||||
|
||||
|
||||
gl.utils.preventDisabledButtons = ->
|
||||
|
||||
$('.btn').click (e) ->
|
||||
if $(this).hasClass 'disabled'
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
|
||||
|
||||
jQuery.timefor = (time, suffix, expiredLabel) ->
|
||||
|
||||
@@ -24,20 +62,4 @@
|
||||
|
||||
return timefor
|
||||
|
||||
|
||||
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
|
||||
|
||||
$tooltipEl
|
||||
.tooltip 'destroy'
|
||||
.attr 'title', newTitle
|
||||
.tooltip 'fixTitle'
|
||||
|
||||
gl.utils.preventDisabledButtons = ->
|
||||
|
||||
$('.btn').click (e) ->
|
||||
if $(this).hasClass 'disabled'
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
|
||||
) 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
|
||||
@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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
!!! 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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.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
|
||||
@@ -1,7 +1,7 @@
|
||||
#= 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', ->
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#= 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
|
||||
Reference in New Issue
Block a user