summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/awards_handler.coffee335
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/flash.js.coffee2
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee95
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js.coffee109
-rw-r--r--app/assets/javascripts/labels_select.js.coffee70
-rw-r--r--app/assets/javascripts/lib/emoji_aliases.js.coffee.erb2
-rw-r--r--app/assets/javascripts/notes.js.coffee2
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee13
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss11
-rw-r--r--app/assets/stylesheets/framework/mixins.scss8
-rw-r--r--app/assets/stylesheets/pages/awards.scss13
-rw-r--r--app/assets/stylesheets/pages/builds.scss7
-rw-r--r--app/assets/stylesheets/pages/search.scss2
14 files changed, 483 insertions, 189 deletions
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index bf95e06b4e5..766c653111a 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,201 +1,300 @@
class @AwardsHandler
- constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
- $('.js-add-award').on 'click', (event) =>
- event.stopPropagation()
- event.preventDefault()
- @showEmojiMenu()
+ constructor: ->
+
+ @aliases = emojiAliases()
+
+ $(document)
+ .off 'click', '.js-add-award'
+ .on 'click', '.js-add-award', (event) =>
+ event.stopPropagation()
+ event.preventDefault()
+
+ @showEmojiMenu $(event.currentTarget)
$('html').on 'click', (event) ->
- if !$(event.target).closest('.emoji-menu').length
+ unless $(event.target).closest('.emoji-menu').length
if $('.emoji-menu').is(':visible')
+ $('.js-add-award.is-active').removeClass 'is-active'
$('.emoji-menu').removeClass 'is-visible'
- $('.awards')
- .off 'click'
- .on 'click', '.js-emoji-btn', @handleClick
+ $(document)
+ .off 'click', '.js-emoji-btn'
+ .on 'click', '.js-emoji-btn', @handleClick
- @renderFrequentlyUsedBlock()
- handleClick: (e) ->
+ handleClick: (e) =>
+
e.preventDefault()
- emoji = $(this)
- .find('.icon')
- .data 'emoji'
- if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown'
- awardsHandler.addAward 'thumbsdown'
+ emoji = $(e.currentTarget).find('.icon').data 'emoji'
+ @getVotesBlock().addClass 'js-awards-block'
+ @addAward @getAwardUrl(), emoji
- else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
- awardsHandler.addAward 'thumbsup'
- awardsHandler.addAward emoji
+ showEmojiMenu: ($addBtn) ->
- $(this).trigger 'blur'
+ $menu = $('.emoji-menu')
- didUserClickEmoji: (that, emoji) ->
- if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title')
- $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
+ if $menu.length
+ $holder = $addBtn.closest('.js-award-holder')
- showEmojiMenu: ->
- if $('.emoji-menu').length
- if $('.emoji-menu').is '.is-visible'
- $('.emoji-menu').removeClass 'is-visible'
+ if $menu.is '.is-visible'
+ $addBtn.removeClass 'is-active'
+ $menu.removeClass 'is-visible'
$('#emoji_search').blur()
else
- $('.emoji-menu').addClass 'is-visible'
+ $addBtn.addClass 'is-active'
+ @positionMenu($menu, $addBtn)
+
+ $menu.addClass 'is-visible'
$('#emoji_search').focus()
else
- $('.js-add-award').addClass 'is-loading'
- $.get @getEmojisUrl, (response) =>
- $('.js-add-award').removeClass 'is-loading'
- $('.js-award-holder').append response
+ $addBtn.addClass 'is-loading is-active'
+ url = $addBtn.data 'award-menu-url'
+
+ @createEmojiMenu url, =>
+ $addBtn.removeClass 'is-loading'
+ $menu = $('.emoji-menu')
+ @positionMenu($menu, $addBtn)
+ @renderFrequentlyUsedBlock()
+
setTimeout =>
- $('.emoji-menu').addClass 'is-visible'
+ $menu.addClass 'is-visible'
$('#emoji_search').focus()
@setupSearch()
, 200
- addAward: (emoji) ->
- @postEmoji emoji, =>
- @addAwardToEmojiBar(emoji)
+
+ createEmojiMenu: (awardMenuUrl, callback) ->
+
+ $.get awardMenuUrl, (response) =>
+ $('body').append response
+ callback()
+
+
+ positionMenu: ($menu, $addBtn) ->
+ position = $addBtn.data('position')
+
+ # The menu could potentially be off-screen or in a hidden overflow element
+ # So we position the element absolute in the body
+ css =
+ top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
+
+ if position? and position is 'right'
+ css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
+ $menu.addClass 'is-aligned-right'
+ else
+ css.left = "#{$addBtn.offset().left}px"
+ $menu.removeClass 'is-aligned-right'
+
+ $menu.css(css)
+
+
+ addAward: (awardUrl, emoji, checkMutuality = yes) ->
+
+ emoji = @normilizeEmojiName(emoji)
+ @postEmoji awardUrl, emoji, =>
+ @addAwardToEmojiBar(emoji, checkMutuality)
+
+ $('.js-awards-block-current').removeClass 'js-awards-block-current'
$('.emoji-menu').removeClass 'is-visible'
- addAwardToEmojiBar: (emoji) ->
+
+ addAwardToEmojiBar: (emoji, checkForMutuality = yes) ->
+
+ @checkMutuality emoji if checkForMutuality
@addEmojiToFrequentlyUsedList(emoji)
- if @exist(emoji)
- if @isActive(emoji)
- @decrementCounter(emoji)
+ emoji = @normilizeEmojiName(emoji)
+ $emojiBtn = @findEmojiIcon(emoji).parent()
+
+ if $emojiBtn.length > 0
+ if @isActive($emojiBtn)
+ @decrementCounter($emojiBtn, emoji)
else
- counter = @findEmojiIcon(emoji).siblings('.js-counter')
+ counter = $emojiBtn.find('.js-counter')
counter.text(parseInt(counter.text()) + 1)
- counter.parent().addClass('active')
- @addMeToAuthorList(emoji)
+ $emojiBtn.addClass('active')
+ @addMeToUserList(emoji)
else
@createEmoji(emoji)
- exist: (emoji) ->
- @findEmojiIcon(emoji).length > 0
-
- isActive: (emoji) ->
- @findEmojiIcon(emoji).parent().hasClass('active')
-
- decrementCounter: (emoji) ->
- counter = @findEmojiIcon(emoji).siblings('.js-counter')
- emojiIcon = counter.parent()
- if parseInt(counter.text()) > 1
- counter.text(parseInt(counter.text()) - 1)
- emojiIcon.removeClass('active')
- @removeMeFromAuthorList(emoji)
- else if emoji == 'thumbsup' || emoji == 'thumbsdown'
- emojiIcon.tooltip('destroy')
- counter.text(0)
- emojiIcon.removeClass('active')
- @removeMeFromAuthorList(emoji)
+
+ getVotesBlock: -> return $ '.awards.js-awards-block'
+
+
+ getAwardUrl: -> @getVotesBlock().data 'award-url'
+
+
+ checkMutuality: (emoji) ->
+
+ awardUrl = @getAwardUrl()
+
+ if emoji in [ 'thumbsup', 'thumbsdown' ]
+ mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
+
+ isAlreadyVoted = $("[data-emoji=#{mutualVote}]").parent().hasClass 'active'
+ @addAward awardUrl, mutualVote, no if isAlreadyVoted
+
+
+ isActive: ($emojiBtn) -> $emojiBtn.hasClass 'active'
+
+
+ decrementCounter: ($emojiBtn, emoji) ->
+ isntNoteBody = $emojiBtn.closest('.note-body').length is 0
+ counter = $('.js-counter', $emojiBtn)
+ counterNumber = parseInt(counter.text())
+
+ if !isntNoteBody
+ # If this is a note body, we just hide the award emoji row like the initial state
+ $emojiBtn.closest('.js-awards-block').addClass 'hidden'
+
+ if counterNumber > 1
+ counter.text(counterNumber - 1)
+ @removeMeFromUserList($emojiBtn, emoji)
+ else if (emoji == 'thumbsup' || emoji == 'thumbsdown') && isntNoteBody
+ $emojiBtn.tooltip('destroy')
+ counter.text('0')
+ @removeMeFromUserList($emojiBtn, emoji)
else
- emojiIcon.tooltip('destroy')
- emojiIcon.remove()
+ $emojiBtn.tooltip('destroy')
+ $emojiBtn.remove()
+
+ $emojiBtn.removeClass('active')
+
+
+ getAwardTooltip: ($awardBlock) ->
+
+ return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title')
+
+
+ removeMeFromUserList: ($emojiBtn, emoji) ->
+
+ awardBlock = $emojiBtn
+ originalTitle = @getAwardTooltip awardBlock
+
+ authors = originalTitle.split ', '
+ authors.splice authors.indexOf('me'), 1
+
+ newAuthors = authors.join ', '
- removeMeFromAuthorList: (emoji) ->
- awardBlock = @findEmojiIcon(emoji).parent()
- authors = awardBlock
- .attr('data-original-title')
- .split(', ')
- authors.splice(authors.indexOf('me'),1)
awardBlock
- .closest('.js-emoji-btn')
- .attr('data-original-title', authors.join(', '))
+ .closest '.js-emoji-btn'
+ .removeData 'original-title'
+ .removeData 'title'
+ .attr 'data-original-title', newAuthors
+ .attr 'data-title', newAuthors
+
@resetTooltip(awardBlock)
- addMeToAuthorList: (emoji) ->
+
+ addMeToUserList: (emoji) ->
+
awardBlock = @findEmojiIcon(emoji).parent()
- origTitle = awardBlock.attr('data-original-title').trim()
- authors = []
+ origTitle = @getAwardTooltip awardBlock
+ users = []
+
if origTitle
- authors = origTitle.split(', ')
- authors.push('me')
- awardBlock.attr('data-original-title', authors.join(', '))
+ users = origTitle.trim().split(', ')
+
+ users.push('me')
+ awardBlock.attr('title', users.join(', '))
+
@resetTooltip(awardBlock)
+
resetTooltip: (award) ->
award.tooltip('destroy')
- # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
+ # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
setTimeout (->
award.tooltip()
), 200
- createEmoji: (emoji) ->
- emojiCssClass = @resolveNameToCssClass(emoji)
-
- nodes = []
- nodes.push(
- "<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>",
- "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
- "<span class='award-control-text js-counter'>1</span>",
- "</button>"
- )
-
- $(nodes.join("\n"))
- .insertBefore('.js-award-holder')
- .find('.emoji-icon')
- .data('emoji', emoji)
+ createEmoji_: (emoji) ->
+
+ emojiCssClass = @resolveNameToCssClass emoji
+
+ buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
+ <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
+ <span class='award-control-text js-counter'>1</span>
+ </button>"
+
+ emoji_node = $(buttonHtml)
+ .insertBefore '.js-awards-block .js-award-holder:not(.js-award-action-btn)'
+ .find '.emoji-icon'
+ .data 'emoji', emoji
+
$('.award-control').tooltip()
+ $currentBlock = $ '.js-awards-block'
+
+ if $currentBlock.is '.hidden'
+ $currentBlock.removeClass 'hidden'
+
+
+ createEmoji: (emoji) ->
+
+ return @createEmoji_ emoji if $('.emoji-menu').length
+
+ awardMenuUrl = gl.awardMenuUrl or '/emojis'
+ @createEmojiMenu awardMenuUrl, => @createEmoji emoji
+
+
resolveNameToCssClass: (emoji) ->
- emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
- if emojiIcon.length > 0
- unicodeName = emojiIcon.data('unicode-name')
+ emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
+
+ if emoji_icon.length > 0
+ unicodeName = emoji_icon.data('unicode-name')
else
# Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
- "emoji-#{unicodeName}"
+ return "emoji-#{unicodeName}"
- postEmoji: (emoji, callback) ->
- $.post @postEmojiUrl, { note: {
- note: ":#{emoji}:"
- noteable_type: @noteableType
- noteable_id: @noteableId
- }},(data) ->
+
+ postEmoji: (awardUrl, emoji, callback) ->
+ $.post awardUrl, { name: emoji }, (data) ->
if data.ok
callback.call()
findEmojiIcon: (emoji) ->
- $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
+ $(".js-awards-block.awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
scrollTop: $('.awards').offset().top - 80
}, 200)
+ normilizeEmojiName: (emoji) ->
+ @aliases[emoji] || emoji
+
addEmojiToFrequentlyUsedList: (emoji) ->
- frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
- frequentlyUsedEmojis.push(emoji)
- $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 })
+ frequently_used_emojis = @getFrequentlyUsedEmojis()
+ frequently_used_emojis.push(emoji)
+ $.cookie('frequently_used_emojis', frequently_used_emojis.join(','), { expires: 365 })
getFrequentlyUsedEmojis: ->
- frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',')
- _.compact(_.uniq(frequentlyUsedEmojis))
+ frequently_used_emojis = ($.cookie('frequently_used_emojis') || '').split(',')
+ _.compact(_.uniq(frequently_used_emojis))
renderFrequentlyUsedBlock: ->
if $.cookie('frequently_used_emojis')
- frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
+ frequently_used_emojis = @getFrequentlyUsedEmojis()
- ul = $('<ul>')
+ ul = $("<ul class='clearfix emoji-menu-list'>")
- for emoji in frequentlyUsedEmojis
- do (emoji) ->
- $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
+ for emoji in frequently_used_emojis
+ $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
setupSearch: ->
- $('input.emoji-search').keyup (ev) =>
+ $('input.emoji-search').on 'keyup', (ev) =>
term = $(ev.target).val()
# Clean previous search results
@@ -204,12 +303,12 @@ class @AwardsHandler
if term
# Generate a search result block
h5 = $('<h5>').text('Search results').addClass('emoji-search')
- foundEmojis = @searchEmojis(term).show()
- ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis)
+ found_emojis = @searchEmojis(term).show()
+ ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
$('.emoji-menu-content ul, .emoji-menu-content h5').hide()
$('.emoji-menu-content').append(h5).append(ul)
else
$('.emoji-menu-content').children().show()
searchEmojis: (term)->
- $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
+ $(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index a3185f87640..bae67a2ebaf 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -17,11 +17,13 @@ class Dispatcher
switch page
when 'projects:issues:index'
Issuable.init()
+ new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
+ window.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
when 'dashboard:todos:index'
@@ -52,6 +54,7 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
+ window.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee
index 5de012e409f..4f73d215b85 100644
--- a/app/assets/javascripts/flash.js.coffee
+++ b/app/assets/javascripts/flash.js.coffee
@@ -1,5 +1,5 @@
class @Flash
- constructor: (message, type)->
+ constructor: (message, type = 'alert')->
@flash = $(".flash-container")
@flash.html("")
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index b3f1dc969b8..7c7334e9e40 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
+ @indeterminateIds = []
+
# Clear click
$clearButton.on 'click', (e) =>
e.preventDefault()
@@ -35,20 +37,20 @@ class GitLabDropdownFilter
if keyCode is 13
return false
- clearTimeout timeout
- timeout = setTimeout =>
- blur_field = @shouldBlur keyCode
- search_text = @input.val()
+ # Only filter asynchronously only if option remote is set
+ if @options.remote
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur keyCode
- if blur_field and @filterInputBlur
- @input.blur()
+ if blur_field and @filterInputBlur
+ @input.blur()
- if @options.remote
- @options.query search_text, (data) =>
+ @options.query @input.val(), (data) =>
@options.callback(data)
- else
- @filter search_text
- , 250
+ , 250
+ else
+ @filter @input.val()
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
@@ -142,6 +144,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
+ INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
@@ -182,9 +185,6 @@ class GitLabDropdown
@fullData = data
@parseData @fullData
-
- if @options.filterable
- @filterInput.trigger 'keyup'
}
# Init filterable
@@ -298,6 +298,13 @@ class GitLabDropdown
opened: =>
@addArrowKeyEvent()
+ if @options.setIndeterminateIds
+ @options.setIndeterminateIds.call(@)
+
+ # Makes indeterminate items effective
+ if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @parseData @fullData
+
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
@@ -309,12 +316,18 @@ class GitLabDropdown
hidden: (e) =>
@removeArrayKeyEvent()
+
+ $input = @dropdown.find(".dropdown-input-field")
+
if @options.filterable
- @dropdown
- .find(".dropdown-input-field")
+ $input
.blur()
.val("")
- .trigger("keyup")
+
+ # Triggering 'keyup' will re-render the dropdown which is not always required
+ # specially if we want to keep the state of the dropdown needed for bulk-assignment
+ if not @options.persistWhenHide
+ $input.trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
@@ -358,7 +371,7 @@ class GitLabDropdown
if @options.renderRow
# Call the render function
- html = @options.renderRow(data)
+ html = @options.renderRow.call(@options, data, @)
else
if not selected
value = if @options.id then @options.id(data) else data.id
@@ -443,6 +456,17 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else
selectedObject
+ else if el.hasClass(INDETERMINATE_CLASS)
+ el.addClass ACTIVE_CLASS
+ el.removeClass INDETERMINATE_CLASS
+
+ if not value?
+ field.remove()
+
+ if not field.length and fieldName
+ @addInput(fieldName, value)
+
+ return selectedObject
else
if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@@ -459,31 +483,42 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
if value?
if !field.length and fieldName
- # Create hidden input for form
- input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
- if @options.inputId?
- input = $(input)
- .attr('id', @options.inputId)
- @dropdown.before input
+ @addInput(fieldName, value)
else
field.val value
return selectedObject
- selectRowAtIndex: (index) ->
- selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
+ addInput: (fieldName, value)->
+ # Create hidden input for form
+ $input = $('<input>').attr('type', 'hidden')
+ .attr('name', fieldName)
+ .val(value)
+
+ if @options.inputId?
+ $input.attr('id', @options.inputId)
+
+ @dropdown.before $input
+
+ selectRowAtIndex: (e, index) ->
+ selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link
- $(selector, @dropdown).trigger "click"
+ $el = $(selector, @dropdown)
+
+ if $el.length
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ $(selector, @dropdown)[0].click()
addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field")
- selector = '.dropdown-content li:not(.divider)'
+ selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}"
@@ -511,8 +546,8 @@ class GitLabDropdown
return false
- if currentKeyCode is 13
- @selectRowAtIndex if currentIndex < 0 then 0 else currentIndex
+ if currentKeyCode is 13 and currentIndex isnt -1
+ @selectRowAtIndex e, currentIndex
removeArrayKeyEvent: ->
$('body').off 'keydown'
diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee
new file mode 100644
index 00000000000..16d023dd391
--- /dev/null
+++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee
@@ -0,0 +1,109 @@
+class @IssuableBulkActions
+ constructor: (opts = {}) ->
+ # Set defaults
+ {
+ @container = $('.content')
+ @form = @getElement('.bulk-update')
+ @issues = @getElement('.issues-list .issue')
+ } = opts
+
+ @bindEvents()
+
+ getElement: (selector) ->
+ @container.find selector
+
+ bindEvents: ->
+ @form.off('submit').on('submit', @onFormSubmit.bind(@))
+
+ onFormSubmit: (e) ->
+ e.preventDefault()
+ @submit()
+
+ submit: ->
+ _this = @
+
+ xhr = $.ajax
+ url: @form.attr 'action'
+ method: @form.attr 'method'
+ dataType: 'JSON',
+ data: @getFormDataAsObject()
+
+ xhr.done (response, status, xhr) ->
+ location.reload()
+
+ xhr.fail ->
+ new Flash("Issue update failed")
+
+ xhr.always @onFormSubmitAlways.bind(@)
+
+ onFormSubmitAlways: ->
+ @form.find('[type="submit"]').enable()
+
+ getSelectedIssues: ->
+ @issues.has('.selected_issue:checked')
+
+ getLabelsFromSelection: ->
+ labels = []
+
+ @getSelectedIssues().map ->
+ _labels = $(@).data('labels')
+ if _labels
+ _labels.map (labelId) ->
+ labels.push(labelId) if labels.indexOf(labelId) is -1
+
+ labels
+
+ ###*
+ * Will return only labels that were marked previously and the user has unmarked
+ * @return {Array} Label IDs
+ ###
+ getUnmarkedIndeterminedLabels: ->
+ result = []
+ labelsToKeep = []
+
+ for el in @getElement('.labels-filter .is-indeterminate')
+ labelsToKeep.push $(el).data('labelId')
+
+ for id in @getLabelsFromSelection()
+ # Only the ones that we are not going to keep
+ result.push(id) if labelsToKeep.indexOf(id) is -1
+
+ result
+
+ ###*
+ * Simple form serialization, it will return just what we need
+ * Returns key/value pairs from form data
+ ###
+ getFormDataAsObject: ->
+ formData =
+ update:
+ state_event : @form.find('input[name="update[state_event]"]').val()
+ assignee_id : @form.find('input[name="update[assignee_id]"]').val()
+ milestone_id : @form.find('input[name="update[milestone_id]"]').val()
+ issues_ids : @form.find('input[name="update[issues_ids]"]').val()
+ add_label_ids : []
+ remove_label_ids : []
+
+ @getLabelsToApply().map (id) ->
+ formData.update.add_label_ids.push id
+
+ @getLabelsToRemove().map (id) ->
+ formData.update.remove_label_ids.push id
+
+ formData
+
+ getLabelsToApply: ->
+ labelIds = []
+ $labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
+
+ $labels.each (k, label) ->
+ labelIds.push $(label).val() if label
+
+ labelIds
+
+ ###*
+ * Just an alias of @getUnmarkedIndeterminedLabels
+ * @return {Array} Array of labels
+ ###
+ getLabelsToRemove: ->
+ @getUnmarkedIndeterminedLabels()
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 995fd768603..ec74dfaae1a 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -1,5 +1,7 @@
class @LabelsSelect
constructor: ->
+ _this = @
+
$('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown)
projectId = $dropdown.data('project-id')
@@ -196,10 +198,18 @@ class @LabelsSelect
callback data
- renderRow: (label) ->
- removesAll = label.id is 0 or not label.id?
+ renderRow: (label, instance) ->
+ $li = $('<li>')
+ $a = $('<a href="#">')
selectedClass = []
+ removesAll = label.id is 0 or not label.id?
+
+ if $dropdown.hasClass('js-filter-bulk-update')
+ indeterminate = instance.indeterminateIds
+ if indeterminate.indexOf(label.id) isnt -1
+ selectedClass.push 'is-indeterminate'
+
if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
@@ -230,13 +240,17 @@ class @LabelsSelect
else
colorEl = ''
- "<li>
- <a href='#' class='#{selectedClass.join(' ')}'>
- #{colorEl}
- #{_.escape(label.title)}
- </a>
- </li>"
- filterable: true
+ # We need to identify which items are actually labels
+ if label.id
+ selectedClass.push('label-item')
+ $a.attr('data-label-id', label.id)
+
+ $a.addClass(selectedClass.join(' '))
+ .html("#{colorEl} #{_.escape(label.title)}")
+
+ # Return generated html
+ $li.html($a).prop('outerHTML')
+ persistWhenHide: $dropdown.data('persistWhenHide')
search:
fields: ['title']
selectable: true
@@ -280,10 +294,19 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
- saveLabelData()
+ if not $dropdown.hasClass 'js-filter-bulk-update'
+ saveLabelData()
+
+ if $dropdown.hasClass('js-filter-bulk-update')
+ # If we are persisting state we need the classes
+ if not @options.persistWhenHide
+ $dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
+ if $dropdown.hasClass('js-filter-bulk-update')
+ return
+
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index'
@@ -298,4 +321,31 @@ class @LabelsSelect
return
else
saveLabelData()
+
+ setIndeterminateIds: ->
+ if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @indeterminateIds = _this.getIndeterminateIds()
)
+
+ @bindEvents()
+
+ bindEvents: ->
+ $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
+
+ onSelectCheckboxIssue: ->
+ return if $('.selected_issue:checked').length
+
+ # Remove inputs
+ $('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
+
+ # Also restore button text
+ $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
+
+ getIndeterminateIds: ->
+ label_ids = []
+
+ $('.selected_issue:checked').each (i, el) ->
+ issue_id = $(el).data('id')
+ label_ids.push $("#issue_#{issue_id}").data('labels')
+
+ _.flatten(label_ids)
diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb
new file mode 100644
index 00000000000..97be65116e2
--- /dev/null
+++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb
@@ -0,0 +1,2 @@
+window.emojiAliases = ->
+ JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index f8151963fa7..7c3d57fc194 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -167,7 +167,7 @@ class @Notes
return
if note.award
- awardsHandler.addAwardToEmojiBar(note.note)
+ awardsHandler.addAwardToEmojiBar(note.name)
awardsHandler.scrollToAwards()
# render note if it not present in loaded list
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index 2122e80f57a..5eb915a51ea 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -156,11 +156,14 @@ class @SearchAutocomplete
# No need to enable anything if user is not logged in
return if !gon.current_user_id
- _this = @
- @loadingSuggestions = false
+ unless @dropdown.hasClass('open')
+ _this = @
+ @loadingSuggestions = false
- @dropdown.addClass('open')
- @searchInput.removeClass('disabled')
+ @dropdown
+ .addClass('open')
+ .trigger('shown.bs.dropdown')
+ @searchInput.removeClass('disabled')
onSearchInputKeyDown: =>
# Saves last length of the entered text
@@ -191,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete()
else
# We should display the menu only when input is not empty
- @enableAutocomplete()
+ @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 93c63c69843..28634d0c59f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -232,9 +232,8 @@
a {
padding-left: 25px;
- &.is-active {
+ &.is-indeterminate, &.is-active {
&::before {
- content: "\f00c";
position: absolute;
left: 5px;
top: 50%;
@@ -246,6 +245,14 @@
-moz-osx-font-smoothing: grayscale;
}
}
+
+ &.is-indeterminate::before {
+ content: "\f068";
+ }
+
+ &.is-active::before {
+ content: "\f00c";
+ }
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 250d6309291..828e7224231 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -2,18 +2,10 @@
* Generic mixins
*/
@mixin box-shadow($shadow) {
- -webkit-box-shadow: $shadow;
- -moz-box-shadow: $shadow;
- -ms-box-shadow: $shadow;
- -o-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin border-radius($radius) {
- -webkit-border-radius: $radius;
- -moz-border-radius: $radius;
- -ms-border-radius: $radius;
- -o-border-radius: $radius;
border-radius: $radius;
}
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 37bf38fa65d..07d40f40556 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,6 +1,4 @@
.awards {
- line-height: 34px;
-
.emoji-icon {
width: 20px;
height: 20px;
@@ -9,8 +7,6 @@
.emoji-menu {
position: absolute;
- top: 100%;
- left: 0;
margin-top: 3px;
z-index: 1000;
min-width: 160px;
@@ -23,7 +19,12 @@
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
- transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
+ transition: .3s cubic-bezier(.87,-.41,.19,1.44);
+ transition-property: transform, opacity;
+
+ &.is-aligned-right {
+ transform-origin: 100% -45px;
+ }
&.is-visible {
pointer-events: all;
@@ -107,7 +108,7 @@
}
&.is-loading {
- .award-control-icon {
+ .award-control-icon-normal {
display: none;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index aa41565f812..44222e8e8a4 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -3,12 +3,7 @@
background: #111;
color: #fff;
font-family: $monospace_font;
- white-space: pre;
- white-space: pre-wrap; /* css-3 */
- white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
- white-space: -pre-wrap; /* Opera 4-6 */
- white-space: -o-pre-wrap; /* Opera 7 */
- word-wrap: break-word; /* Internet Explorer 5.5+ */
+ white-space: pre-wrap;
overflow: auto;
overflow-y: hidden;
font-size: 12px;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 037ad520545..ae524cd6bae 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -158,13 +158,11 @@
.search-holder {
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
- display: -ms-flexbox;
display: flex;
}
.search-field-holder {
-webkit-flex: 1 0 auto;
- -ms-flex: 1 0 auto;
flex: 1 0 auto;
position: relative;
margin-right: 0;