summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/application.js.coffee13
-rw-r--r--app/assets/javascripts/awards_handler.coffee335
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/due_date_select.js.coffee5
-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/graphs/application.js.coffee7
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js.coffee (renamed from app/assets/javascripts/stat_graph.js.coffee)0
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js.coffee (renamed from app/assets/javascripts/stat_graph_contributors.js.coffee)1
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee (renamed from app/assets/javascripts/stat_graph_contributors_graph.js.coffee)2
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee (renamed from app/assets/javascripts/stat_graph_contributors_util.js.coffee)0
-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/milestone_select.js.coffee4
-rw-r--r--app/assets/javascripts/notes.js.coffee2
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee35
-rw-r--r--app/assets/javascripts/u2f/authenticate.js.coffee63
-rw-r--r--app/assets/javascripts/u2f/error.js.coffee13
-rw-r--r--app/assets/javascripts/u2f/register.js.coffee63
-rw-r--r--app/assets/javascripts/u2f/util.js.coffee.erb15
-rw-r--r--app/assets/javascripts/users/application.js.coffee8
-rw-r--r--app/assets/javascripts/users/calendar.js.coffee (renamed from app/assets/javascripts/calendar.js.coffee)0
-rw-r--r--app/assets/javascripts/users_select.js.coffee2
24 files changed, 661 insertions, 188 deletions
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 8f275510bad..7526398dadc 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -4,7 +4,7 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
-#= require jquery
+#= require jquery2
#= require jquery-ui/autocomplete
#= require jquery-ui/datepicker
#= require jquery-ui/draggable
@@ -18,7 +18,6 @@
#= require jquery.atwho
#= require jquery.scrollTo
#= require jquery.turbolinks
-#= require d3
#= require turbolinks
#= require autosave
#= require bootstrap/affix
@@ -51,9 +50,17 @@
#= require shortcuts_network
#= require jquery.nicescroll
#= require date.format
-#= require_tree .
+#= require_directory ./behaviors
+#= require_directory ./blob
+#= require_directory ./ci
+#= require_directory ./commit
+#= require_directory ./extensions
+#= require_directory ./lib
+#= require_directory ./u2f
+#= require_directory .
#= require fuzzaldrin-plus
#= require cropper
+#= require u2f
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
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/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee
index 3cc70185178..3d009a96d05 100644
--- a/app/assets/javascripts/due_date_select.js.coffee
+++ b/app/assets/javascripts/due_date_select.js.coffee
@@ -21,7 +21,7 @@ class @DueDateSelect
$dropdown.glDropdown(
hidden: ->
$selectbox.hide()
- $value.removeAttr('style')
+ $value.css('display', '')
)
addDueDate = (isDropdown) ->
@@ -42,12 +42,13 @@ class @DueDateSelect
type: 'PUT'
url: issueUpdateURL
data: data
+ dataType: 'json'
beforeSend: ->
$loading.fadeIn()
if isDropdown
$dropdown.trigger('loading.gl.dropdown')
$selectbox.hide()
- $value.removeAttr('style')
+ $value.css('display', '')
$valueContent.html(mediumDate)
$sidebarValue.html(mediumDate)
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/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee
new file mode 100644
index 00000000000..e0f681acf0b
--- /dev/null
+++ b/app/assets/javascripts/graphs/application.js.coffee
@@ -0,0 +1,7 @@
+# This is a manifest file that'll be compiled into including all the files listed below.
+# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+# be included in the compiled file accessible from http://example.com/assets/application.js
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+#= require_tree .
diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee
index f36c71fd25e..f36c71fd25e 100644
--- a/app/assets/javascripts/stat_graph.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph.js.coffee
diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee
index 3be14cb43dd..1d9fae7cf79 100644
--- a/app/assets/javascripts/stat_graph_contributors.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee
@@ -1,5 +1,4 @@
#= require d3
-#= require stat_graph_contributors_util
class @ContributorsStatGraph
init: (log) ->
diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
index b7a0e073766..584d281a510 100644
--- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
@@ -1,6 +1,4 @@
#= require d3
-#= require jquery
-#= require underscore
class @ContributorsGraph
MARGIN:
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee
index 31617c88b4a..31617c88b4a 100644
--- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee
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/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 345a0e447af..1d061d5edb7 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -83,7 +83,7 @@ class @MilestoneSelect
$selectbox.hide()
# display:block overrides the hide-collapse rule
- $value.removeAttr('style')
+ $value.css('display', '')
clicked: (selected) ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
@@ -118,7 +118,7 @@ class @MilestoneSelect
$dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut()
$selectbox.hide()
- $value.removeAttr('style')
+ $value.css('display', '')
if data.milestone?
data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path
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 6a7b4ad1db7..5eb915a51ea 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -20,8 +20,7 @@ class @SearchAutocomplete
@dropdown = @wrap.find('.dropdown')
@dropdownContent = @dropdown.find('.dropdown-content')
- @locationBadgeEl = @getElement('.search-location-badge')
- @locationText = @getElement('.location-text')
+ @locationBadgeEl = @getElement('.location-badge')
@scopeInputEl = @getElement('#scope')
@searchInput = @getElement('.search-input')
@projectInputEl = @getElement('#search_project_id')
@@ -133,7 +132,7 @@ class @SearchAutocomplete
scope: @scopeInputEl.val()
# Location badge
- _location: @locationText.text()
+ _location: @locationBadgeEl.text()
}
bindEvents: ->
@@ -143,23 +142,28 @@ class @SearchAutocomplete
@searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus
@clearInput.on 'click', @onClearInputClick
+ @locationBadgeEl.on 'click', =>
+ @searchInput.focus()
onDocumentClick: (e) =>
# If clicking outside the search box
# And search input is not focused
# And we are not clicking inside a suggestion
- if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
+ if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length
@onSearchInputBlur()
enableAutocomplete: ->
# 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
@@ -190,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
@@ -221,10 +225,8 @@ class @SearchAutocomplete
category = if item.category? then "#{item.category}: " else ''
value = if item.value? then item.value else ''
- html = "<span class='location-badge'>
- <i class='location-text'>#{category}#{value}</i>
- </span>"
- @locationBadgeEl.html(html)
+ badgeText = "#{category}#{value}"
+ @locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge')
restoreOriginalState: ->
@@ -233,9 +235,8 @@ class @SearchAutocomplete
for input in inputs
@getElement("##{input}").val(@originalState[input])
-
if @originalState._location is ''
- @locationBadgeEl.empty()
+ @locationBadgeEl.hide()
else
@addLocationBadge(
value: @originalState._location
@@ -244,7 +245,7 @@ class @SearchAutocomplete
@dropdown.removeClass 'open'
badgePresent: ->
- @locationBadgeEl.children().length
+ @locationBadgeEl.length
resetSearchState: ->
inputs = Object.keys @originalState
@@ -257,7 +258,7 @@ class @SearchAutocomplete
@getElement("##{input}").val('')
removeLocationBadge: ->
- @locationBadgeEl.empty()
+ @locationBadgeEl.hide()
# Reset state
@resetSearchState()
diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee
new file mode 100644
index 00000000000..6deb902c8de
--- /dev/null
+++ b/app/assets/javascripts/u2f/authenticate.js.coffee
@@ -0,0 +1,63 @@
+# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
+#
+# State Flow #1: setup -> in_progress -> authenticated -> POST to server
+# State Flow #2: setup -> in_progress -> error -> setup
+
+class @U2FAuthenticate
+ constructor: (@container, u2fParams) ->
+ @appId = u2fParams.app_id
+ @challenges = u2fParams.challenges
+ @signRequests = u2fParams.sign_requests
+
+ start: () =>
+ if U2FUtil.isU2FSupported()
+ @renderSetup()
+ else
+ @renderNotSupported()
+
+ authenticate: () =>
+ u2f.sign(@appId, @challenges, @signRequests, (response) =>
+ if response.errorCode
+ error = new U2FError(response.errorCode)
+ @renderError(error);
+ else
+ @renderAuthenticated(JSON.stringify(response))
+ , 10)
+
+ #############
+ # Rendering #
+ #############
+
+ templates: {
+ "notSupported": "#js-authenticate-u2f-not-supported",
+ "setup": '#js-authenticate-u2f-setup',
+ "inProgress": '#js-authenticate-u2f-in-progress',
+ "error": '#js-authenticate-u2f-error',
+ "authenticated": '#js-authenticate-u2f-authenticated'
+ }
+
+ renderTemplate: (name, params) =>
+ templateString = $(@templates[name]).html()
+ template = _.template(templateString)
+ @container.html(template(params))
+
+ renderSetup: () =>
+ @renderTemplate('setup')
+ @container.find('#js-login-u2f-device').on('click', @renderInProgress)
+
+ renderInProgress: () =>
+ @renderTemplate('inProgress')
+ @authenticate()
+
+ renderError: (error) =>
+ @renderTemplate('error', {error_message: error.message()})
+ @container.find('#js-u2f-try-again').on('click', @renderSetup)
+
+ renderAuthenticated: (deviceResponse) =>
+ @renderTemplate('authenticated')
+ # Prefer to do this instead of interpolating using Underscore templates
+ # because of JSON escaping issues.
+ @container.find("#js-device-response").val(deviceResponse)
+
+ renderNotSupported: () =>
+ @renderTemplate('notSupported')
diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee
new file mode 100644
index 00000000000..1a2fc3e757f
--- /dev/null
+++ b/app/assets/javascripts/u2f/error.js.coffee
@@ -0,0 +1,13 @@
+class @U2FError
+ constructor: (@errorCode) ->
+ @httpsDisabled = (window.location.protocol isnt 'https:')
+ console.error("U2F Error Code: #{@errorCode}")
+
+ message: () =>
+ switch
+ when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
+ "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
+ when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
+ "This device has already been registered with us."
+ else
+ "There was a problem communicating with your device."
diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee
new file mode 100644
index 00000000000..74472cfa120
--- /dev/null
+++ b/app/assets/javascripts/u2f/register.js.coffee
@@ -0,0 +1,63 @@
+# Register U2F (universal 2nd factor) devices for users to authenticate with.
+#
+# State Flow #1: setup -> in_progress -> registered -> POST to server
+# State Flow #2: setup -> in_progress -> error -> setup
+
+class @U2FRegister
+ constructor: (@container, u2fParams) ->
+ @appId = u2fParams.app_id
+ @registerRequests = u2fParams.register_requests
+ @signRequests = u2fParams.sign_requests
+
+ start: () =>
+ if U2FUtil.isU2FSupported()
+ @renderSetup()
+ else
+ @renderNotSupported()
+
+ register: () =>
+ u2f.register(@appId, @registerRequests, @signRequests, (response) =>
+ if response.errorCode
+ error = new U2FError(response.errorCode)
+ @renderError(error);
+ else
+ @renderRegistered(JSON.stringify(response))
+ , 10)
+
+ #############
+ # Rendering #
+ #############
+
+ templates: {
+ "notSupported": "#js-register-u2f-not-supported",
+ "setup": '#js-register-u2f-setup',
+ "inProgress": '#js-register-u2f-in-progress',
+ "error": '#js-register-u2f-error',
+ "registered": '#js-register-u2f-registered'
+ }
+
+ renderTemplate: (name, params) =>
+ templateString = $(@templates[name]).html()
+ template = _.template(templateString)
+ @container.html(template(params))
+
+ renderSetup: () =>
+ @renderTemplate('setup')
+ @container.find('#js-setup-u2f-device').on('click', @renderInProgress)
+
+ renderInProgress: () =>
+ @renderTemplate('inProgress')
+ @register()
+
+ renderError: (error) =>
+ @renderTemplate('error', {error_message: error.message()})
+ @container.find('#js-u2f-try-again').on('click', @renderSetup)
+
+ renderRegistered: (deviceResponse) =>
+ @renderTemplate('registered')
+ # Prefer to do this instead of interpolating using Underscore templates
+ # because of JSON escaping issues.
+ @container.find("#js-device-response").val(deviceResponse)
+
+ renderNotSupported: () =>
+ @renderTemplate('notSupported')
diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb
new file mode 100644
index 00000000000..d59341c38b9
--- /dev/null
+++ b/app/assets/javascripts/u2f/util.js.coffee.erb
@@ -0,0 +1,15 @@
+# Helper class for U2F (universal 2nd factor) device registration and authentication.
+
+class @U2FUtil
+ @isU2FSupported: ->
+ if @testMode
+ true
+ else
+ gon.u2f.browser_supports_u2f
+
+ @enableTestMode: ->
+ @testMode = true
+
+<% if Rails.env.test? %>
+U2FUtil.enableTestMode();
+<% end %>
diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee
new file mode 100644
index 00000000000..647ffbf5f45
--- /dev/null
+++ b/app/assets/javascripts/users/application.js.coffee
@@ -0,0 +1,8 @@
+# This is a manifest file that'll be compiled into including all the files listed below.
+# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+# be included in the compiled file accessible from http://example.com/assets/application.js
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+#= require d3
+#= require_tree .
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee
index 26a26061539..26a26061539 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/users/calendar.js.coffee
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 519618aa617..de0eae58bff 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -149,7 +149,7 @@ class @UsersSelect
hidden: (e) ->
$selectbox.hide()
# display:block overrides the hide-collapse rule
- $value.removeAttr('style')
+ $value.css('display', '')
clicked: (user) ->
page = $('body').data 'page'