diff options
Diffstat (limited to 'app/assets/javascripts')
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' |