summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/api.js.coffee14
-rw-r--r--app/assets/javascripts/application.js.coffee38
-rw-r--r--app/assets/javascripts/aside.js.coffee1
-rw-r--r--app/assets/javascripts/awards_handler.coffee77
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/ci/build.coffee13
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee10
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee280
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee52
-rw-r--r--app/assets/javascripts/issue.js.coffee25
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee11
-rw-r--r--app/assets/javascripts/issues.js.coffee26
-rw-r--r--app/assets/javascripts/labels_select.js.coffee100
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee46
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee24
-rw-r--r--app/assets/javascripts/milestone.js.coffee7
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee65
-rw-r--r--app/assets/javascripts/notes.js.coffee109
-rw-r--r--app/assets/javascripts/profile.js.coffee55
-rw-r--r--app/assets/javascripts/project.js.coffee1
-rw-r--r--app/assets/javascripts/project_new.js.coffee13
-rw-r--r--app/assets/javascripts/projects_list.js.coffee6
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee15
-rw-r--r--app/assets/javascripts/sidebar.js.coffee18
-rw-r--r--app/assets/javascripts/subscription.js.coffee34
-rw-r--r--app/assets/javascripts/todos.js.coffee56
-rw-r--r--app/assets/javascripts/users_select.js.coffee88
27 files changed, 1045 insertions, 176 deletions
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 3e0fdb3f795..2ddf8612db3 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
+ labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -61,6 +62,19 @@
).done (projects) ->
callback(projects)
+ newLabel: (project_id, data, callback) ->
+ url = Api.buildUrl(Api.labels_path)
+ url = url.replace(':id', project_id)
+
+ data.private_token = gon.api_token
+ $.ajax(
+ url: url
+ type: "POST"
+ data: data
+ dataType: "json"
+ ).done (label) ->
+ callback(label)
+
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 321da10a009..7f983325049 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -7,6 +7,7 @@
#= require jquery
#= require jquery-ui/autocomplete
#= require jquery-ui/datepicker
+#= require jquery-ui/draggable
#= require jquery-ui/effect-highlight
#= require jquery-ui/sortable
#= require jquery_ujs
@@ -42,7 +43,6 @@
#= require jquery.nicescroll
#= require_tree .
#= require fuzzaldrin-plus
-#= require cropper.js
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -108,6 +108,8 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
+
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
@@ -220,17 +222,17 @@ $ ->
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
- $gutterIcon = $('aside .gutter-toggle').find('i')
+ $gutterIcon = $('.js-sidebar-toggle').find('i')
if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click')
$(document)
- .off 'click', 'aside .gutter-toggle'
- .on 'click', 'aside .gutter-toggle', (e, triggered) ->
+ .off 'click', '.js-sidebar-toggle'
+ .on 'click', '.js-sidebar-toggle', (e, triggered) ->
e.preventDefault()
$this = $(this)
$thisIcon = $this.find 'i'
- $allGutterToggleIcons = $('.gutter-toggle i')
+ $allGutterToggleIcons = $('.js-sidebar-toggle i')
if $thisIcon.hasClass('fa-angle-double-right')
$allGutterToggleIcons
.removeClass('fa-angle-double-right')
@@ -256,35 +258,14 @@ $ ->
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
- bootstrapBreakpoint = undefined;
- checkBootstrapBreakpoints = ->
- if $('.device-xs').is(':visible')
- bootstrapBreakpoint = "xs"
- else if $('.device-sm').is(':visible')
- bootstrapBreakpoint = "sm"
- else if $('.device-md').is(':visible')
- bootstrapBreakpoint = "md"
- else if $('.device-lg').is(':visible')
- bootstrapBreakpoint = "lg"
-
- setBootstrapBreakpoints = ->
- if $('.device-xs').length
- return
-
- $("body")
- .append('<div class="device-xs visible-xs"></div>'+
- '<div class="device-sm visible-sm"></div>'+
- '<div class="device-md visible-md"></div>'+
- '<div class="device-lg visible-lg"></div>')
- checkBootstrapBreakpoints()
-
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
- checkBootstrapBreakpoints()
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
+ bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
@@ -293,6 +274,5 @@ $ ->
.on "resize", (e) ->
fitSidebarForSize()
- setBootstrapBreakpoints()
checkInitialSidebarSize()
new Aside()
diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee
index 85473101944..66ab5054326 100644
--- a/app/assets/javascripts/aside.js.coffee
+++ b/app/assets/javascripts/aside.js.coffee
@@ -5,7 +5,6 @@ class @Aside
e.preventDefault()
btn = $(e.currentTarget)
icon = btn.find('i')
- console.log('1')
if icon.hasClass('fa-angle-left')
btn.parent().find('section').hide()
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 8f89d3e61a2..03a44874161 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -1,6 +1,6 @@
class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
- $(".add-award").click (event) =>
+ $(".js-add-award").on "click", (event) =>
event.stopPropagation()
event.preventDefault()
@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible")
- $(".emoji-menu").hide()
+ $(".emoji-menu").removeClass "is-visible"
+
+ $(".awards")
+ .off "click"
+ .on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock()
- @setupSearch()
+
+ handleClick: (e) ->
+ e.preventDefault()
+ emoji = $(this)
+ .find(".icon")
+ .data "emoji"
+ awards_handler.addAward emoji
showEmojiMenu: ->
if $(".emoji-menu").length
- $(".emoji-menu").show()
- $("#emoji_search").focus()
- else
- $.get "/emojis", (response) ->
- $(".add-award").after response
- $(".emoji-menu").show()
+ if $(".emoji-menu").is ".is-visible"
+ $(".emoji-menu").removeClass "is-visible"
+ $("#emoji_search").blur()
+ else
+ $(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus()
+ else
+ $('.js-add-award').addClass "is-loading"
+ $.get "/emojis", (response) =>
+ $('.js-add-award').removeClass "is-loading"
+ $(".js-award-holder").append response
+ setTimeout =>
+ $(".emoji-menu").addClass "is-visible"
+ $("#emoji_search").focus()
+ @setupSearch()
+ , 200
addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, =>
@addAwardToEmojiBar(emoji)
- $(".emoji-menu").hide()
+ $(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji)
@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji)
@decrementCounter(emoji)
else
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active")
@addMeToAuthorList(emoji)
@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) ->
- counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent()
if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1)
@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent()
- authors = award_block.attr("data-original-title").split(", ")
+ authors = award_block
+ .attr("data-original-title")
+ .split(", ")
authors.splice(authors.indexOf("me"),1)
- award_block.closest(".award").attr("data-original-title", authors.join(", "))
+ award_block
+ .closest(".js-emoji-btn")
+ .attr("data-original-title", authors.join(", "))
@resetTooltip(award_block)
addMeToAuthorList: (emoji) ->
@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji)
nodes = []
- nodes.push("<div class='award active' title='me'>")
- nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
- nodes.push("<div class='counter'>1</div>")
- nodes.push("</div>")
-
- emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
-
- $(".award").tooltip()
+ nodes.push(
+ "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
+ "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
+ "<span class='award-control-text js-counter'>1</span>",
+ "</button>"
+ )
+
+ emoji_node = $(nodes.join("\n"))
+ .insertBefore(".js-award-holder")
+ .find(".emoji-icon")
+ .data("emoji", emoji)
+ $('.award-control').tooltip()
resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call()
findEmojiIcon: (emoji) ->
- $(".award [data-emoji='#{emoji}']")
+ $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: ->
$('body, html').animate({
@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val()
# Clean previous search results
- $("ul.emoji-search,h5.emoji-search").remove()
+ $("ul.emoji-menu-search, h5.emoji-search").remove()
if term
# Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show()
- ul = $("<ul>").addClass("emoji-search").append(found_emojis)
+ 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
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
new file mode 100644
index 00000000000..5457430f921
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.coffee
@@ -0,0 +1,37 @@
+class @Breakpoints
+ instance = null;
+
+ class BreakpointInstance
+ BREAKPOINTS = ["xs", "sm", "md", "lg"]
+
+ constructor: ->
+ @setup()
+
+ setup: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ return if $(allDeviceSelector.join(",")).length
+
+ # Create all the elements
+ els = $.map BREAKPOINTS, (breakpoint) ->
+ "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
+ $("body").append els.join('')
+
+ visibleDevice: ->
+ allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
+ ".device-#{breakpoint}"
+ $(allDeviceSelector.join(",")).filter(":visible")
+
+ getBreakpointSize: ->
+ $visibleDevice = @visibleDevice
+ # the page refreshed via turbolinks
+ if not $visibleDevice().length
+ @setup()
+ $visibleDevice = @visibleDevice()
+ return $visibleDevice.attr("class").split("visible-")[1]
+
+ @get: ->
+ return instance ?= new BreakpointInstance
+
+$ =>
+ @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index 44d5ddb7d95..7afe8bf79e2 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval)
+ @initScrollButtonAffix()
+
if build_status == "running" || build_status == "pending"
#
# Bind autoscroll button to follow build output
@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
+ initScrollButtonAffix: ->
+ $buildScroll = $('#js-build-scroll')
+ $body = $('body')
+ $buildTrace = $('#build-trace')
+
+ $buildScroll.affix(
+ offset:
+ bottom: ->
+ $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
+ )
+
@CiBuild = CiBuild
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index d7feb5d5c87..f5e1ca9860d 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -14,7 +14,6 @@ class Dispatcher
path = page.split(':')
shortcut_handler = null
-
switch page
when 'projects:issues:index'
Issues.init()
@@ -23,8 +22,10 @@ class Dispatcher
new Issue()
shortcut_handler = new ShortcutsIssuable()
new ZenMode()
- when 'projects:milestones:show'
+ when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
+ when 'dashboard:todos:index'
+ new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
@@ -74,8 +75,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length
- when 'groups:show'
+ when 'groups:activity'
new Activities()
+ when 'groups:show'
shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index'
new GroupMembers()
@@ -103,6 +105,8 @@ class Dispatcher
new ProjectFork()
when 'projects:artifacts:browse'
new BuildArtifacts()
+ when 'projects:group_links:index'
+ new GroupsSelect()
switch path.first()
when 'admin'
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
new file mode 100644
index 00000000000..960585245d7
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -0,0 +1,280 @@
+class GitLabDropdownFilter
+ BLUR_KEYCODES = [27, 40]
+
+ constructor: (@dropdown, @options) ->
+ @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+
+ # Key events
+ timeout = ""
+ @input.on "keyup", (e) =>
+ if e.keyCode is 13 && @input.val() isnt ""
+ if @options.enterCallback
+ @options.enterCallback()
+ return
+
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur e.keyCode
+ search_text = @input.val()
+
+ if blur_field
+ @input.blur()
+
+ if @options.remote
+ @options.query search_text, (data) =>
+ @options.callback(data)
+ else
+ @filter search_text
+ , 250
+
+ shouldBlur: (keyCode) ->
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0
+
+ filter: (search_text) ->
+ data = @options.data()
+ results = data
+
+ if search_text isnt ""
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+
+ @options.callback results
+
+class GitLabDropdownRemote
+ constructor: (@dataEndpoint, @options) ->
+
+ execute: ->
+ if typeof @dataEndpoint is "string"
+ @fetchData()
+ else if typeof @dataEndpoint is "function"
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data by calling the data funcfion
+ @dataEndpoint "", (data) =>
+ if @options.success
+ @options.success(data)
+
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data through ajax if the data is a string
+ fetchData: ->
+ $.ajax(
+ url: @dataEndpoint,
+ dataType: @options.dataType,
+ beforeSend: =>
+ if @options.beforeSend
+ @options.beforeSend()
+ success: (data) =>
+ if @options.success
+ @options.success(data)
+ )
+
+class GitLabDropdown
+ LOADING_CLASS = "is-loading"
+ PAGE_TWO_CLASS = "is-page-two"
+ ACTIVE_CLASS = "is-active"
+
+ constructor: (@el, @options) ->
+ self = @
+ @dropdown = $(@el).parent()
+ search_fields = if @options.search then @options.search.fields else [];
+
+ if @options.data
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
+
+ @parseData @fullData
+ }
+
+ # Init filiterable
+ if @options.filterable
+ @filter = new GitLabDropdownFilter @dropdown,
+ remote: @options.filterRemote
+ query: @options.data
+ keys: @options.search.fields
+ data: =>
+ return @fullData
+ callback: (data) =>
+ @parseData data
+ enterCallback: =>
+ @selectFirstRow()
+
+ # Event listeners
+ @dropdown.on "shown.bs.dropdown", @opened
+ @dropdown.on "hidden.bs.dropdown", @hidden
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+
+ @togglePage()
+
+ if @options.selectable
+ selector = ".dropdown-content a"
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content a"
+
+ @dropdown.on "click", selector, (e) ->
+ self.rowClicked $(@)
+
+ if self.options.clicked
+ self.options.clicked()
+
+ toggleLoading: ->
+ $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
+
+ togglePage: ->
+ menu = $('.dropdown-menu', @dropdown)
+
+ if menu.hasClass(PAGE_TWO_CLASS)
+ if @remote
+ @remote.execute()
+
+ menu.toggleClass PAGE_TWO_CLASS
+
+ parseData: (data) ->
+ @renderedData = data
+
+ # Render each row
+ html = $.map data, (obj) =>
+ return @renderItem(obj)
+
+ if @options.filterable and data.length is 0
+ # render no matching results
+ html = [@noResults()]
+
+ # Render the full menu
+ full_html = @renderMenu(html.join(""))
+
+ @appendMenu(full_html)
+
+ opened: =>
+ contentHtml = $('.dropdown-content', @dropdown).html()
+ if @remote && contentHtml is ""
+ @remote.execute()
+
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").focus()
+
+ hidden: =>
+ if @options.filterable
+ @dropdown
+ .find(".dropdown-input-field")
+ .blur()
+ .val("")
+ .trigger("keyup")
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+
+
+ # Render the full menu
+ renderMenu: (html) ->
+ menu_html = ""
+
+ if @options.renderMenu
+ menu_html = @options.renderMenu(html)
+ else
+ menu_html = "<ul>#{html}</ul>"
+
+ return menu_html
+
+ # Append the menu into the dropdown
+ appendMenu: (html) ->
+ selector = '.dropdown-content'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content"
+
+ $(selector, @dropdown).html html
+
+ # Render the row
+ renderItem: (data) ->
+ html = ""
+
+ return "<li class='divider'></li>" if data is "divider"
+
+ if @options.renderRow
+ # Call the render function
+ html = @options.renderRow(data)
+ else
+ selected = if @options.isSelected then @options.isSelected(data) else false
+ url = if @options.url then @options.url(data) else "#"
+ text = if @options.text then @options.text(data) else ""
+ cssClass = "";
+
+ if selected
+ cssClass = "is-active"
+
+ html = "<li>"
+ html += "<a href='#{url}' class='#{cssClass}'>"
+ html += text
+ html += "</a>"
+ html += "</li>"
+
+ return html
+
+ noResults: ->
+ html = "<li>"
+ html += "<a href='#' class='is-focused'>"
+ html += "No matching results."
+ html += "</a>"
+ html += "</li>"
+
+ rowClicked: (el) ->
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}']")
+
+ if el.hasClass(ACTIVE_CLASS)
+ field.remove()
+ else
+ fieldName = @options.fieldName
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+
+ if !value?
+ field.remove()
+
+ if @options.multiSelect
+ oldValue = field.val()
+ if oldValue
+ value = "#{oldValue},#{value}"
+ else
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
+
+ # Toggle active class for the tick mark
+ el.toggleClass "is-active"
+
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
+
+ if value?
+ if !field.length
+ # Create hidden input for form
+ input = "<input type='hidden' name='#{fieldName}' />"
+ @dropdown.before input
+
+ @dropdown.parent().find("input[name='#{fieldName}']").val value
+
+ selectFirstRow: ->
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ # similute a click on the first link
+ $(selector).trigger "click"
+
+$.fn.glDropdown = (opts) ->
+ return @.each ->
+ new GitLabDropdown @, opts
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 48c249943f2..7a788f761b7 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -1,4 +1,7 @@
class @IssuableForm
+ issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?'
+ wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
+
constructor: (@form) ->
GitLab.GfmAutoComplete.setup()
new UsersSelect()
@@ -6,14 +9,17 @@ class @IssuableForm
@titleField = @form.find("input[name*='[title]']")
@descriptionField = @form.find("textarea[name*='[description]']")
+ @issueMoveField = @form.find("#move_to_project_id")
return unless @titleField.length && @descriptionField.length
@initAutosave()
- @form.on "submit", @resetAutosave
+ @form.on "submit", @handleSubmit
@form.on "click", ".btn-cancel", @resetAutosave
+ @initWip()
+
initAutosave: ->
new Autosave @titleField, [
document.location.pathname,
@@ -27,6 +33,50 @@ class @IssuableForm
"description"
]
+ handleSubmit: =>
+ if (parseInt(@issueMoveField?.val()) ? 0) > 0
+ return false unless confirm(@issueMoveConfirmMsg)
+
+ @resetAutosave()
+
resetAutosave: =>
@titleField.data("autosave").reset()
@descriptionField.data("autosave").reset()
+
+ initWip: ->
+ @$wipExplanation = @form.find(".js-wip-explanation")
+ @$noWipExplanation = @form.find(".js-no-wip-explanation")
+ return unless @$wipExplanation.length and @$noWipExplanation.length
+
+ @form.on "click", ".js-toggle-wip", @toggleWip
+
+ @titleField.on "keyup blur", @renderWipExplanation
+
+ @renderWipExplanation()
+
+ workInProgress: ->
+ @wipRegex.test @titleField.val()
+
+ renderWipExplanation: =>
+ if @workInProgress()
+ @$wipExplanation.show()
+ @$noWipExplanation.hide()
+ else
+ @$wipExplanation.hide()
+ @$noWipExplanation.show()
+
+ toggleWip: (event) =>
+ event.preventDefault()
+
+ if @workInProgress()
+ @removeWip()
+ else
+ @addWip()
+
+ @renderWipExplanation()
+
+ removeWip: ->
+ @titleField.val @titleField.val().replace(@wipRegex, "")
+
+ addWip: ->
+ @titleField.val "WIP: #{@titleField.val()}"
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index d663e34871c..f50df1f5ea3 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -7,6 +7,7 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
@fixAffixScroll()
+ @initParticipants()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
@@ -84,3 +85,27 @@ class @Issue
type: 'PATCH'
url: $('form.js-issuable-update').attr('action')
data: patchData
+
+ initParticipants: ->
+ _this = @
+ $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+ $(".js-participants-author").each (i) ->
+ if i >= _this.PARTICIPANTS_ROW_COUNT
+ $(@)
+ .addClass "js-participants-hidden"
+ .hide()
+
+ toggleHiddenParticipants: (e) ->
+ e.preventDefault()
+
+ currentText = $(this).text().trim()
+ lessText = $(this).data("less-text")
+ originalText = $(this).data("original-text")
+
+ if currentText is originalText
+ $(this).text(lessText)
+ else
+ $(this).text(originalText)
+
+ $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
new file mode 100644
index 00000000000..c5740f27ddd
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -0,0 +1,11 @@
+class @IssueStatusSelect
+ constructor: ->
+ $('.js-issue-status').each (i, el) ->
+ fieldName = $(el).data("field-name")
+
+ $(el).glDropdown(
+ selectable: true
+ fieldName: fieldName
+ id: (obj, el) ->
+ $(el).data("id")
+ )
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index a0acf3028bf..1127b289264 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -41,24 +41,28 @@
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
- @timer = setTimeout(Issues.filterResults, 500)
+ @timer = setTimeout( ->
+ Issues.filterResults $("#issue_search_form")
+ , 500)
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
+ filterResults: (form) =>
+ $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
+ formAction = form.attr('action')
+ formData = form.serialize()
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
+ issuesUrl += formData
$.ajax
type: "GET"
- url: form.attr('action')
- data: form.serialize()
+ url: formAction
+ data: formData
complete: ->
- $('.issues-holder').css("opacity", '1.0')
+ $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
- $('.issues-holder').html(data.html)
+ $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
new file mode 100644
index 00000000000..4a0c18a99a6
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -0,0 +1,100 @@
+class @LabelsSelect
+ constructor: ->
+ $('.js-label-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ selectedLabel = $dropdown.data('selected')
+ if selectedLabel
+ selectedLabel = selectedLabel.split(',')
+ newLabelField = $('#new_label_name')
+ newColorField = $('#new_label_color')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ defaultLabel = $dropdown.data('default-label')
+
+ if newLabelField.length
+ $('.suggest-colors-dropdown a').on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ newColorField.val $(this).data('color')
+ $('.js-dropdown-label-color-preview')
+ .css 'background-color', $(this).data('color')
+ .addClass 'is-active'
+
+ $('.js-new-label-btn').on 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
+ $('.js-new-label-btn').disable()
+
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $('.js-new-label-btn').enable()
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: labelUrl
+ ).done (data) ->
+ if showNo
+ data.unshift(
+ id: 0
+ title: 'No Label'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Label'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+
+ callback data
+ renderRow: (label) ->
+ if $.isArray(selectedLabel)
+ selected = ''
+ $.each selectedLabel, (i, selectedLbl) ->
+ selectedLbl = selectedLbl.trim()
+ if selected is '' and label.title is selectedLbl
+ selected = 'is-active'
+ else
+ selected = if label.title is selectedLabel then 'is-active' else ''
+
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{label.title}
+ </a>
+ </li>"
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ id: (label) ->
+ if label.isAny?
+ ''
+ else
+ label.title
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
index 98fc8f17340..2a0b9479445 100644
--- a/app/assets/javascripts/markdown_preview.js.coffee
+++ b/app/assets/javascripts/markdown_preview.js.coffee
@@ -6,6 +6,7 @@
class @MarkdownPreview
# Minimum number of users referenced before triggering a warning
referenceThreshold: 10
+ ajaxCache: {}
showPreview: (form) ->
preview = form.find('.js-md-preview')
@@ -24,12 +25,16 @@ class @MarkdownPreview
renderMarkdown: (text, success) ->
return unless window.markdown_preview_path
+ return success(@ajaxCache.response) if text == @ajaxCache.text
+
$.ajax
type: 'POST'
url: window.markdown_preview_path
data: { text: text }
dataType: 'json'
- success: success
+ success: (response) =>
+ @ajaxCache = text: text, response: response
+ success(response)
hideReferencedUsers: (form) ->
referencedUsers = form.find('.referenced-users')
@@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview()
previewButtonSelector = '.js-md-preview-button'
writeButtonSelector = '.js-md-write-button'
+lastTextareaPreviewed = null
$.fn.setupMarkdownPreview = ->
$form = $(this)
@@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = ->
form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
form_textarea.on 'blur', -> markdownPreview.showPreview($form)
-$(document).on 'click', previewButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:show', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = $form.find('textarea.markdown-area')
# toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active')
@@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) ->
markdownPreview.showPreview($form)
-$(document).on 'click', writeButtonSelector, (e) ->
- e.preventDefault()
+$(document).on 'markdown-preview:hide', (e, $form) ->
+ return unless $form
- $form = $(this).closest('form')
+ lastTextareaPreviewed = null
# toggle tabs
$form.find(writeButtonSelector).parent().addClass('active')
@@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) ->
# toggle content
$form.find('.md-write-holder').show()
+ $form.find('textarea.markdown-area').focus()
$form.find('.md-preview-holder').hide()
+
+$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
+ $target = $(keyboardEvent.target)
+
+ if $target.is('textarea.markdown-area')
+ $(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+ else if lastTextareaPreviewed
+ $target = lastTextareaPreviewed
+ $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
+ keyboardEvent.preventDefault()
+
+$(document).on 'click', previewButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:show', [$form])
+
+$(document).on 'click', writeButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ $(document).triggerHandler('markdown-preview:hide', [$form])
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 58373ba87a5..839e6ec2c08 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -3,6 +3,8 @@
# Handles persisting and restoring the current tab selection and lazily-loading
# content on the MergeRequests#show page.
#
+#= require jquery.cookie
+#
# ### Example Markup
#
# <ul class="nav-links merge-request-tabs">
@@ -68,11 +70,15 @@ class @MergeRequestTabs
if action == 'commits'
@loadCommits($target.attr('href'))
+ @expandView()
else if action == 'diffs'
@loadDiff($target.attr('href'))
@shrinkView()
else if action == 'builds'
@loadBuilds($target.attr('href'))
+ @expandView()
+ else
+ @expandView()
@setCurrentAction(action)
@@ -189,12 +195,24 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited')
shrinkView: ->
- $gutterIcon = $('.gutter-toggle i')
+ $gutterIcon = $('.js-sidebar-toggle i:visible')
# Wait until listeners are set
setTimeout( ->
- # Only when sidebar is collapsed
+ # Only when sidebar is expanded
if $gutterIcon.is('.fa-angle-double-right')
- $gutterIcon.closest('a').trigger('click',[true])
+ $gutterIcon.closest('a').trigger('click', [true])
, 0)
+ # Expand the issuable sidebar unless the user explicitly collapsed it
+ expandView: ->
+ return if $.cookie('collapsed_gutter') == 'true'
+
+ $gutterIcon = $('.js-sidebar-toggle i:visible')
+
+ # Wait until listeners are set
+ setTimeout( ->
+ # Only when sidebar is collapsed
+ if $gutterIcon.is('.fa-angle-double-left')
+ $gutterIcon.closest('a').trigger('click', [true])
+ , 0)
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
index e6d8518bec8..0037a3a21c2 100644
--- a/app/assets/javascripts/milestone.js.coffee
+++ b/app/assets/javascripts/milestone.js.coffee
@@ -69,7 +69,7 @@ class @Milestone
@bindIssuesSorting()
@bindMergeRequestSorting()
- @bindTabsSwitching
+ @bindTabsSwitching()
bindIssuesSorting: ->
$("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
@@ -104,7 +104,7 @@ class @Milestone
).disableSelection()
- bindMergeRequestSorting: ->
+ bindTabsSwitching: ->
$('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
currentTabClass = $(e.target).data('show')
previousTabClass = $(e.relatedTarget).data('show')
@@ -112,7 +112,8 @@ class @Milestone
$(previousTabClass).hide()
$(currentTabClass).removeClass('hidden')
$(currentTabClass).show()
-
+
+ bindMergeRequestSorting: ->
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
connectWith: ".merge_requests-sortable-list",
dropOnEmpty: true,
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
new file mode 100644
index 00000000000..e17a1adb648
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -0,0 +1,65 @@
+class @MilestoneSelect
+ constructor: ->
+ $('.js-milestone-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ milestonesUrl = $dropdown.data('milestones')
+ selectedMilestone = $dropdown.data('selected')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ useId = $dropdown.data('use-id')
+ defaultLabel = $dropdown.data('default-label')
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: milestonesUrl
+ ).done (data) ->
+ if showNo
+ data.unshift(
+ id: '0'
+ title: 'No Milestone'
+ )
+
+ if showAny
+ data.unshift(
+ isAny: true
+ title: 'Any Milestone'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, 'divider'
+
+ callback(data)
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
+ text: (milestone) ->
+ milestone.title
+ id: (milestone) ->
+ if !useId
+ if !milestone.isAny?
+ milestone.title
+ else
+ ''
+ else
+ milestone.id
+ isSelected: (milestone) ->
+ milestone.title is selectedMilestone
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index c95ead22e6c..ff06c57f2b5 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -30,8 +30,11 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
+ # catch note ajax errors
+ $(document).on "ajax:error", ".js-main-target-form", @addNoteError
+
# change note in UI after update
- $(document).on "ajax:success", "form.edit_note", @updateNote
+ $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
@@ -51,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
+ # reset main target form when clicking discard
+ $(document).on "click", ".js-note-discard", @resetMainTargetForm
+
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
@@ -72,7 +78,7 @@ class @Notes
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
- $(document).off "ajax:success", "form.edit_note"
+ $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
@@ -85,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
+ $(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
@@ -219,7 +226,7 @@ class @Notes
Resets text and preview.
Resets buttons.
###
- resetMainTargetForm: ->
+ resetMainTargetForm: (e) =>
form = $(".js-main-target-form")
# remove validation errors
@@ -231,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
+ @updateTargetButtons(e)
+
reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form")
@@ -274,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
+ # hide discard button
+ form.find('.js-note-discard').hide()
+
# setup preview buttons
- form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text")
@@ -309,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) =>
@renderNote(note)
+ addNoteError: (xhr, note, status) =>
+ flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
+ flash.pinTo('.md-area')
+
###
Called in response to the new note form being submitted
@@ -328,6 +343,7 @@ class @Notes
updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html)
+ $('.js-timeago', $html).timeago()
$html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable')
@@ -345,24 +361,26 @@ class @Notes
showEditForm: (e) ->
e.preventDefault()
note = $(this).closest(".note")
- note.find(".note-body > .note-text").hide()
- note.find(".note-header").hide()
- base_form = note.find(".note-edit-form")
- form = base_form.clone().insertAfter(base_form)
- form.addClass('current-note-edit-form gfm-form')
- form.find('.div-dropzone').remove()
+ note.addClass "is-editting"
+ form = note.find(".note-edit-form")
+ isNewForm = form.is(':not(.gfm-form)')
+ if isNewForm
+ form.addClass('gfm-form')
+ form.addClass('current-note-edit-form')
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
- GitLab.GfmAutoComplete.setup()
- new DropzoneInput(form)
+ if isNewForm
+ GitLab.GfmAutoComplete.setup()
+ new DropzoneInput(form)
- form.show()
textarea = form.find("textarea")
textarea.focus()
- autosize(textarea)
+
+ if isNewForm
+ autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
@@ -371,7 +389,8 @@ class @Notes
textarea.val ""
textarea.val value
- disableButtonIfEmptyField textarea, form.find(".js-comment-button")
+ if isNewForm
+ disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
@@ -381,9 +400,9 @@ class @Notes
cancelEdit: (e) ->
e.preventDefault()
note = $(this).closest(".note")
- note.find(".note-body > .note-text").show()
- note.find(".note-header").show()
- note.find(".current-note-edit-form").remove()
+ note.removeClass "is-editting"
+ note.find(".current-note-edit-form")
+ .removeClass("current-note-edit-form")
###
Called in response to deleting a note of any kind.
@@ -462,6 +481,11 @@ class @Notes
form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId")
+ form.find('.js-note-discard')
+ .show()
+ .removeClass('js-note-discard')
+ .addClass('js-close-discussion-note-form')
+ .text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form
form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form"
@@ -561,21 +585,52 @@ class @Notes
updateCloseButton: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
- form.find('.js-note-target-close').text('Close')
+ closebtn = form.find('.js-note-target-close')
+ closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
+ reopenbtn = form.find('.js-note-target-reopen')
+ closebtn = form.find('.js-note-target-close')
+ discardbtn = form.find('.js-note-discard')
+
if textarea.val().trim().length > 0
- form.find('.js-note-target-reopen').text('Comment & reopen')
- form.find('.js-note-target-close').text('Comment & close')
- form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
- form.find('.js-note-target-close').addClass('btn-comment-and-close')
+ reopentext = reopenbtn.data('alternative-text')
+ closetext = closebtn.data('alternative-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ reopenbtn.addClass('btn-comment-and-reopen')
+
+ if closebtn.is(':not(.btn-comment-and-close)')
+ closebtn.addClass('btn-comment-and-close')
+
+ if discardbtn.is(':hidden')
+ discardbtn.show()
else
- form.find('.js-note-target-reopen').text('Reopen')
- form.find('.js-note-target-close').text('Close')
- form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
- form.find('.js-note-target-close').removeClass('btn-comment-and-close')
+ reopentext = reopenbtn.data('original-text')
+ closetext = closebtn.data('original-text')
+
+ if reopenbtn.text() isnt reopentext
+ reopenbtn.text(reopentext)
+
+ if closebtn.text() isnt closetext
+ closebtn.text(closetext)
+
+ if reopenbtn.is('.btn-comment-and-reopen')
+ reopenbtn.removeClass('btn-comment-and-reopen')
+
+ if closebtn.is('.btn-comment-and-close')
+ closebtn.removeClass('btn-comment-and-close')
+
+ if discardbtn.is(':visible')
+ discardbtn.hide()
initTaskList: ->
@enableTaskList()
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 9110b732adc..20f87440551 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -4,64 +4,27 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
- $('.update-username form').on 'ajax:before', ->
- $('.loading-gif').show()
+ $('.update-username').on 'ajax:before', ->
+ $('.loading-username').show()
$(this).find('.update-success').hide()
$(this).find('.update-failed').hide()
- $('.update-username form').on 'ajax:complete', ->
+ $('.update-username').on 'ajax:complete', ->
+ $('.loading-username').hide()
$(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide()
$('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable()
- # Avatar management
-
- $avatarInput = $('.js-user-avatar-input')
- $filename = $('.js-avatar-filename')
- $modalCrop = $('.modal-profile-crop')
- $modalCropImg = $('.modal-profile-crop-image')
-
- $('.js-choose-user-avatar-button').on "click", ->
- $form = $(this).closest("form")
- $form.find(".js-user-avatar-input").click()
-
- $modalCrop.on 'shown.bs.modal', ->
- setTimeout ( -> # The cropper must be asynchronously initialized
- $modalCropImg.cropper
- aspectRatio: 1
- modal: false
- scalable: false
- rotatable: false
- zoomable: false
-
- crop: (event) ->
- ['x', 'y'].forEach (key) ->
- $("#user_avatar_crop_#{key}").val(Math.floor(event[key]))
- $("#user_avatar_crop_size").val(Math.floor(event.width))
- ), 0
-
- $modalCrop.on 'hidden.bs.modal', ->
- $modalCropImg.attr('src', '').cropper('destroy')
- $avatarInput.val('')
- $filename.text($filename.data('label'))
-
- $('.js-upload-user-avatar').on 'click', ->
- $('.edit-user').submit()
+ $('.js-choose-user-avatar-button').bind "click", ->
+ form = $(this).closest("form")
+ form.find(".js-user-avatar-input").click()
- $avatarInput.on "change", ->
+ $('.js-user-avatar-input').bind "change", ->
form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '')
- $filename.data('label', $filename.text()).text(filename)
-
- reader = new FileReader
-
- reader.onload = (event) ->
- $modalCrop.modal('show')
- $modalCropImg.attr('src', event.target.result)
-
- fileData = reader.readAsDataURL(this.files[0])
+ form.find(".js-avatar-filename").text(filename)
$ ->
# Extract the SSH Key title from its comment
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 76bc4ff42a2..87d313ed67c 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -11,7 +11,6 @@ class @Project
$(@).toggleClass('active')
url = $("#project_clone").val()
- console.log("url",url)
# Update the input field
$('#project_clone').val(url)
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
index fecdb9fc2e7..63dee4ed5d7 100644
--- a/app/assets/javascripts/project_new.js.coffee
+++ b/app/assets/javascripts/project_new.js.coffee
@@ -3,3 +3,16 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide()
$('.save-project-loader').show()
+ @toggleSettings()
+ @toggleSettingsOnclick()
+
+
+ toggleSettings: ->
+ checked = $("#project_builds_enabled").prop("checked")
+ if checked
+ $('.builds-feature').show()
+ else
+ $('.builds-feature').hide()
+
+ toggleSettingsOnclick: ->
+ $("#project_builds_enabled").on 'click', @toggleSettings
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
index ed5206368ce..e4c4bf3b273 100644
--- a/app/assets/javascripts/projects_list.js.coffee
+++ b/app/assets/javascripts/projects_list.js.coffee
@@ -2,6 +2,7 @@
init: ->
$(".projects-list-filter").off('keyup')
this.initSearch()
+ this.initPagination()
initSearch: ->
@timer = null
@@ -29,3 +30,8 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json"
+
+ initPagination: ->
+ $('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ )
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index 9c7c2474aa4..100e3aac535 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -4,11 +4,15 @@ class @Shortcuts
Mousetrap.reset()
Mousetrap.bind('?', @selectiveHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
+ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
+ toggleMarkdownPreview: (e) =>
+ $(document).triggerHandler('markdown-preview:toggle', [e])
+
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
@@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) ->
$(@).remove()
$('.hidden-shortcut').show()
e.preventDefault()
+
+Mousetrap.stopCallback = (->
+ defaultStopCallback = Mousetrap.stopCallback
+
+ return (e, element, combo) ->
+ # allowed shortcuts if textarea, input, contenteditable are focused
+ if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
+ return false
+ else
+ return defaultStopCallback.apply(@, arguments)
+)()
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index cff309c5972..eea3f5ee910 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -1,8 +1,7 @@
-$(document).on("click", '.toggle-nav-collapse', (e) ->
- e.preventDefault()
- collapsed = 'page-sidebar-collapsed'
- expanded = 'page-sidebar-expanded'
+collapsed = 'page-sidebar-collapsed'
+expanded = 'page-sidebar-expanded'
+toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
niceScrollBars.updateScrollBar();
), 300
+$(document).on("click", '.toggle-nav-collapse', (e) ->
+ e.preventDefault()
+
+ toggleSidebar()
)
+
+$ ->
+ size = bp.getBreakpointSize()
+
+ if size is "xs" or size is "sm"
+ if $('.page-with-sidebar').hasClass(expanded)
+ toggleSidebar()
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
index 7f41616d4e7..084f0e0dc65 100644
--- a/app/assets/javascripts/subscription.js.coffee
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -1,17 +1,21 @@
class @Subscription
- constructor: (url) ->
- $(".subscribe-button").unbind("click").click (event)=>
- btn = $(event.currentTarget)
- action = btn.find("span").text()
- current_status = $(".subscription-status").attr("data-status")
- btn.prop("disabled", true)
-
- $.post url, =>
- btn.prop("disabled", false)
- status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
- $(".subscription-status").attr("data-status", status)
- action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
- btn.find("span").text(action)
- $(".subscription-status>div").toggleClass("hidden")
+ constructor: (container) ->
+ $container = $(container)
+ @url = $container.attr('data-url')
+ @subscribe_button = $container.find('.subscribe-button')
+ @subscription_status = $container.find('.subscription-status')
+ @subscribe_button.unbind('click').click(@toggleSubscription)
-
+ toggleSubscription: (event) =>
+ btn = $(event.currentTarget)
+ action = btn.find('span').text()
+ current_status = @subscription_status.attr('data-status')
+ btn.prop('disabled', true)
+
+ $.post @url, =>
+ btn.prop('disabled', false)
+ status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
+ @subscription_status.attr('data-status', status)
+ action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
+ btn.find('span').text(action)
+ @subscription_status.find('>div').toggleClass('hidden')
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..b6b4bd90e6a
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,56 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+
+ doneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ @clearDone $this.closest('li')
+ @updateBadges data
+
+ allDoneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ $this.remove()
+ $('.js-todos-list').remove()
+ @updateBadges data
+
+ clearDone: ($row) ->
+ $ul = $row.closest('ul')
+ $row.remove()
+
+ if not $ul.find('li').length
+ $ul.parents('.panel').remove()
+
+ updateBadges: (data) ->
+ $('.todos-pending .badge, .todos-pending-count').text data.count
+ $('.todos-done .badge').text data.done_count
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9467011799f..3d6452d2f46 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -3,6 +3,94 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ $('.js-user-search').each (i, dropdown) =>
+ $dropdown = $(dropdown)
+ @projectId = $dropdown.data('project-id')
+ @showCurrentUser = $dropdown.data('current-user')
+ showNullUser = $dropdown.data('null-user')
+ showAnyUser = $dropdown.data('any-user')
+ firstUser = $dropdown.data('first-user')
+ selectedId = $dropdown.data('selected')
+ defaultLabel = $dropdown.data('default-label')
+
+ $dropdown.glDropdown(
+ data: (term, callback) =>
+ @users term, (users) =>
+ if term.length is 0
+ showDivider = 0
+
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in users
+ if obj.username == firstUser
+ users.splice(index, 1)
+ users.unshift(obj)
+ break
+
+ if showNullUser
+ showDivider += 1
+ users.unshift(
+ name: 'Unassigned',
+ id: 0
+ )
+
+ if showAnyUser
+ showDivider += 1
+ name = showAnyUser
+ name = 'Any User' if name == true
+ anyUser = {
+ name: name,
+ id: null
+ }
+ users.unshift(anyUser)
+
+ if showDivider
+ users.splice(showDivider, 0, "divider")
+
+ # Send the data back
+ callback users
+ filterable: true
+ filterRemote: true
+ search:
+ fields: ['name', 'username']
+ selectable: true
+ fieldName: $dropdown.data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
+ clicked: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
+ renderRow: (user) ->
+ username = if user.username then "@#{user.username}" else ""
+ avatar = if user.avatar_url then user.avatar_url else false
+ selected = if user.id is selectedId then "is-active" else ""
+ img = ""
+
+ if avatar
+ img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+
+ "<li>
+ <a href='#' class='dropdown-menu-user-link #{selected}'>
+ #{img}
+ <strong class='dropdown-menu-user-full-name'>
+ #{user.name}
+ </strong>
+ <span class='dropdown-menu-user-username'>
+ #{username}
+ </span>
+ </a>
+ </li>"
+ )
+
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')