Merge remote-tracking branch 'origin/master' into label-dropdown-fix
This commit is contained in:
commit
7e0ef892f4
17
CHANGELOG
17
CHANGELOG
|
@ -19,6 +19,7 @@ v 8.7.0 (unreleased)
|
||||||
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
|
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
|
||||||
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
|
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
|
||||||
- Restrict user profiles when public visibility level is restricted.
|
- Restrict user profiles when public visibility level is restricted.
|
||||||
|
- Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan)
|
||||||
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
|
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
|
||||||
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
|
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
|
||||||
- Add setting for customizing the list of trusted proxies !3524
|
- Add setting for customizing the list of trusted proxies !3524
|
||||||
|
@ -86,6 +87,10 @@ v 8.7.0 (unreleased)
|
||||||
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
|
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
|
||||||
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
|
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
|
||||||
- Improved markdown forms
|
- Improved markdown forms
|
||||||
|
- Show JavaScript errors in sentry
|
||||||
|
- Diff design updates (colors, button styles, etc)
|
||||||
|
- Copying and pasting a diff no longer pastes the line numbers or +/-
|
||||||
|
- Add null check to formData when updating profile content to fix Firefox bug
|
||||||
- Delete tags using Rugged for performance reasons (Robert Schilling)
|
- Delete tags using Rugged for performance reasons (Robert Schilling)
|
||||||
- Add Slack notifications when Wiki is edited (Sebastian Klier)
|
- Add Slack notifications when Wiki is edited (Sebastian Klier)
|
||||||
- Diffs load at the correct point when linking from from number
|
- Diffs load at the correct point when linking from from number
|
||||||
|
@ -107,7 +112,7 @@ v 8.7.0 (unreleased)
|
||||||
- Add RAW build trace output and button on build page
|
- Add RAW build trace output and button on build page
|
||||||
- Add incremental build trace update into CI API
|
- Add incremental build trace update into CI API
|
||||||
|
|
||||||
v 8.6.7 (unreleased)
|
v 8.6.7
|
||||||
- Fix persistent XSS vulnerability in `commit_person_link` helper
|
- Fix persistent XSS vulnerability in `commit_person_link` helper
|
||||||
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
|
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
|
||||||
- Fix vulnerability that made it possible to enumerate private projects belonging to group
|
- Fix vulnerability that made it possible to enumerate private projects belonging to group
|
||||||
|
@ -117,7 +122,6 @@ v 8.6.6
|
||||||
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
|
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
|
||||||
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
|
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
|
||||||
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
|
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
|
||||||
- Project switcher uses new dropdown styling
|
|
||||||
- Issuable header is consistent between issues and merge requests
|
- Issuable header is consistent between issues and merge requests
|
||||||
- Improved spacing in issuable header on mobile
|
- Improved spacing in issuable header on mobile
|
||||||
|
|
||||||
|
@ -249,6 +253,9 @@ v 8.6.0
|
||||||
- Trigger a todo for mentions on commits page
|
- Trigger a todo for mentions on commits page
|
||||||
- Let project owners and admins soft delete issues and merge requests
|
- Let project owners and admins soft delete issues and merge requests
|
||||||
|
|
||||||
|
v 8.5.11
|
||||||
|
- Fix persistent XSS vulnerability in `commit_person_link` helper
|
||||||
|
|
||||||
v 8.5.10
|
v 8.5.10
|
||||||
- Fix a 2FA authentication spoofing vulnerability.
|
- Fix a 2FA authentication spoofing vulnerability.
|
||||||
|
|
||||||
|
@ -396,6 +403,9 @@ v 8.5.0
|
||||||
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
|
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
|
||||||
- Add Todos
|
- Add Todos
|
||||||
|
|
||||||
|
v 8.4.9
|
||||||
|
- Fix persistent XSS vulnerability in `commit_person_link` helper
|
||||||
|
|
||||||
v 8.4.8
|
v 8.4.8
|
||||||
- Fix a 2FA authentication spoofing vulnerability.
|
- Fix a 2FA authentication spoofing vulnerability.
|
||||||
|
|
||||||
|
@ -518,6 +528,9 @@ v 8.4.0
|
||||||
- Add IP check against DNSBLs at account sign-up
|
- Add IP check against DNSBLs at account sign-up
|
||||||
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
|
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
|
||||||
|
|
||||||
|
v 8.3.8
|
||||||
|
- Fix persistent XSS vulnerability in `commit_person_link` helper
|
||||||
|
|
||||||
v 8.3.7
|
v 8.3.7
|
||||||
- Fix a 2FA authentication spoofing vulnerability.
|
- Fix a 2FA authentication spoofing vulnerability.
|
||||||
|
|
||||||
|
|
|
@ -323,6 +323,7 @@ request is as follows:
|
||||||
[shell command guidelines](doc/development/shell_commands.md)
|
[shell command guidelines](doc/development/shell_commands.md)
|
||||||
1. If your code creates new files on disk please read the
|
1. If your code creates new files on disk please read the
|
||||||
[shared files guidelines](doc/development/shared_files.md).
|
[shared files guidelines](doc/development/shared_files.md).
|
||||||
|
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
|
||||||
|
|
||||||
The **official merge window** is in the beginning of the month from the 1st to
|
The **official merge window** is in the beginning of the month from the 1st to
|
||||||
the 7th day of the month. This is the best time to submit an MR and get
|
the 7th day of the month. This is the best time to submit an MR and get
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
#= require_tree .
|
#= require_tree .
|
||||||
#= require fuzzaldrin-plus
|
#= require fuzzaldrin-plus
|
||||||
#= require cropper
|
#= require cropper
|
||||||
|
#= require raven
|
||||||
|
|
||||||
window.slugify = (text) ->
|
window.slugify = (text) ->
|
||||||
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
||||||
|
|
|
@ -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
|
||||||
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'
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
class @DueDateSelect
|
||||||
|
constructor: ->
|
||||||
|
$loading = $('.js-issuable-update .due_date')
|
||||||
|
.find('.block-loading')
|
||||||
|
.hide()
|
||||||
|
|
||||||
|
$('.js-due-date-select').each (i, dropdown) ->
|
||||||
|
$dropdown = $(dropdown)
|
||||||
|
$dropdownParent = $dropdown.closest('.dropdown')
|
||||||
|
$datePicker = $dropdownParent.find('.js-due-date-calendar')
|
||||||
|
$block = $dropdown.closest('.block')
|
||||||
|
$selectbox = $dropdown.closest('.selectbox')
|
||||||
|
$value = $block.find('.value')
|
||||||
|
$sidebarValue = $('.js-due-date-sidebar-value', $block)
|
||||||
|
|
||||||
|
fieldName = $dropdown.data('field-name')
|
||||||
|
abilityName = $dropdown.data('ability-name')
|
||||||
|
issueUpdateURL = $dropdown.data('issue-update')
|
||||||
|
|
||||||
|
$dropdown.glDropdown(
|
||||||
|
hidden: ->
|
||||||
|
$selectbox.hide()
|
||||||
|
$value.removeAttr('style')
|
||||||
|
)
|
||||||
|
|
||||||
|
addDueDate = ->
|
||||||
|
# Create the post date
|
||||||
|
value = $("input[name='#{fieldName}']").val()
|
||||||
|
date = new Date value.replace(new RegExp('-', 'g'), ',')
|
||||||
|
mediumDate = $.datepicker.formatDate 'M d, yy', date
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data[abilityName] = {}
|
||||||
|
data[abilityName].due_date = value
|
||||||
|
|
||||||
|
$.ajax(
|
||||||
|
type: 'PUT'
|
||||||
|
url: issueUpdateURL
|
||||||
|
data: data
|
||||||
|
beforeSend: ->
|
||||||
|
$loading.fadeIn()
|
||||||
|
$dropdown.trigger('loading.gl.dropdown')
|
||||||
|
$selectbox.hide()
|
||||||
|
$value.removeAttr('style')
|
||||||
|
|
||||||
|
$value.html(mediumDate)
|
||||||
|
$sidebarValue.html(mediumDate)
|
||||||
|
).done (data) ->
|
||||||
|
$dropdown.trigger('loaded.gl.dropdown')
|
||||||
|
$dropdown.dropdown('toggle')
|
||||||
|
$loading.fadeOut()
|
||||||
|
|
||||||
|
$datePicker.datepicker(
|
||||||
|
dateFormat: 'yy-mm-dd',
|
||||||
|
defaultDate: $("input[name='#{fieldName}']").val()
|
||||||
|
altField: "input[name='#{fieldName}']"
|
||||||
|
onSelect: ->
|
||||||
|
addDueDate()
|
||||||
|
)
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.off 'click', '.ui-datepicker-header a'
|
||||||
|
.on 'click', '.ui-datepicker-header a', (e) ->
|
||||||
|
e.stopImmediatePropagation()
|
|
@ -389,13 +389,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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
@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
|
|
@ -33,7 +33,6 @@ class @IssuableContext
|
||||||
$block.find('.dropdown-menu-toggle').trigger 'click'
|
$block.find('.dropdown-menu-toggle').trigger 'click'
|
||||||
, 0
|
, 0
|
||||||
|
|
||||||
|
|
||||||
$(".right-sidebar").niceScroll()
|
$(".right-sidebar").niceScroll()
|
||||||
|
|
||||||
initParticipants: ->
|
initParticipants: ->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@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 @@
|
||||||
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 @@
|
||||||
|
|
||||||
$(".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
|
||||||
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
|
||||||
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')
|
||||||
$newLabelError = $('.js-label-error')
|
$newLabelError = $('.js-label-error')
|
||||||
|
@ -151,7 +152,7 @@ class @LabelsSelect
|
||||||
.find('a')
|
.find('a')
|
||||||
.each((i) ->
|
.each((i) ->
|
||||||
setTimeout(=>
|
setTimeout(=>
|
||||||
glAnimate($(@), 'pulse')
|
gl.animate.animate($(@), 'pulse')
|
||||||
,200 * i
|
,200 * i
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -180,16 +181,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>
|
||||||
|
@ -199,37 +205,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?
|
||||||
selected.title
|
if selected_labels.length > 1
|
||||||
|
"#{selected.title} +#{selected_labels.length - 1} more"
|
||||||
|
else
|
||||||
|
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'
|
||||||
saveLabelData()
|
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()
|
||||||
|
|
||||||
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)
|
||||||
selectedLabel = label.title
|
if not $dropdown.hasClass 'js-multiselect'
|
||||||
|
selectedLabel = label.title
|
||||||
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
|
||||||
|
|
|
@ -1,13 +1,39 @@
|
||||||
((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')
|
||||||
|
if done?
|
||||||
|
done()
|
||||||
|
if options?.cssEnd?
|
||||||
|
$el.css(options.cssEnd)
|
||||||
return
|
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
|
|
@ -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 @@
|
||||||
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
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
#
|
|
||||||
# * 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
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
@raven =
|
||||||
|
init: ->
|
||||||
|
if gon.sentry_dsn?
|
||||||
|
Raven.config(gon.sentry_dsn, {
|
||||||
|
includePaths: [/gon.relative_url_root/]
|
||||||
|
ignoreErrors: [
|
||||||
|
# Random plugins/extensions
|
||||||
|
'top.GLOBALS',
|
||||||
|
# See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
|
||||||
|
'originalCreateNotification',
|
||||||
|
'canvas.contentDocument',
|
||||||
|
'MyApp_RemoveAllHighlights',
|
||||||
|
'http://tt.epicplay.com',
|
||||||
|
'Can\'t find variable: ZiteReader',
|
||||||
|
'jigsaw is not defined',
|
||||||
|
'ComboSearch is not defined',
|
||||||
|
'http://loading.retry.widdit.com/',
|
||||||
|
'atomicFindClose',
|
||||||
|
# ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
|
||||||
|
# reduce this. (thanks @acdha)
|
||||||
|
# See http://stackoverflow.com/questions/4113268
|
||||||
|
'bmi_SafeAddOnload',
|
||||||
|
'EBCallBackMessageReceived',
|
||||||
|
# See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
|
||||||
|
'conduitPage'
|
||||||
|
],
|
||||||
|
ignoreUrls: [
|
||||||
|
# Chrome extensions
|
||||||
|
/extensions\//i,
|
||||||
|
/^chrome:\/\//i,
|
||||||
|
# Other plugins
|
||||||
|
/127\.0\.0\.1:4001\/isrunning/i, # Cacaoweb
|
||||||
|
/webappstoolbarba\.texthelp\.com\//i,
|
||||||
|
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i
|
||||||
|
]
|
||||||
|
}).install()
|
||||||
|
|
||||||
|
if gon.current_user_id
|
||||||
|
Raven.setUserContext({
|
||||||
|
id: gon.current_user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
raven.init()
|
|
@ -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
|
||||||
|
|
|
@ -248,7 +248,7 @@
|
||||||
|
|
||||||
.dropdown-title {
|
.dropdown-title {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 25px 15px;
|
padding: 0 25px 10px;
|
||||||
margin: 0 10px 10px;
|
margin: 0 10px 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
@ -278,7 +278,7 @@
|
||||||
right: 5px;
|
right: 5px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
top: -1px;
|
top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-back {
|
.dropdown-menu-back {
|
||||||
|
@ -358,6 +358,13 @@
|
||||||
border-top: 1px solid $dropdown-divider-color;
|
border-top: 1px solid $dropdown-divider-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-due-date-footer {
|
||||||
|
padding-top: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-footer-list {
|
.dropdown-footer-list {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
|
@ -395,3 +402,122 @@
|
||||||
height: 15px;
|
height: 15px;
|
||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-due-date {
|
||||||
|
.dropdown-content {
|
||||||
|
max-height: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-widget {
|
||||||
|
table {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ui-datepicker-inline {
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-datepicker-header {
|
||||||
|
padding: 0 8px 10px;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.ui-icon {
|
||||||
|
background: none;
|
||||||
|
font-size: 20px;
|
||||||
|
text-indent: 0;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
color: $dropdown-title-btn-color;
|
||||||
|
font: normal normal normal 14px/1 FontAwesome;
|
||||||
|
font-size: inherit;
|
||||||
|
text-rendering: auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-state-active,
|
||||||
|
.ui-state-hover {
|
||||||
|
color: $md-link-color;
|
||||||
|
background-color: $calendar-hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-datepicker-prev,
|
||||||
|
.ui-datepicker-next {
|
||||||
|
top: 0;
|
||||||
|
height: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.ui-icon:before {
|
||||||
|
color: $md-link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-datepicker-prev {
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
.ui-icon:before {
|
||||||
|
content: '\f104';
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-datepicker-next {
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
.ui-icon:before {
|
||||||
|
content: '\f105';
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid $calendar-border-color;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
line-height: 17px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-datepicker-title {
|
||||||
|
color: $gl-gray;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 2px 0;
|
||||||
|
color: $calendar-header-color;
|
||||||
|
font-weight: normal;
|
||||||
|
text-transform: lowercase;
|
||||||
|
border-top: 1px solid $calendar-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-datepicker-unselectable {
|
||||||
|
background-color: $calendar-unselectable-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -90,3 +90,12 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md {
|
||||||
|
&.md-preview-holder {
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
|
||||||
$note-toolbar-color: #959494;
|
$note-toolbar-color: #959494;
|
||||||
|
|
||||||
$zen-control-hover-color: #111;
|
$zen-control-hover-color: #111;
|
||||||
|
|
||||||
|
$calendar-header-color: #b8b8b8;
|
||||||
|
$calendar-hover-bg: #ecf3fe;
|
||||||
|
$calendar-border-color: rgba(#000, .1);
|
||||||
|
$calendar-unselectable-bg: #faf9f9;
|
||||||
|
|
|
@ -36,4 +36,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wiki {
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #333;
|
color: #333;
|
||||||
border-radius: 0 0 3px 3px;
|
border-radius: 0 0 3px 3px;
|
||||||
|
-webkit-overflow-scrolling: auto;
|
||||||
|
|
||||||
.unfold {
|
.unfold {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -86,7 +87,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
white-space: pre;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,7 +336,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-file .line_content {
|
.diff-file .line_content {
|
||||||
white-space: pre;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diff-wrap-lines .line_content {
|
.diff-wrap-lines .line_content {
|
||||||
|
|
|
@ -59,8 +59,10 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
margin: -$gl-padding+1;
|
margin: -$gl-padding+1;
|
||||||
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,7 @@
|
||||||
top: 58px;
|
top: 58px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
transition: width .3s;
|
transition: width .3s;
|
||||||
background: $gray-light;
|
background: $gray-light;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
@ -241,7 +242,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.issuable-pager {
|
||||||
background: $gray-normal;
|
background: $gray-normal;
|
||||||
border: 1px solid $border-gray-normal;
|
border: 1px solid $border-gray-normal;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -254,7 +255,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a:not(.btn) {
|
a:not(.issuable-pager) {
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $md-link-color;
|
color: $md-link-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -84,18 +84,6 @@
|
||||||
border-color: $gl-success;
|
border-color: $gl-success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
code {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
code {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,16 +81,8 @@ ul.notes {
|
||||||
@include md-typography;
|
@include md-typography;
|
||||||
|
|
||||||
// On diffs code should wrap nicely and not overflow
|
// On diffs code should wrap nicely and not overflow
|
||||||
p {
|
code {
|
||||||
code {
|
white-space: pre-wrap;
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
code {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset ul style types since we're nested inside a ul already
|
// Reset ul style types since we're nested inside a ul already
|
||||||
|
@ -137,7 +129,7 @@ ul.notes {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.line_content {
|
.line_content {
|
||||||
white-space: pre;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,11 +163,6 @@ ul.notes {
|
||||||
|
|
||||||
&.parallel {
|
&.parallel {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
|
|
||||||
.code,
|
|
||||||
code {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes {
|
.notes {
|
||||||
|
@ -308,7 +295,7 @@ ul.notes {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: $gl-link-color;
|
color: $gl-link-color;
|
||||||
margin-left: -60px;
|
margin-left: -56px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
|
|
@ -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
|
||||||
|
@ -191,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
def issue_params
|
def issue_params
|
||||||
params.require(:issue).permit(
|
params.require(:issue).permit(
|
||||||
:title, :assignee_id, :position, :description, :confidential,
|
:title, :assignee_id, :position, :description, :confidential,
|
||||||
:milestone_id, :state_event, :task_num, label_ids: []
|
:milestone_id, :due_date, :state_event, :task_num, label_ids: []
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -39,6 +39,7 @@ class IssuableFinder
|
||||||
items = by_assignee(items)
|
items = by_assignee(items)
|
||||||
items = by_author(items)
|
items = by_author(items)
|
||||||
items = by_label(items)
|
items = by_label(items)
|
||||||
|
items = by_due_date(items)
|
||||||
sort(items)
|
sort(items)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -117,7 +118,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,9 +279,47 @@ class IssuableFinder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def by_due_date(items)
|
||||||
|
if due_date?
|
||||||
|
if filter_by_no_due_date?
|
||||||
|
items = items.without_due_date
|
||||||
|
elsif filter_by_overdue?
|
||||||
|
items = items.due_before(Date.today)
|
||||||
|
elsif filter_by_due_this_week?
|
||||||
|
items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
|
||||||
|
elsif filter_by_due_this_month?
|
||||||
|
items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
items
|
items
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_by_no_due_date?
|
||||||
|
due_date? && params[:due_date] == Issue::NoDueDate.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_overdue?
|
||||||
|
due_date? && params[:due_date] == Issue::Overdue.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_due_this_week?
|
||||||
|
due_date? && params[:due_date] == Issue::DueThisWeek.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_due_this_month?
|
||||||
|
due_date? && params[:due_date] == Issue::DueThisMonth.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def due_date?
|
||||||
|
params[:due_date].present? && klass.column_names.include?('due_date')
|
||||||
|
end
|
||||||
|
|
||||||
def label_names
|
def label_names
|
||||||
params[:label_name].split(',')
|
params[:label_name].split(',')
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,7 @@ module IssuesHelper
|
||||||
class: "icon emoji-icon emoji-#{unicode}",
|
class: "icon emoji-icon emoji-#{unicode}",
|
||||||
title: name,
|
title: name,
|
||||||
data: data
|
data: data
|
||||||
else
|
else
|
||||||
# Emoji icons displayed separately, used for the awards already given
|
# Emoji icons displayed separately, used for the awards already given
|
||||||
# to an issue or merge request.
|
# to an issue or merge request.
|
||||||
content_tag :img, "",
|
content_tag :img, "",
|
||||||
|
@ -172,6 +172,18 @@ module IssuesHelper
|
||||||
end.to_h
|
end.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def due_date_options
|
||||||
|
options = [
|
||||||
|
Issue::AnyDueDate,
|
||||||
|
Issue::NoDueDate,
|
||||||
|
Issue::DueThisWeek,
|
||||||
|
Issue::DueThisMonth,
|
||||||
|
Issue::Overdue
|
||||||
|
]
|
||||||
|
|
||||||
|
options_from_collection_for_select(options, 'name', 'title', params[:due_date])
|
||||||
|
end
|
||||||
|
|
||||||
# Required for Banzai::Filter::IssueReferenceFilter
|
# Required for Banzai::Filter::IssueReferenceFilter
|
||||||
module_function :url_for_issue
|
module_function :url_for_issue
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,8 @@ module SortingHelper
|
||||||
sort_value_oldest_created => sort_title_oldest_created,
|
sort_value_oldest_created => sort_title_oldest_created,
|
||||||
sort_value_milestone_soon => sort_title_milestone_soon,
|
sort_value_milestone_soon => sort_title_milestone_soon,
|
||||||
sort_value_milestone_later => sort_title_milestone_later,
|
sort_value_milestone_later => sort_title_milestone_later,
|
||||||
|
sort_value_due_date_soon => sort_title_due_date_soon,
|
||||||
|
sort_value_due_date_later => sort_title_due_date_later,
|
||||||
sort_value_largest_repo => sort_title_largest_repo,
|
sort_value_largest_repo => sort_title_largest_repo,
|
||||||
sort_value_recently_signin => sort_title_recently_signin,
|
sort_value_recently_signin => sort_title_recently_signin,
|
||||||
sort_value_oldest_signin => sort_title_oldest_signin,
|
sort_value_oldest_signin => sort_title_oldest_signin,
|
||||||
|
@ -50,6 +52,14 @@ module SortingHelper
|
||||||
'Milestone due later'
|
'Milestone due later'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sort_title_due_date_soon
|
||||||
|
'Due soon'
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_title_due_date_later
|
||||||
|
'Due later'
|
||||||
|
end
|
||||||
|
|
||||||
def sort_title_name
|
def sort_title_name
|
||||||
'Name'
|
'Name'
|
||||||
end
|
end
|
||||||
|
@ -98,6 +108,14 @@ module SortingHelper
|
||||||
'milestone_due_desc'
|
'milestone_due_desc'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sort_value_due_date_soon
|
||||||
|
'due_date_asc'
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_value_due_date_later
|
||||||
|
'due_date_desc'
|
||||||
|
end
|
||||||
|
|
||||||
def sort_value_name
|
def sort_value_name
|
||||||
'name_asc'
|
'name_asc'
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,13 @@ class Issue < ActiveRecord::Base
|
||||||
include Sortable
|
include Sortable
|
||||||
include Taskable
|
include Taskable
|
||||||
|
|
||||||
|
DueDateStruct = Struct.new(:title, :name).freeze
|
||||||
|
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
|
||||||
|
AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
|
||||||
|
Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
|
||||||
|
DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
|
||||||
|
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
|
||||||
|
|
||||||
ActsAsTaggableOn.strict_case_match = true
|
ActsAsTaggableOn.strict_case_match = true
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
|
@ -39,6 +46,13 @@ class Issue < ActiveRecord::Base
|
||||||
scope :open_for, ->(user) { opened.assigned_to(user) }
|
scope :open_for, ->(user) { opened.assigned_to(user) }
|
||||||
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
|
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
|
||||||
|
|
||||||
|
scope :without_due_date, -> { where(due_date: nil) }
|
||||||
|
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
|
||||||
|
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
|
||||||
|
|
||||||
|
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
|
||||||
|
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
|
||||||
|
|
||||||
state_machine :state, initial: :opened do
|
state_machine :state, initial: :opened do
|
||||||
event :close do
|
event :close do
|
||||||
transition [:reopened, :opened] => :closed
|
transition [:reopened, :opened] => :closed
|
||||||
|
@ -82,6 +96,15 @@ class Issue < ActiveRecord::Base
|
||||||
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
|
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.sort(method)
|
||||||
|
case method.to_s
|
||||||
|
when 'due_date_asc' then order_due_date_asc
|
||||||
|
when 'due_date_desc' then order_due_date_desc
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def to_reference(from_project = nil)
|
def to_reference(from_project = nil)
|
||||||
reference = "#{self.class.reference_prefix}#{iid}"
|
reference = "#{self.class.reference_prefix}#{iid}"
|
||||||
|
|
||||||
|
@ -169,4 +192,8 @@ class Issue < ActiveRecord::Base
|
||||||
self.related_branches(current_user).empty? &&
|
self.related_branches(current_user).empty? &&
|
||||||
self.closed_by_merge_requests(current_user).empty?
|
self.closed_by_merge_requests(current_user).empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def overdue?
|
||||||
|
due_date.try(:past?) || false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -9,4 +9,7 @@
|
||||||
= spinner
|
= spinner
|
||||||
|
|
||||||
:javascript
|
:javascript
|
||||||
new Activities();
|
var activity = new Activities();
|
||||||
|
$(document).on('page:restore', function (event) {
|
||||||
|
activity.reloadActivities()
|
||||||
|
})
|
||||||
|
|
|
@ -48,6 +48,11 @@
|
||||||
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
|
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
|
||||||
= icon('clock-o')
|
= icon('clock-o')
|
||||||
= issue.milestone.title
|
= issue.milestone.title
|
||||||
|
- if issue.due_date
|
||||||
|
%span{class: "#{'cred' if issue.overdue?}"}
|
||||||
|
|
||||||
|
= icon('calendar')
|
||||||
|
= issue.due_date.to_s(:medium)
|
||||||
- if issue.labels.any?
|
- if issue.labels.any?
|
||||||
|
|
||||||
- issue.labels.each do |label|
|
- issue.labels.each do |label|
|
||||||
|
|
|
@ -24,9 +24,9 @@
|
||||||
%li{ class: issue_button_visibility(@merge_request, false) }
|
%li{ class: issue_button_visibility(@merge_request, false) }
|
||||||
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
|
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
|
||||||
%li
|
%li
|
||||||
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request'
|
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
|
||||||
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
|
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
|
||||||
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
|
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
|
||||||
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do
|
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do
|
||||||
= icon('pencil-square-o')
|
= icon('pencil-square-o')
|
||||||
Edit
|
Edit
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
%span.label-name
|
%span.label-name
|
||||||
= link_to_label(label, tooltip: false)
|
= link_to_label(label, tooltip: false)
|
||||||
%span.prepend-left-10
|
%span.prepend-left-10
|
||||||
= markdown(label.description, pipeline: :single_line)
|
= markdown(label.description, pipeline: :single_line)
|
|
@ -0,0 +1,3 @@
|
||||||
|
- labels.each do |label|
|
||||||
|
%span.label-row
|
||||||
|
= link_to_label(label, tooltip: false)
|
|
@ -20,6 +20,11 @@
|
||||||
= sort_title_milestone_soon
|
= sort_title_milestone_soon
|
||||||
= link_to page_filter_path(sort: sort_value_milestone_later) do
|
= link_to page_filter_path(sort: sort_value_milestone_later) do
|
||||||
= sort_title_milestone_later
|
= sort_title_milestone_later
|
||||||
|
- if controller.controller_name == 'issues' || controller.action_name == 'issues'
|
||||||
|
= link_to page_filter_path(sort: sort_value_due_date_soon) do
|
||||||
|
= sort_title_due_date_soon
|
||||||
|
= link_to page_filter_path(sort: sort_value_due_date_later) do
|
||||||
|
= sort_title_due_date_later
|
||||||
= link_to page_filter_path(sort: sort_value_upvotes) do
|
= link_to page_filter_path(sort: sort_value_upvotes) do
|
||||||
= sort_title_upvotes
|
= sort_title_upvotes
|
||||||
= link_to page_filter_path(sort: sort_value_downvotes) do
|
= link_to page_filter_path(sort: sort_value_downvotes) do
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
.filter-item.inline.labels-filter
|
.filter-item.inline.labels-filter
|
||||||
= render "shared/issuable/label_dropdown"
|
= render "shared/issuable/label_dropdown"
|
||||||
|
|
||||||
.pull-right
|
.pull-right
|
||||||
= render 'shared/sort_dropdown'
|
= render 'shared/sort_dropdown'
|
||||||
|
|
||||||
|
@ -46,9 +47,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();
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
- 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
|
||||||
= render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" }
|
= render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" }
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -10,14 +10,14 @@
|
||||||
= sidebar_gutter_toggle_icon
|
= sidebar_gutter_toggle_icon
|
||||||
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
|
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
|
||||||
- if prev_issuable = prev_issuable_for(issuable)
|
- if prev_issuable = prev_issuable_for(issuable)
|
||||||
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
|
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager'
|
||||||
- else
|
- else
|
||||||
%a.btn.btn-default.disabled{href: '#'}
|
%a.btn.btn-default.issuable-pager.disabled{href: '#'}
|
||||||
Prev
|
Prev
|
||||||
- if next_issuable = next_issuable_for(issuable)
|
- if next_issuable = next_issuable_for(issuable)
|
||||||
= link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn'
|
= link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager'
|
||||||
- else
|
- else
|
||||||
%a.btn.btn-default.disabled{href: '#'}
|
%a.btn.btn-default.issuable-pager.disabled{href: '#'}
|
||||||
Next
|
Next
|
||||||
|
|
||||||
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
|
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
- if issuable.milestone
|
- if issuable.milestone
|
||||||
= issuable.milestone.title
|
= issuable.milestone.title
|
||||||
- else
|
- else
|
||||||
No
|
None
|
||||||
.title.hide-collapsed
|
.title.hide-collapsed
|
||||||
Milestone
|
Milestone
|
||||||
= icon('spinner spin', class: 'block-loading')
|
= icon('spinner spin', class: 'block-loading')
|
||||||
|
@ -75,6 +75,34 @@
|
||||||
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
|
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
|
||||||
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
|
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
|
||||||
|
|
||||||
|
- if issuable.has_attribute?(:due_date)
|
||||||
|
.block.due_date
|
||||||
|
.sidebar-collapsed-icon
|
||||||
|
= icon('calendar')
|
||||||
|
%span.js-due-date-sidebar-value
|
||||||
|
= issuable.due_date.try(:to_s, :medium) || 'None'
|
||||||
|
.title.hide-collapsed
|
||||||
|
Due date
|
||||||
|
= icon('spinner spin', class: 'block-loading')
|
||||||
|
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
|
||||||
|
= link_to 'Edit', '#', class: 'edit-link pull-right'
|
||||||
|
.value.bold.hide-collapsed
|
||||||
|
- if issuable.due_date
|
||||||
|
= issuable.due_date.to_s(:medium)
|
||||||
|
- else
|
||||||
|
.light None
|
||||||
|
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
|
||||||
|
.selectbox.hide-collapsed
|
||||||
|
= f.hidden_field :due_date, value: issuable.due_date
|
||||||
|
.dropdown
|
||||||
|
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
|
||||||
|
%span.dropdown-toggle-text Due date
|
||||||
|
= icon('chevron-down')
|
||||||
|
.dropdown-menu.dropdown-menu-due-date
|
||||||
|
= dropdown_title('Due date')
|
||||||
|
= dropdown_content do
|
||||||
|
.js-due-date-calendar
|
||||||
|
|
||||||
- if issuable.project.labels.any?
|
- if issuable.project.labels.any?
|
||||||
.block.labels
|
.block.labels
|
||||||
.sidebar-collapsed-icon
|
.sidebar-collapsed-icon
|
||||||
|
@ -139,3 +167,4 @@
|
||||||
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
|
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
|
||||||
new Subscription('.subscription')
|
new Subscription('.subscription')
|
||||||
new Sidebar();
|
new Sidebar();
|
||||||
|
new DueDateSelect();
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddDueDateToIssues < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :issues, :due_date, :date
|
||||||
|
add_index :issues, :due_date
|
||||||
|
end
|
||||||
|
end
|
|
@ -422,6 +422,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
|
||||||
t.integer "moved_to_id"
|
t.integer "moved_to_id"
|
||||||
t.boolean "confidential", default: false
|
t.boolean "confidential", default: false
|
||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
|
t.date "due_date"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
|
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
|
||||||
|
@ -431,6 +432,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
|
||||||
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
|
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
|
||||||
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
|
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
|
||||||
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
|
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
|
||||||
|
add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
|
||||||
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
|
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
|
||||||
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
|
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
|
||||||
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
|
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
|
||||||
|
|
|
@ -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
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ module API
|
||||||
end
|
end
|
||||||
|
|
||||||
post "/allowed" do
|
post "/allowed" do
|
||||||
Gitlab::Metrics.tag_transaction('action', 'Grape#/internal/allowed')
|
Gitlab::Metrics.action = 'Grape#/internal/allowed'
|
||||||
|
|
||||||
status 200
|
status 200
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ module Gitlab
|
||||||
gon.max_file_size = current_application_settings.max_attachment_size
|
gon.max_file_size = current_application_settings.max_attachment_size
|
||||||
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
|
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
|
||||||
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
|
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
|
||||||
|
gon.sentry_dsn = ApplicationSetting.current.sentry_dsn if Rails.env.production?
|
||||||
|
|
||||||
if current_user
|
if current_user
|
||||||
gon.current_user_id = current_user.id
|
gon.current_user_id = current_user.id
|
||||||
|
|
|
@ -115,6 +115,15 @@ module Gitlab
|
||||||
trans.add_tag(name, value) if trans
|
trans.add_tag(name, value) if trans
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Sets the action of the current transaction (if any)
|
||||||
|
#
|
||||||
|
# action - The name of the action.
|
||||||
|
def self.action=(action)
|
||||||
|
trans = current_transaction
|
||||||
|
|
||||||
|
trans.action = action if trans
|
||||||
|
end
|
||||||
|
|
||||||
# When enabled this should be set before being used as the usual pattern
|
# When enabled this should be set before being used as the usual pattern
|
||||||
# "@foo ||= bar" is _not_ thread-safe.
|
# "@foo ||= bar" is _not_ thread-safe.
|
||||||
if enabled?
|
if enabled?
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -112,7 +112,7 @@ describe 'Issues', feature: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'filter issue' do
|
describe 'filter issue' do
|
||||||
titles = ['foo','bar','baz']
|
titles = %w[foo bar baz]
|
||||||
titles.each_with_index do |title, index|
|
titles.each_with_index do |title, index|
|
||||||
let!(title.to_sym) do
|
let!(title.to_sym) do
|
||||||
create(:issue, title: title,
|
create(:issue, title: title,
|
||||||
|
@ -153,8 +153,94 @@ describe 'Issues', feature: true do
|
||||||
expect(first_issue).to include('baz')
|
expect(first_issue).to include('baz')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'sorting by due date' do
|
||||||
|
before do
|
||||||
|
foo.update(due_date: 1.day.from_now)
|
||||||
|
bar.update(due_date: 6.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts by recently due date' do
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_soon)
|
||||||
|
|
||||||
|
expect(first_issue).to include('foo')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts by least recently due date' do
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
|
||||||
|
|
||||||
|
expect(first_issue).to include('bar')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts by least recently due date by excluding nil due dates' do
|
||||||
|
bar.update(due_date: nil)
|
||||||
|
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
|
||||||
|
|
||||||
|
expect(first_issue).to include('foo')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'filtering by due date' do
|
||||||
|
before do
|
||||||
|
foo.update(due_date: 1.day.from_now)
|
||||||
|
bar.update(due_date: 6.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by none' do
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, due_date: Issue::NoDueDate.name)
|
||||||
|
|
||||||
|
expect(page).not_to have_content('foo')
|
||||||
|
expect(page).not_to have_content('bar')
|
||||||
|
expect(page).to have_content('baz')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by any' do
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, due_date: Issue::AnyDueDate.name)
|
||||||
|
|
||||||
|
expect(page).to have_content('foo')
|
||||||
|
expect(page).to have_content('bar')
|
||||||
|
expect(page).to have_content('baz')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by due this week' do
|
||||||
|
foo.update(due_date: Date.today.beginning_of_week + 2.days)
|
||||||
|
bar.update(due_date: Date.today.end_of_week)
|
||||||
|
baz.update(due_date: Date.today - 8.days)
|
||||||
|
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisWeek.name)
|
||||||
|
|
||||||
|
expect(page).to have_content('foo')
|
||||||
|
expect(page).to have_content('bar')
|
||||||
|
expect(page).not_to have_content('baz')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by due this month' do
|
||||||
|
foo.update(due_date: Date.today.beginning_of_month + 2.days)
|
||||||
|
bar.update(due_date: Date.today.end_of_month)
|
||||||
|
baz.update(due_date: Date.today - 50.days)
|
||||||
|
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisMonth.name)
|
||||||
|
|
||||||
|
expect(page).to have_content('foo')
|
||||||
|
expect(page).to have_content('bar')
|
||||||
|
expect(page).not_to have_content('baz')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters by overdue' do
|
||||||
|
foo.update(due_date: Date.today + 2.days)
|
||||||
|
bar.update(due_date: Date.today + 20.days)
|
||||||
|
baz.update(due_date: Date.yesterday)
|
||||||
|
|
||||||
|
visit namespace_project_issues_path(project.namespace, project, due_date: Issue::Overdue.name)
|
||||||
|
|
||||||
|
expect(page).not_to have_content('foo')
|
||||||
|
expect(page).not_to have_content('bar')
|
||||||
|
expect(page).to have_content('baz')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'sorting by milestone' do
|
describe 'sorting by milestone' do
|
||||||
before :each do
|
before do
|
||||||
foo.milestone = newer_due_milestone
|
foo.milestone = newer_due_milestone
|
||||||
foo.save
|
foo.save
|
||||||
bar.milestone = later_due_milestone
|
bar.milestone = later_due_milestone
|
||||||
|
@ -177,7 +263,7 @@ describe 'Issues', feature: true do
|
||||||
describe 'combine filter and sort' do
|
describe 'combine filter and sort' do
|
||||||
let(:user2) { create(:user) }
|
let(:user2) { create(:user) }
|
||||||
|
|
||||||
before :each do
|
before do
|
||||||
foo.assignee = user2
|
foo.assignee = user2
|
||||||
foo.save
|
foo.save
|
||||||
bar.assignee = user2
|
bar.assignee = user2
|
||||||
|
@ -224,7 +310,7 @@ describe 'Issues', feature: true do
|
||||||
|
|
||||||
let(:guest) { create(:user) }
|
let(:guest) { create(:user) }
|
||||||
|
|
||||||
before :each do
|
before do
|
||||||
project.team << [[guest], :guest]
|
project.team << [[guest], :guest]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -267,7 +353,7 @@ describe 'Issues', feature: true do
|
||||||
context 'by unauthorized user' do
|
context 'by unauthorized user' do
|
||||||
let(:guest) { create(:user) }
|
let(:guest) { create(:user) }
|
||||||
|
|
||||||
before :each do
|
before do
|
||||||
project.team << [guest, :guest]
|
project.team << [guest, :guest]
|
||||||
issue.milestone = milestone
|
issue.milestone = milestone
|
||||||
issue.save
|
issue.save
|
||||||
|
@ -285,7 +371,7 @@ describe 'Issues', feature: true do
|
||||||
describe 'removing assignee' do
|
describe 'removing assignee' do
|
||||||
let(:user2) { create(:user) }
|
let(:user2) { create(:user) }
|
||||||
|
|
||||||
before :each do
|
before do
|
||||||
issue.assignee = user2
|
issue.assignee = user2
|
||||||
issue.save
|
issue.save
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
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
|
||||||
|
|
|
@ -123,4 +123,28 @@ describe Gitlab::Metrics do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.action=' do
|
||||||
|
context 'without a transaction' do
|
||||||
|
it 'does nothing' do
|
||||||
|
expect_any_instance_of(Gitlab::Metrics::Transaction).
|
||||||
|
not_to receive(:action=)
|
||||||
|
|
||||||
|
Gitlab::Metrics.action = 'foo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a transaction' do
|
||||||
|
it 'sets the action of a transaction' do
|
||||||
|
trans = Gitlab::Metrics::Transaction.new
|
||||||
|
|
||||||
|
expect(Gitlab::Metrics).to receive(:current_transaction).
|
||||||
|
and_return(trans)
|
||||||
|
|
||||||
|
expect(trans).to receive(:action=).with('foo')
|
||||||
|
|
||||||
|
Gitlab::Metrics.action = 'foo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue