Commit 54851c01 authored by Jacob Schatz's avatar Jacob Schatz Committed by Yorick Peterse

Merge branch 'multi-filter-labels' into 'master'

Mutliple label filter

Fixes #989 

See merge request !3438
parent 4715ab91
...@@ -17,6 +17,7 @@ class Dispatcher ...@@ -17,6 +17,7 @@ class Dispatcher
switch page switch page
when 'projects:issues:index' when 'projects:issues:index'
Issues.init() Issues.init()
Issuable.init()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show' when 'projects:issues:show'
new Issue() new Issue()
...@@ -57,7 +58,7 @@ class Dispatcher ...@@ -57,7 +58,7 @@ class Dispatcher
new ZenMode() new ZenMode()
when 'projects:merge_requests:index' when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
MergeRequests.init() Issuable.init()
when 'dashboard:activity' when 'dashboard:activity'
new Activities() new Activities()
when 'dashboard:projects:starred' when 'dashboard:projects:starred'
......
...@@ -386,13 +386,13 @@ class GitLabDropdown ...@@ -386,13 +386,13 @@ class GitLabDropdown
else else
selectedObject selectedObject
else else
if !value? if not @options.multiSelect or el.hasClass('dropdown-clear-active')
field.remove()
if not @options.multiSelect
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove() @dropdown.parent().find("input[name='#{fieldName}']").remove()
if !value?
field.remove()
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.addClass ACTIVE_CLASS el.addClass ACTIVE_CLASS
......
@Issuable =
init: ->
Issuable.initTemplates()
Issuable.initSearch()
initTemplates: ->
Issuable.labelRow = _.template(
'<% _.each(labels, function(label){ %>
<span class="label-row">
<a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
</span>
<% }); %>'
)
initSearch: ->
@timer = null
$('#issue_search')
.off 'keyup'
.on 'keyup', ->
clearTimeout(@timer)
@timer = setTimeout( ->
Issuable.filterResults $('#issue_search_form')
, 500)
toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0
$filteredLabels.removeClass('hidden')
else
$filteredLabels.addClass('hidden')
filterResults: (form) =>
formData = form.serialize()
$('.issues-holder, .merge-requests-holder').css('opacity', '0.5')
formAction = form.attr('action')
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: 'GET'
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issuable.reload()
Issuable.updateStateFilters()
$filteredLabels = $('.filtered-labels')
if typeof Issuable.labelRow is 'function'
$filteredLabels.html(Issuable.labelRow(data))
Issuable.toggleLabelFilters()
dataType: "json"
reload: ->
if Issues.created
Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
updateStateFilters: ->
stateFilters = $('.issues-state-filters')
newParams = {}
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
labelNameValues = gl.utils.getParameterValues('label_name[]')
if labelNameValues
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
else
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
$(this).attr 'href', newUrl
@Issues = @Issues =
init: -> init: ->
Issues.initSearch() Issues.created = true
Issues.initChecks() Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", -> $("body").on "ajax:success", ".close_issue, .reopen_issue", ->
...@@ -15,10 +15,6 @@ ...@@ -15,10 +15,6 @@
else else
$(this).html totalIssues - 1 $(this).html totalIssues - 1
reload: ->
Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
initChecks: -> initChecks: ->
$(".check_all_issues").click -> $(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked) $(".selected_issue").prop("checked", @checked)
...@@ -26,51 +22,6 @@ ...@@ -26,51 +22,6 @@
$(".selected_issue").bind "change", Issues.checkChanged $(".selected_issue").bind "change", Issues.checkChanged
# Update state filters if present in page
updateStateFilters: ->
stateFilters = $('.issues-state-filters')
newParams = {}
paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = $(this).attr 'href'
$(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
@timer = setTimeout( ->
Issues.filterResults $("#issue_search_form")
, 500)
filterResults: (form) =>
$('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
formAction = form.attr('action')
formData = form.serialize()
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: "GET"
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
Issues.updateStateFilters()
dataType: "json"
checkChanged: -> checkChanged: ->
checked_issues = $(".selected_issue:checked") checked_issues = $(".selected_issue:checked")
if checked_issues.length > 0 if checked_issues.length > 0
......
...@@ -6,7 +6,7 @@ class @LabelsSelect ...@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl = $dropdown.data('labels') labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate') issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected') selectedLabel = $dropdown.data('selected')
if selectedLabel? if selectedLabel? and not $dropdown.hasClass 'js-multiselect'
selectedLabel = selectedLabel.split(',') selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
...@@ -16,6 +16,7 @@ class @LabelsSelect ...@@ -16,6 +16,7 @@ class @LabelsSelect
abilityName = $dropdown.data('ability-name') abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox') $selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block') $block = $selectbox.closest('.block')
$form = $dropdown.closest('form')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$value = $block.find('.value') $value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut() $loading = $block.find('.block-loading').fadeOut()
...@@ -171,7 +172,7 @@ class @LabelsSelect ...@@ -171,7 +172,7 @@ class @LabelsSelect
.find('a') .find('a')
.each((i) -> .each((i) ->
setTimeout(=> setTimeout(=>
glAnimate($(@), 'pulse') gl.animate.animate($(@), 'pulse')
,200 * i ,200 * i
) )
) )
...@@ -200,16 +201,21 @@ class @LabelsSelect ...@@ -200,16 +201,21 @@ class @LabelsSelect
callback data callback data
renderRow: (label) -> renderRow: (label) ->
selectedClass = '' removesAll = label.id is 0 or not label.id?
if $selectbox.find("input[type='hidden']\
[name='#{$dropdown.data('field-name')}']\ selectedClass = []
[value='#{label.id}']").length if $form.find("input[type='hidden']\
selectedClass = 'is-active' [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
selectedClass.push 'is-active'
if $dropdown.hasClass('js-multiselect') and removesAll
selectedClass.push 'dropdown-clear-active'
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li> "<li>
<a href='#' class='#{selectedClass}'> <a href='#' class='#{selectedClass.join(' ')}'>
#{color} #{color}
#{_.escape(label.title)} #{_.escape(label.title)}
</a> </a>
...@@ -219,37 +225,56 @@ class @LabelsSelect ...@@ -219,37 +225,56 @@ class @LabelsSelect
fields: ['title'] fields: ['title']
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected, el) ->
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
if selected and selected.title? if selected and selected.title?
if selected_labels.length > 1
"#{selected.title} +#{selected_labels.length - 1} more"
else
selected.title selected.title
else if not selected and selected_labels.length isnt 0
if selected_labels.length > 1
"#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more"
else if selected_labels.length is 1
$(selected_labels).text()
else else
defaultLabel defaultLabel
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
id: (label) -> id: (label) ->
if label.isAny? if $dropdown.hasClass("js-filter-submit") and not label.isAny?
''
else if $dropdown.hasClass "js-filter-submit"
label.title label.title
else else
label.id label.id
hidden: -> hidden: ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index'
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect' if $dropdown.hasClass 'js-multiselect'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedLabels = $dropdown
.closest('form')
.find("input:hidden[name='#{$dropdown.data('fieldName')}']")
Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
saveLabelData() saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect' multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) -> clicked: (label) ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index' isMRIndex = page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
if not $dropdown.hasClass 'js-multiselect'
selectedLabel = label.title selectedLabel = label.title
Issuable.filterResults $dropdown.closest('form')
Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
......
((w) -> ((w) ->
if not w.gl? then w.gl = {}
if not gl.animate? then gl.animate = {}
w.glAnimate = ($el, animation, done) -> gl.animate.animate = ($el, animation, options, done) ->
if options?.cssStart?
$el.css(options.cssStart)
$el $el
.removeClass() .removeClass(animation + ' animated')
.addClass(animation + ' animated') .addClass(animation + ' animated')
.one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
$(this).removeClass() $(this).removeClass(animation + ' animated')
return if done?
done()
if options?.cssEnd?
$el.css(options.cssEnd)
return return
return return
gl.animate.animateEach = ($els, animation, time, options, done) ->
dfd = $.Deferred()
if not $els.length
dfd.resolve()
$els.each((i) ->
setTimeout(=>
$this = $(@)
gl.animate.animate($this, animation, options, =>
if i is $els.length - 1
dfd.resolve()
if done?
done()
)
,time * i
)
return
)
return dfd.promise()
return
) window ) window
\ No newline at end of file
...@@ -3,16 +3,20 @@ ...@@ -3,16 +3,20 @@
w.gl ?= {} w.gl ?= {}
w.gl.utils ?= {} w.gl.utils ?= {}
w.gl.utils.getUrlParameter = (sParam) -> # Returns an array containing the value(s) of the
# of the key passed as an argument
w.gl.utils.getParameterValues = (sParam) ->
sPageURL = decodeURIComponent(window.location.search.substring(1)) sPageURL = decodeURIComponent(window.location.search.substring(1))
sURLVariables = sPageURL.split('&') sURLVariables = sPageURL.split('&')
sParameterName = undefined sParameterName = undefined
values = []
i = 0 i = 0
while i < sURLVariables.length while i < sURLVariables.length
sParameterName = sURLVariables[i].split('=') sParameterName = sURLVariables[i].split('=')
if sParameterName[0] is sParam if sParameterName[0] is sParam
return if sParameterName[1] is undefined then true else sParameterName[1] values.push(sParameterName[1])
i++ i++
values
# # # #
# @param {Object} params - url keys and value to merge # @param {Object} params - url keys and value to merge
...@@ -28,4 +32,12 @@ ...@@ -28,4 +32,12 @@
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
newUrl newUrl
# removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = (url, param) ->
url = decodeURIComponent(url)
urlVariables = url.split('&')
(
variables for variables in urlVariables when variables.indexOf(param) is -1
).join('&')
) window ) window
#
# * Filter merge requests
#
@MergeRequests =
init: ->
MergeRequests.initSearch()
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
@timer = setTimeout(MergeRequests.filterResults, 500)
filterResults: =>
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5')
issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
url: form.attr('action')
data: form.serialize()
complete: ->
$('.merge-requests-holder').css("opacity", '1.0')
success: (data) ->
$('.merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issues_url}, document.title, issues_url
MergeRequests.reload()
dataType: "json"
reload: ->
$('#filter_issue_search').val($('#issue_search').val())
...@@ -97,7 +97,7 @@ class @MilestoneSelect ...@@ -97,7 +97,7 @@ class @MilestoneSelect
selectedMilestone = selected.name selectedMilestone = selected.name
else else
selectedMilestone = '' selectedMilestone = ''
Issues.filterResults $dropdown.closest('form') Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit') else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
......
...@@ -158,7 +158,7 @@ class @UsersSelect ...@@ -158,7 +158,7 @@ class @UsersSelect
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedId = user.id selectedId = user.id
Issues.filterResults $dropdown.closest('form') Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
......
...@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
end end
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@label = @project.labels.find_by(title: params[:label_name]) @labels = @project.labels.where(title: params[:label_name])
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
format.json do format.json do
render json: { render json: {
html: view_to_html_string("projects/issues/_issues") html: view_to_html_string("projects/issues/_issues"),
labels: @labels.as_json(methods: :text_color)
} }
end end
end end
......
...@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project) @merge_requests = @merge_requests.preload(:target_project)
@label = @project.labels.find_by(title: params[:label_name]) @labels = @project.labels.where(title: params[:label_name])
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: { render json: {
html: view_to_html_string("projects/merge_requests/_merge_requests") html: view_to_html_string("projects/merge_requests/_merge_requests"),
labels: @labels.as_json(methods: :text_color)
} }
end end
end end
......
...@@ -117,7 +117,7 @@ class IssuableFinder ...@@ -117,7 +117,7 @@ class IssuableFinder
end end
def filter_by_no_label? def filter_by_no_label?
labels? && params[:label_name] == Label::None.title labels? && params[:label_name].include?(Label::None.title)
end end
def labels def labels
...@@ -278,7 +278,9 @@ class IssuableFinder ...@@ -278,7 +278,9 @@ class IssuableFinder
end end
end end
items # When filtering by multiple labels we may end up duplicating issues (if one
# has multiple labels). This ensures we only return unique issues.
items.distinct
end end
def label_names def label_names
......
...@@ -254,11 +254,11 @@ module ApplicationHelper ...@@ -254,11 +254,11 @@ module ApplicationHelper
def page_filter_path(options = {}) def page_filter_path(options = {})
without = options.delete(:without) without = options.delete(:without)
add_label = options.delete(:label)
exist_opts = { exist_opts = {
state: params[:state], state: params[:state],
scope: params[:scope], scope: params[:scope],
label_name: params[:label_name],
milestone_title: params[:milestone_title], milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id], assignee_id: params[:assignee_id],
author_id: params[:author_id], author_id: params[:author_id],
...@@ -275,6 +275,13 @@ module ApplicationHelper ...@@ -275,6 +275,13 @@ module ApplicationHelper
path = request.path path = request.path
path << "?#{options.to_param}" path << "?#{options.to_param}"
if add_label
if params[:label_name].present? and params[:label_name].respond_to?('any?')
params[:label_name].each do |label|
path << "&label_name[]=#{label}"
end
end
end
path path
end end
......
...@@ -16,6 +16,25 @@ module IssuablesHelper ...@@ -16,6 +16,25 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end end
def multi_label_name(current_labels, default_label)
# current_labels may be a string from before
if current_labels.is_a?(Array)
if current_labels.count > 1
"#{current_labels[0]} +#{current_labels.count - 1} more"
else
current_labels[0]
end
elsif current_labels.is_a?(String)
if current_labels.nil? || current_labels.empty?
default_label
else
current_labels
end
else
default_label
end
end
def issuable_json_path(issuable) def issuable_json_path(issuable)
project = issuable.project project = issuable.project
......
...@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base ...@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
template template
end end
def text_color
LabelsHelper::text_color_for_bg(self.color)
end
private private
def label_format_reference(format = :id) def label_format_reference(format = :id)
......
- labels.each do |label|
%span.label-row
= link_to_label(label, tooltip: false)
...@@ -46,9 +46,10 @@ ...@@ -46,9 +46,10 @@
.filter-item.inline .filter-item.inline
= button_tag "Update issues", class: "btn update_selected_issues btn-save" = button_tag "Update issues", class: "btn update_selected_issues btn-save"
- if @label - if !@labels.nil?
.gray-content-block.second-block .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
= render "shared/label_row", label: @label - if @labels.any?
= render "shared/labels_row", labels: @labels
:javascript :javascript
new UsersSelect(); new UsersSelect();
......
- if params[:label_name].present? - if params[:label_name].present?
= hidden_field_tag(:label_name, params[:label_name]) - if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label|
= hidden_field_tag "label_name[]", label, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
%span.dropdown-toggle-text %span.dropdown-toggle-text
= h(params[:label_name].presence || "Label") = h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one .dropdown-page-one
......
...@@ -4,22 +4,22 @@ ...@@ -4,22 +4,22 @@
- else - else
- page_context_word = 'issues' - page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')} %li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
#{state_filters_text_for(:opened, @project)} #{state_filters_text_for(:opened, @project)}
- if defined?(type) && type == :merge_requests - if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')} %li{class: ("active" if params[:state] == 'merged')}
= link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
#{state_filters_text_for(:merged, @project)} #{state_filters_text_for(:merged, @project)}
%li{class: ("active" if params[:state] == 'closed')} %li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
#{state_filters_text_for(:closed, @project)} #{state_filters_text_for(:closed, @project)}
- else - else
%li{class: ("active" if params[:state] == 'closed')} %li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
#{state_filters_text_for(:closed, @project)} #{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')} %li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
#{state_filters_text_for(:all, @project)} #{state_filters_text_for(:all, @project)}
...@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels ...@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels
@javascript @javascript
Scenario: I filter by one label Scenario: I filter by one label
Given I click link "bug" Given I click link "bug"
And I click "dropdown close button"
Then I should see "Bugfix1" in issues list Then I should see "Bugfix1" in issues list
And I should see "Bugfix2" in issues list And I should see "Bugfix2" in issues list
And I should not see "Feature1" in issues list And I should not see "Feature1" in issues list
......
...@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps ...@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
page.find('.js-label-select').click page.find('.js-label-select').click
sleep 0.5 sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
end
step 'I click "dropdown close button"' do
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2 sleep 2
end end
......
require 'rails_helper'
feature 'Issue filtering by Labels', feature: true do
let(:project) { create(:project, :public) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project) }
before do
['bug', 'feature', 'enhancement'].each do |title|
create(:label,
project: project,
title: title)
end
issue1 = create(:issue, title: "Bugfix1", project: project)
issue1.labels << project.labels.find_by(title: 'bug')
issue2 = create(:issue, title: "Bugfix2", project: project)
issue2.labels << project.labels.find_by(title: 'bug')
issue2.labels << project.labels.find_by(title: 'enhancement')
issue3 = create(:issue, title: "Feature1", project: project)
issue3.labels << project.labels.find_by(title: 'feature')
project.team << [user, :master]
login_as(user)
visit namespace_project_issues_path(project.namespace, project)
end
context 'filter by label bug', js: true do
before do
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
end
it 'should not show "Feature1" in issues list' do
expect(page).not_to have_content "Feature1"
end
it 'should show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
end
it 'should not show label "feature" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
end
context 'filter by label feature', js: true do
before do
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
it 'should show issue "Feature1" in issues list' do
expect(page).to have_content "Feature1"
end
it 'should not show "Bugfix1" and "Bugfix2" in issues list' do
expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1"
end
it 'should show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "feature"
end
it 'should not show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
end
context 'filter by label enhancement', js: true do
before do
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
it 'should show issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
end
it 'should not show "Feature1" and "Bugfix1" in issues list' do
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
end
it 'should show label "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
end
it 'should not show label "feature" and "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
context 'filter by label enhancement or feature', js: true do
before do
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
it 'should show issue "Bugfix2" or "Feature1" in issues list' do
expect(page).to have_content "Bugfix2"
expect(page).to have_content "Feature1"
end
it 'should not show "Bugfix1" in issues list' do
expect(page).not_to have_content "Bugfix1"
end
it 'should show label "enhancement" and "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).to have_content "feature"
end
it 'should not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
end
end
context 'filter by label enhancement or bug in issues list', js: true do
before do
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
it 'should show issue "Bugfix2" or "Bugfix1" in issues list' do
expect(page).to have_content "Bugfix2"
expect(page).to have_content "Bugfix1"
end
it 'should not show "Feature1"' do
expect(page).not_to have_content "Feature1"
end
it 'should show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
end
it 'should not show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
end
...@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do ...@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do
it 'should filter by any label' do it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
page.within '.labels-filter' do page.within '.labels-filter' do
expect(page).to have_content 'Any Label' expect(page).to have_content 'Any Label'
end end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label') expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
end end
it 'should filter by no label' do it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
page.within '.labels-filter' do page.within '.labels-filter' do
expect(page).to have_content 'No Label' expect(page).to have_content 'No Label'
end end
...@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do ...@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do
find('.js-label-select').click find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2 sleep 2
end end
......
...@@ -2,8 +2,14 @@ require 'rails_helper' ...@@ -2,8 +2,14 @@ require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do feature 'Merge Request filtering by Milestone', feature: true do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let!(:user) { create(:user)}
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
before do
project.team << [user, :master]
login_as(user)
end
scenario 'filters by no Milestone', js: true do scenario 'filters by no Milestone', js: true do
create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project, milestone: milestone)
......
...@@ -62,6 +62,22 @@ describe IssuesFinder do ...@@ -62,6 +62,22 @@ describe IssuesFinder do
expect(issues).to eq([issue2]) expect(issues).to eq([issue2])
end end
it 'returns unique issues when filtering by multiple labels' do
label2 = create(:label, project: project2)
create(:label_link, label: label2, target: issue2)
params = {
scope: 'all',
label_name: [label.title, label2.title].join(','),
state: 'opened'
}
issues = IssuesFinder.new(user, params).execute
expect(issues).to eq([issue2])
end
it 'should filter by no label name' do it 'should filter by no label name' do
params = { scope: "all", label_name: Label::None.title, state: 'opened' } params = { scope: "all", label_name: Label::None.title, state: 'opened' }
issues = IssuesFinder.new(user, params).execute issues = IssuesFinder.new(user, params).execute
......
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