diff options
Diffstat (limited to 'app')
118 files changed, 1339 insertions, 505 deletions
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index f3ed9a66715..dd1bbb37551 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,6 +5,7 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" + license_path: "/api/:version/licenses/:key" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -92,6 +93,16 @@ ).done (projects) -> callback(projects) + # Return text for a specific license + licenseText: (key, data, callback) -> + url = Api.buildUrl(Api.license_path).replace(':key', key) + + $.ajax( + url: url + data: data + ).done (license) -> + callback(license) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 5bac8eef1cb..642e7429acf 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -55,6 +55,7 @@ #= require_tree . #= require fuzzaldrin-plus #= require cropper +#= require raven window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee new file mode 100644 index 00000000000..e17eaa75dc1 --- /dev/null +++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee @@ -0,0 +1,30 @@ +class @BlobLicenseSelector + licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i + + constructor: (editor) -> + @$licenseSelector = $('.js-license-selector') + $fileNameInput = $('#file_name') + + initialFileNameValue = if $fileNameInput.length + $fileNameInput.val() + else if $('.editor-file-name').length + $('.editor-file-name').text().trim() + + @toggleLicenseSelector(initialFileNameValue) + + if $fileNameInput + $fileNameInput.on 'keyup blur', (e) => + @toggleLicenseSelector($(e.target).val()) + + $('select.license-select').on 'change', (e) -> + data = + project: $(this).data('project') + fullname: $(this).data('fullname') + Api.licenseText $(this).val(), data, (license) -> + editor.setValue(license.content, -1) + + toggleLicenseSelector: (fileName) => + if @licenseRegex.test(fileName) + @$licenseSelector.show() + else + @$licenseSelector.hide() diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 390e41ed8d4..eea9aa972ee 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -1,44 +1,39 @@ class @EditBlob - constructor: (assets_path, mode)-> - ace.config.set "modePath", assets_path + '/ace' + constructor: (assets_path, ace_mode = null) -> + ace.config.set "modePath", "#{assets_path}/ace" ace.config.loadModule "ace/ext/searchbox" - if mode - ace_mode = mode - editor = ace.edit("editor") - editor.focus() - @editor = editor - - if ace_mode - editor.getSession().setMode "ace/mode/" + ace_mode + @editor = ace.edit("editor") + @editor.focus() + @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode # Before a form submission, move the content from the Ace editor into the # submitted textarea - $('form').submit -> - $("#file-content").val(editor.getValue()) + $('form').submit => + $("#file-content").val(@editor.getValue()) + + @initModePanesAndLinks() + new BlobLicenseSelector(@editor) - editModePanes = $(".js-edit-mode-pane") - editModeLinks = $(".js-edit-mode a") - editModeLinks.click (event) -> - event.preventDefault() - currentLink = $(this) - paneId = currentLink.attr("href") - currentPane = editModePanes.filter(paneId) - editModeLinks.parent().removeClass "active hover" - currentLink.parent().addClass "active hover" - editModePanes.hide() - if paneId is "#preview" - currentPane.fadeIn 200 - $.post currentLink.data("preview-url"), - content: editor.getValue() - , (response) -> - currentPane.empty().append response - currentPane.syntaxHighlight() - return + initModePanesAndLinks: -> + @$editModePanes = $(".js-edit-mode-pane") + @$editModeLinks = $(".js-edit-mode a") + @$editModeLinks.click @editModeLinkClickHandler - else - currentPane.fadeIn 200 - editor.focus() - return + editModeLinkClickHandler: (event) => + event.preventDefault() + currentLink = $(event.target) + paneId = currentLink.attr("href") + currentPane = @$editModePanes.filter(paneId) + @$editModeLinks.parent().removeClass "active hover" + currentLink.parent().addClass "active hover" + @$editModePanes.hide() + currentPane.fadeIn 200 + if paneId is "#preview" + $.post currentLink.data("preview-url"), + content: @editor.getValue() + , (response) -> + currentPane.empty().append response + currentPane.syntaxHighlight() - editor: -> - return @editor + else + @editor.focus() diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee deleted file mode 100644 index 68c5e5195e3..00000000000 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @NewBlob - constructor: (assets_path, mode)-> - ace.config.set "modePath", assets_path + '/ace' - ace.config.loadModule "ace/ext/searchbox" - if mode - ace_mode = mode - editor = ace.edit("editor") - editor.focus() - @editor = editor - - if ace_mode - editor.getSession().setMode "ace/mode/" + ace_mode - - # Before a form submission, move the content from the Ace editor into the - # submitted textarea - $('form').submit -> - $("#file-content").val(editor.getValue()) - - editor: -> - return @editor diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 0b9110d35fa..2fdb7562515 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -17,6 +17,7 @@ class Dispatcher switch page when 'projects:issues:index' Issues.init() + Issuable.init() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() @@ -57,7 +58,7 @@ class Dispatcher new ZenMode() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() - MergeRequests.init() + Issuable.init() when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee new file mode 100644 index 00000000000..a4304786cbb --- /dev/null +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -0,0 +1,64 @@ +class @DueDateSelect + constructor: -> + $loading = $('.js-issuable-update .due_date') + .find('.block-loading') + .hide() + + $('.js-due-date-select').each (i, dropdown) -> + $dropdown = $(dropdown) + $dropdownParent = $dropdown.closest('.dropdown') + $datePicker = $dropdownParent.find('.js-due-date-calendar') + $block = $dropdown.closest('.block') + $selectbox = $dropdown.closest('.selectbox') + $value = $block.find('.value') + $sidebarValue = $('.js-due-date-sidebar-value', $block) + + fieldName = $dropdown.data('field-name') + abilityName = $dropdown.data('ability-name') + issueUpdateURL = $dropdown.data('issue-update') + + $dropdown.glDropdown( + hidden: -> + $selectbox.hide() + $value.removeAttr('style') + ) + + addDueDate = -> + # Create the post date + value = $("input[name='#{fieldName}']").val() + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + + data = {} + data[abilityName] = {} + data[abilityName].due_date = value + + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + beforeSend: -> + $loading.fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() + $value.removeAttr('style') + + $value.html(mediumDate) + $sidebarValue.html(mediumDate) + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') + $loading.fadeOut() + + $datePicker.datepicker( + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='#{fieldName}']").val() + altField: "input[name='#{fieldName}']" + onSelect: -> + addDueDate() + ) + + $(document) + .off 'click', '.ui-datepicker-header a' + .on 'click', '.ui-datepicker-header a', (e) -> + e.stopImmediatePropagation() diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 641141072f4..e5204f9dee9 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -386,13 +386,13 @@ class GitLabDropdown else selectedObject else - if !value? - field.remove() - - if not @options.multiSelect + if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.parent().find("input[name='#{fieldName}']").remove() + if !value? + field.remove() + # Toggle active class for the tick mark el.addClass ACTIVE_CLASS diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee new file mode 100644 index 00000000000..afffed63ac5 --- /dev/null +++ b/app/assets/javascripts/issuable.js.coffee @@ -0,0 +1,84 @@ +@Issuable = + init: -> + Issuable.initTemplates() + Issuable.initSearch() + + initTemplates: -> + Issuable.labelRow = _.template( + '<% _.each(labels, function(label){ %> + <span class="label-row"> + <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a> + </span> + <% }); %>' + ) + + initSearch: -> + @timer = null + $('#issue_search') + .off 'keyup' + .on 'keyup', -> + clearTimeout(@timer) + @timer = setTimeout( -> + Issuable.filterResults $('#issue_search_form') + , 500) + + toggleLabelFilters: -> + $filteredLabels = $('.filtered-labels') + if $filteredLabels.find('.label-row').length > 0 + $filteredLabels.removeClass('hidden') + else + $filteredLabels.addClass('hidden') + + filterResults: (form) => + formData = form.serialize() + + $('.issues-holder, .merge-requests-holder').css('opacity', '0.5') + formAction = form.attr('action') + issuesUrl = formAction + issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") + issuesUrl += formData + $.ajax + type: 'GET' + url: formAction + data: formData + complete: -> + $('.issues-holder, .merge-requests-holder').css('opacity', '1.0') + success: (data) -> + $('.issues-holder, .merge-requests-holder').html(data.html) + # Change url so if user reload a page - search results are saved + history.replaceState {page: issuesUrl}, document.title, issuesUrl + Issuable.reload() + Issuable.updateStateFilters() + $filteredLabels = $('.filtered-labels') + + if typeof Issuable.labelRow is 'function' + $filteredLabels.html(Issuable.labelRow(data)) + + Issuable.toggleLabelFilters() + + dataType: "json" + + reload: -> + if Issues.created + Issues.initChecks() + + $('#filter_issue_search').val($('#issue_search').val()) + + updateStateFilters: -> + stateFilters = $('.issues-state-filters') + newParams = {} + paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search'] + + for paramKey in paramKeys + newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' + + if stateFilters.length + stateFilters.find('a').each -> + initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]') + labelNameValues = gl.utils.getParameterValues('label_name[]') + if labelNameValues + labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&') + newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}" + else + newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) + $(this).attr 'href', newUrl diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 2f19513a831..3a439b94c59 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -9,21 +9,29 @@ class @IssuableContext $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() - $(document).off("click", ".edit-link").on "click",".edit-link", (e) -> - $block = $(@).parents('.block') - $selectbox = $block.find('.selectbox') - if $selectbox.is(':visible') - $selectbox.hide() - $block.find('.value').show() - else - $selectbox.show() - $block.find('.value').hide() - - if $selectbox.is(':visible') - setTimeout (-> - $block.find('.dropdown-menu-toggle').trigger 'click' - ), 0 - + $(document) + .off 'click', '.dropdown-content a' + .on 'click', '.dropdown-content a', (e) -> + e.preventDefault() + + $(document) + .off 'click', '.edit-link' + .on 'click', '.edit-link', (e) -> + e.preventDefault() + + $block = $(@).parents('.block') + $selectbox = $block.find('.selectbox') + if $selectbox.is(':visible') + $selectbox.hide() + $block.find('.value').show() + else + $selectbox.show() + $block.find('.value').hide() + + if $selectbox.is(':visible') + setTimeout -> + $block.find('.dropdown-menu-toggle').trigger 'click' + , 0 $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 0d9f2094c2a..3330e6c68ad 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -1,6 +1,6 @@ @Issues = init: -> - Issues.initSearch() + Issues.created = true Issues.initChecks() $("body").on "ajax:success", ".close_issue, .reopen_issue", -> @@ -15,10 +15,6 @@ else $(this).html totalIssues - 1 - reload: -> - Issues.initChecks() - $('#filter_issue_search').val($('#issue_search').val()) - initChecks: -> $(".check_all_issues").click -> $(".selected_issue").prop("checked", @checked) @@ -26,51 +22,6 @@ $(".selected_issue").bind "change", Issues.checkChanged - # Update state filters if present in page - updateStateFilters: -> - stateFilters = $('.issues-state-filters') - newParams = {} - paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] - - for paramKey in paramKeys - newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or '' - - if stateFilters.length - stateFilters.find('a').each -> - initialUrl = $(this).attr 'href' - $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl) - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout( -> - Issues.filterResults $("#issue_search_form") - , 500) - - filterResults: (form) => - $('.issues-holder, .merge-requests-holder').css("opacity", '0.5') - formAction = form.attr('action') - formData = form.serialize() - issuesUrl = formAction - issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}") - issuesUrl += formData - - $.ajax - type: "GET" - url: formAction - data: formData - complete: -> - $('.issues-holder, .merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.issues-holder, .merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issuesUrl}, document.title, issuesUrl - Issues.reload() - Issues.updateStateFilters() - dataType: "json" - checkChanged: -> checked_issues = $(".selected_issue:checked") if checked_issues.length > 0 diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index bc80980acb7..021ade73d44 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,7 +6,7 @@ class @LabelsSelect labelUrl = $dropdown.data('labels') issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel? + if selectedLabel? and not $dropdown.hasClass 'js-multiselect' selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') @@ -16,6 +16,7 @@ class @LabelsSelect abilityName = $dropdown.data('ability-name') $selectbox = $dropdown.closest('.selectbox') $block = $selectbox.closest('.block') + $form = $dropdown.closest('form') $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') $value = $block.find('.value') $loading = $block.find('.block-loading').fadeOut() @@ -33,13 +34,13 @@ class @LabelsSelect if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> - <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> - <span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;"> - <%= label.title %> + <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>"> + <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>;"> + <%= _.escape(label.title) %> </span> </a> <% }); %>' - ); + ) labelNoneHTMLTemplate = _.template('<div class="light">None</div>') if newLabelField.length and $dropdown.hasClass 'js-extra-options' @@ -171,7 +172,7 @@ class @LabelsSelect .find('a') .each((i) -> setTimeout(=> - glAnimate($(@), 'pulse') + gl.animate.animate($(@), 'pulse') ,200 * i ) ) @@ -200,18 +201,23 @@ class @LabelsSelect callback data renderRow: (label) -> - selectedClass = '' - if $selectbox.find("input[type='hidden']\ - [name='#{$dropdown.data('field-name')}']\ - [value='#{label.id}']").length - selectedClass = 'is-active' + removesAll = label.id is 0 or not label.id? + + selectedClass = [] + if $form.find("input[type='hidden']\ + [name='#{$dropdown.data('fieldName')}']\ + [value='#{this.id(label)}']").length + selectedClass.push 'is-active' + + if $dropdown.hasClass('js-multiselect') and removesAll + selectedClass.push 'dropdown-clear-active' color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" "<li> - <a href='#' class='#{selectedClass}'> + <a href='#' class='#{selectedClass.join(' ')}'> #{color} - #{label.title} + #{_.escape(label.title)} </a> </li>" filterable: true @@ -219,37 +225,56 @@ class @LabelsSelect fields: ['title'] selectable: true - toggleLabel: (selected) -> + toggleLabel: (selected, el) -> + selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') + if selected and selected.title? - selected.title + if selected_labels.length > 1 + "#{selected.title} +#{selected_labels.length - 1} more" + else + selected.title + else if not selected and selected_labels.length isnt 0 + if selected_labels.length > 1 + "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more" + else if selected_labels.length is 1 + $(selected_labels).text() else defaultLabel fieldName: $dropdown.data('field-name') id: (label) -> - if label.isAny? - '' - else if $dropdown.hasClass "js-filter-submit" + if $dropdown.hasClass("js-filter-submit") and not label.isAny? label.title else label.id hidden: -> + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is 'projects:merge_requests:index' + $selectbox.hide() # display:block overrides the hide-collapse rule $value.removeAttr('style') if $dropdown.hasClass 'js-multiselect' - saveLabelData() + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedLabels = $dropdown + .closest('form') + .find("input:hidden[name='#{$dropdown.data('fieldName')}']") + Issuable.filterResults $dropdown.closest('form') + else if $dropdown.hasClass('js-filter-submit') + $dropdown.closest('form').submit() + else + saveLabelData() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' - + isMRIndex = page is 'projects:merge_requests:index' if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) - selectedLabel = label.title - - Issues.filterResults $dropdown.closest('form') + if not $dropdown.hasClass 'js-multiselect' + selectedLabel = label.title + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee index 8f892b5a2b9..ec3b44d6126 100644 --- a/app/assets/javascripts/lib/animate.js.coffee +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -1,13 +1,39 @@ ((w) -> + if not w.gl? then w.gl = {} + if not gl.animate? then gl.animate = {} - w.glAnimate = ($el, animation, done) -> + gl.animate.animate = ($el, animation, options, done) -> + if options?.cssStart? + $el.css(options.cssStart) $el - .removeClass() + .removeClass(animation + ' animated') .addClass(animation + ' animated') .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> - $(this).removeClass() + $(this).removeClass(animation + ' animated') + if done? + done() + if options?.cssEnd? + $el.css(options.cssEnd) return return - return + gl.animate.animateEach = ($els, animation, time, options, done) -> + dfd = $.Deferred() + if not $els.length + dfd.resolve() + $els.each((i) -> + setTimeout(=> + $this = $(@) + gl.animate.animate($this, animation, options, => + if i is $els.length - 1 + dfd.resolve() + if done? + done() + ) + ,time * i + ) + return + ) + return dfd.promise() + return ) window
\ No newline at end of file diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee index abd556e0b4e..6a00932c028 100644 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -3,16 +3,20 @@ w.gl ?= {} w.gl.utils ?= {} - w.gl.utils.getUrlParameter = (sParam) -> + # Returns an array containing the value(s) of the + # of the key passed as an argument + w.gl.utils.getParameterValues = (sParam) -> sPageURL = decodeURIComponent(window.location.search.substring(1)) sURLVariables = sPageURL.split('&') sParameterName = undefined + values = [] i = 0 while i < sURLVariables.length sParameterName = sURLVariables[i].split('=') if sParameterName[0] is sParam - return if sParameterName[1] is undefined then true else sParameterName[1] + values.push(sParameterName[1]) i++ + values # # # @param {Object} params - url keys and value to merge @@ -28,4 +32,12 @@ newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" newUrl + # removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = (url, param) -> + url = decodeURIComponent(url) + urlVariables = url.split('&') + ( + variables for variables in urlVariables when variables.indexOf(param) is -1 + ).join('&') + ) window diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee deleted file mode 100644 index b3c73ffce5d..00000000000 --- a/app/assets/javascripts/merge_requests.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -# -# * Filter merge requests -# -@MergeRequests = - init: -> - MergeRequests.initSearch() - - # Make sure we trigger ajax request only after user stop typing - initSearch: -> - @timer = null - $("#issue_search").keyup -> - clearTimeout(@timer) - @timer = setTimeout(MergeRequests.filterResults, 500) - - filterResults: => - form = $("#issue_search_form") - search = $("#issue_search").val() - $('.merge-requests-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '?' + form.serialize() - - $.ajax - type: "GET" - url: form.attr('action') - data: form.serialize() - complete: -> - $('.merge-requests-holder').css("opacity", '1.0') - success: (data) -> - $('.merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issues_url}, document.title, issues_url - MergeRequests.reload() - dataType: "json" - - reload: -> - $('#filter_issue_search').val($('#issue_search').val()) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 6bd4e885a03..345a0e447af 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -24,7 +24,7 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( - '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>' + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>' ) milestoneLinkNoneTemplate = '<div class="light">None</div>' @@ -71,7 +71,7 @@ class @MilestoneSelect defaultLabel fieldName: $dropdown.data('field-name') text: (milestone) -> - milestone.title + _.escape(milestone.title) id: (milestone) -> if !useId milestone.name @@ -97,7 +97,7 @@ class @MilestoneSelect selectedMilestone = selected.name else selectedMilestone = '' - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else diff --git a/app/assets/javascripts/raven_config.js.coffee b/app/assets/javascripts/raven_config.js.coffee new file mode 100644 index 00000000000..d031a655abf --- /dev/null +++ b/app/assets/javascripts/raven_config.js.coffee @@ -0,0 +1,44 @@ +@raven = + init: -> + if gon.sentry_dsn? + Raven.config(gon.sentry_dsn, { + includePaths: [/gon.relative_url_root/] + ignoreErrors: [ + # Random plugins/extensions + 'top.GLOBALS', + # See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html + 'originalCreateNotification', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'http://tt.epicplay.com', + 'Can\'t find variable: ZiteReader', + 'jigsaw is not defined', + 'ComboSearch is not defined', + 'http://loading.retry.widdit.com/', + 'atomicFindClose', + # ISP "optimizing" proxy - `Cache-Control: no-transform` seems to + # reduce this. (thanks @acdha) + # See http://stackoverflow.com/questions/4113268 + 'bmi_SafeAddOnload', + 'EBCallBackMessageReceived', + # See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + 'conduitPage' + ], + ignoreUrls: [ + # Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + # Other plugins + /127\.0\.0\.1:4001\/isrunning/i, # Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i + ] + }).install() + + if gon.current_user_id + Raven.setUserContext({ + id: gon.current_user_id + }) + +$ -> + raven.init() diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index 00d2b641723..10e698d6a54 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -1,5 +1,11 @@ class @Todos - constructor: (@name) -> + constructor: (opts = {}) -> + { + @el = $('.js-todos-options') + } = opts + + @perPage = @el.data('perPage') + @clearListeners() @initBtnListeners() @@ -26,6 +32,7 @@ class @Todos dataType: 'json' data: '_method': 'delete' success: (data) => + @redirectIfNeeded data.count @clearDone $this.closest('li') @updateBadges data @@ -57,6 +64,40 @@ class @Todos $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + getTotalPages: -> + @el.data('totalPages') + + getCurrentPage: -> + @el.data('currentPage') + + getTodosPerPage: -> + @el.data('perPage') + + redirectIfNeeded: (total) -> + currPages = @getTotalPages() + currPage = @getCurrentPage() + + # Refresh if no remaining Todos + if not total + location.reload() + return + + # Do nothing if no pagination + return if not currPages + + newPages = Math.ceil(total / @getTodosPerPage()) + url = location.href # Includes query strings + + # If new total of pages is different than we have now + if newPages isnt currPages + # Redirect to previous page if there's one available + if currPages > 1 and currPage is currPages + pageParams = + page: currPages - 1 + url = gl.utils.mergeUrlParams(pageParams, url) + + Turbolinks.visit(url) + goToTodoUrl: (e)-> todoLink = $(this).data('url') return unless todoLink diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index a7e934936e9..b80b1b861cc 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -158,7 +158,7 @@ class @UsersSelect if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) selectedId = user.id - Issues.filterResults $dropdown.closest('form') + Issuable.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() else diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ba6c7930cdc..fd395041c3d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -248,7 +248,7 @@ .dropdown-title { position: relative; - padding: 0 25px 15px; + padding: 0 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -278,7 +278,7 @@ right: 5px; width: 20px; height: 20px; - top: -1px; + top: -3px; } .dropdown-menu-back { @@ -358,6 +358,13 @@ border-top: 1px solid $dropdown-divider-color; } +.dropdown-due-date-footer { + padding-top: 0; + margin-left: 10px; + margin-right: 10px; + border-top: 0; +} + .dropdown-footer-list { font-size: 14px; @@ -395,3 +402,122 @@ height: 15px; border-radius: $border-radius-base; } + +.dropdown-menu-due-date { + .dropdown-content { + max-height: 230px; + } + + .ui-widget { + table { + margin: 0; + } + + &.ui-datepicker-inline { + padding: 0 10px; + border: 0; + width: 100%; + } + + .ui-datepicker-header { + padding: 0 8px 10px; + border: 0; + + .ui-icon { + background: none; + font-size: 20px; + text-indent: 0; + + &:before { + display: block; + position: relative; + top: -2px; + color: $dropdown-title-btn-color; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + } + } + + .ui-state-active, + .ui-state-hover { + color: $md-link-color; + background-color: $calendar-hover-bg; + } + + .ui-datepicker-prev, + .ui-datepicker-next { + top: 0; + height: 15px; + cursor: pointer; + + &:hover { + background-color: transparent; + border: 0; + + .ui-icon:before { + color: $md-link-color; + } + } + } + + .ui-datepicker-prev { + left: 0; + + .ui-icon:before { + content: '\f104'; + text-align: left; + } + } + + .ui-datepicker-next { + right: 0; + + .ui-icon:before { + content: '\f105'; + text-align: right; + } + } + + td { + padding: 0; + border: 1px solid $calendar-border-color; + + &:first-child { + border-left: 0; + } + + &:last-child { + border-right: 0; + } + + a { + line-height: 17px; + border: 0; + border-radius: 0; + } + } + + .ui-datepicker-title { + color: $gl-gray; + font-size: 15px; + line-height: 1; + font-weight: normal; + } + } + + th { + padding: 2px 0; + color: $calendar-header-color; + font-weight: normal; + text-transform: lowercase; + border-top: 1px solid $calendar-border-color; + } + + .ui-datepicker-unselectable { + background-color: $calendar-unselectable-bg; + } +} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 0f32d36d59c..704fa1ff800 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -90,3 +90,12 @@ box-shadow: none; width: 100%; } + +.md { + &.md-preview-holder { + code { + white-space: pre-wrap; + word-break: break-all; + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f910cf61817..30ca27ab104 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5; $note-toolbar-color: #959494; $zen-control-hover-color: #111; + +$calendar-header-color: #b8b8b8; +$calendar-hover-bg: #ecf3fe; +$calendar-border-color: rgba(#000, .1); +$calendar-unselectable-bg: #faf9f9; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 2c2ac903f29..751a5ab4d92 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -36,4 +36,10 @@ } } } + + .wiki { + code { + white-space: pre-wrap; + } + } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 183f22a1b24..e7c8198ba45 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -34,6 +34,7 @@ background: #fff; color: #333; border-radius: 0 0 3px 3px; + -webkit-overflow-scrolling: auto; .unfold { cursor: pointer; @@ -86,7 +87,7 @@ } span { - white-space: pre; + white-space: pre-wrap; } } } @@ -335,7 +336,7 @@ } .diff-file .line_content { - white-space: pre; + white-space: pre-wrap; } .diff-wrap-lines .line_content { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 0f0592a0ab8..8981f070a20 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -26,6 +26,10 @@ line-height: 42px; padding-top: 7px; padding-bottom: 7px; + + .pull-right { + height: 20px; + } } .editor-ref { @@ -53,4 +57,9 @@ .select2 { float: right; } + + .encoding-selector, + .license-selector { + display: inline-block; + } } diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 604f1700cf8..ee95bdf488e 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,8 +59,10 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { margin: -$gl-padding+1; + margin-top: 15px; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 5bf44c1cdb6..1d6190c8f18 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -128,6 +128,7 @@ top: 58px; bottom: 0; right: 0; + z-index: 10; transition: width .3s; background: $gray-light; padding: 10px 20px; @@ -241,7 +242,7 @@ } } - .btn { + .issuable-pager { background: $gray-normal; border: 1px solid $border-gray-normal; &:hover { @@ -250,7 +251,7 @@ } } - a:not(.btn) { + a:not(.issuable-pager) { &:hover { color: $md-link-color; text-decoration: none; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index c2371d79989..7fa13e66b43 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -42,6 +42,7 @@ .note-textarea { display: block; padding: 10px 0; + color: $gl-gray; font-family: $regular_font; border: 0; @@ -83,18 +84,6 @@ border-color: $gl-success; } } - - p { - code { - white-space: normal; - } - - pre { - code { - white-space: pre; - } - } - } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ffcd89a3c9f..82f78e8d796 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -81,16 +81,8 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - p { - code { - white-space: normal; - } - - pre { - code { - white-space: pre; - } - } + code { + white-space: pre-wrap; } // Reset ul style types since we're nested inside a ul already @@ -137,7 +129,7 @@ ul.notes { margin-right: 10px; } .line_content { - white-space: pre; + white-space: pre-wrap; } } @@ -171,11 +163,6 @@ ul.notes { &.parallel { border-width: 1px; - - .code, - code { - white-space: pre-wrap; - } } .notes { @@ -308,7 +295,7 @@ ul.notes { padding: 4px; font-size: 16px; color: $gl-link-color; - margin-left: -60px; + margin-left: -56px; position: absolute; z-index: 10; width: 32px; diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 2520d4452a2..ec22548ddeb 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -93,6 +93,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :akismet_api_key, :email_author_in_body, :repository_checks_enabled, + :metrics_packet_size, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index f159e169f6d..b8b9e78427d 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,7 +1,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] - before_action :authorize_update_build!, except: [:index, :show, :status] + before_action :authorize_update_build!, except: [:index, :show, :status, :raw] layout 'project' def index @@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController notice: "Build has been sucessfully erased!" end + def raw + if @build.has_trace? + send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline' + else + render_404 + end + end + private def build diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 72078c3cc68..a202cb38692 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_read_commit_status!, only: [:builds] before_action :commit before_action :define_show_vars, only: [:show, :builds] - before_action :authorize_edit_tree!, only: [:revert] + before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] def show apply_diff_view_cookie! @@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController end def revert - assign_revert_commit_vars + assign_change_commit_vars(@commit.revert_branch_name) return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.", - success_path: successful_revert_path, failure_path: failed_revert_path) + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + success_path: successful_change_path, failure_path: failed_change_path) end + + def cherry_pick + assign_change_commit_vars(@commit.cherry_pick_branch_name) + + return render_404 if @target_branch.blank? - private - - def revert_type_title - @commit.merged_merge_request ? 'merge request' : 'commit' + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + success_path: successful_change_path, failure_path: failed_change_path) end - def successful_revert_path + private + + def successful_change_path return referenced_merge_request_url if @commit.merged_merge_request namespace_project_commits_url(@project.namespace, @project, @target_branch) end - def failed_revert_path + def failed_change_path return referenced_merge_request_url if @commit.merged_merge_request namespace_project_commit_url(@project.namespace, @project, params[:id]) @@ -116,14 +121,13 @@ class Projects::CommitController < Projects::ApplicationController @builds = Ci::Build.where(commit: ci_commits) end - def assign_revert_commit_vars + def assign_change_commit_vars(mr_source_branch) @commit = project.commit(params[:id]) @target_branch = params[:target_branch] - @mr_source_branch = @commit.revert_branch_name + @mr_source_branch = mr_source_branch @mr_target_branch = @target_branch @commit_params = { commit: @commit, - revert_type_title: revert_type_title, create_merge_request: params[:create_merge_request].present? || different_project? } end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 38214f04793..b96ab91c17d 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController end @issues = @issues.page(params[:page]) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.atom { render layout: false } format.json do render json: { - html: view_to_html_string("projects/issues/_issues") + html: view_to_html_string("projects/issues/_issues"), + labels: @labels.as_json(methods: :text_color) } end end @@ -128,10 +129,7 @@ class Projects::IssuesController < Projects::ApplicationController end def related_branches - merge_requests = @issue.referenced_merge_requests(current_user) - - @related_branches = @issue.related_branches - - merge_requests.map(&:source_branch) + @related_branches = @issue.related_branches(current_user) respond_to do |format| format.json do @@ -194,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue_params params.require(:issue).permit( :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :state_event, :task_num, label_ids: [] + :milestone_id, :due_date, :state_event, :task_num, label_ids: [] ) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4b8fc049047..81003c34f59 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(:target_project) - @label = @project.labels.find_by(title: params[:label_name]) + @labels = @project.labels.where(title: params[:label_name]) respond_to do |format| format.html format.json do render json: { - html: view_to_html_string("projects/merge_requests/_merge_requests") + html: view_to_html_string("projects/merge_requests/_merge_requests"), + labels: @labels.as_json(methods: :text_color) } end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 8b2577aebe1..739681f4085 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :build_events, + :note_events, :build_events, :wiki_page_events, :notify_only_broken_builds, :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 9f3a4a69721..c02bc28acef 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :create_wiki, @project) - if @page.update(content, format, message) + if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' @@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController end def create - @page = WikiPage.new(@project_wiki) + @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute - if @page.create(wiki_params) + if @page.persisted? redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), notice: 'Wiki was successfully updated.' @@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController params[:wiki].slice(:title, :content, :format, :message) end - def content - params[:wiki][:content] - end - - def format - params[:wiki][:format] - end - - def message - params[:wiki][:message] - end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f1df6832bf6..a85c214e4c4 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -39,6 +39,7 @@ class IssuableFinder items = by_assignee(items) items = by_author(items) items = by_label(items) + items = by_due_date(items) sort(items) end @@ -117,7 +118,7 @@ class IssuableFinder end def filter_by_no_label? - labels? && params[:label_name] == Label::None.title + labels? && params[:label_name].include?(Label::None.title) end def labels @@ -278,9 +279,47 @@ class IssuableFinder end end + # When filtering by multiple labels we may end up duplicating issues (if one + # has multiple labels). This ensures we only return unique issues. + items.distinct + end + + def by_due_date(items) + if due_date? + if filter_by_no_due_date? + items = items.without_due_date + elsif filter_by_overdue? + items = items.due_before(Date.today) + elsif filter_by_due_this_week? + items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) + elsif filter_by_due_this_month? + items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) + end + end + items end + def filter_by_no_due_date? + due_date? && params[:due_date] == Issue::NoDueDate.name + end + + def filter_by_overdue? + due_date? && params[:due_date] == Issue::Overdue.name + end + + def filter_by_due_this_week? + due_date? && params[:due_date] == Issue::DueThisWeek.name + end + + def filter_by_due_this_month? + due_date? && params[:due_date] == Issue::DueThisMonth.name + end + + def due_date? + params[:due_date].present? && klass.column_names.include?('due_date') + end + def label_names params[:label_name].split(',') end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 16e5b8ac223..3e0074da394 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -254,11 +254,11 @@ module ApplicationHelper def page_filter_path(options = {}) without = options.delete(:without) + add_label = options.delete(:label) exist_opts = { state: params[:state], scope: params[:scope], - label_name: params[:label_name], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], @@ -275,6 +275,13 @@ module ApplicationHelper path = request.path path << "?#{options.to_param}" + if add_label + if params[:label_name].present? and params[:label_name].respond_to?('any?') + params[:label_name].each do |label| + path << "&label_name[]=#{label}" + end + end + end path end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 9e59a295fc4..a4d7c425d0f 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -173,4 +173,15 @@ module BlobHelper response.etag = @blob.id !stale end + + def licenses_for_select + return @licenses_for_select if defined?(@licenses_for_select) + + licenses = Licensee::License.all + + @licenses_for_select = { + Popular: licenses.select(&:featured).map { |license| [license.name, license.key] }, + Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } + } + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 35ba543cef1..b59c3982edd 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -126,12 +126,10 @@ module CommitsHelper def revert_commit_link(commit, continue_to_path, btn_class: nil) return unless current_user - tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request" + tooltip = "Revert this #{commit.change_type_title} in a new merge request" if can_collaborate_with_project? - content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}" - end + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -146,11 +144,24 @@ module CommitsHelper end end - def revert_commit_type(commit) - if commit.merged_merge_request - 'merge request' - else - 'commit' + def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil) + return unless current_user + + tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" + + if can_collaborate_with_project? + link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip end end @@ -183,7 +194,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has-tooltip", - data: { 'original-title'.to_sym => sanitize(source_email) } + title: source_email } if user.nil? diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb new file mode 100644 index 00000000000..109bc1a02d1 --- /dev/null +++ b/app/helpers/import_helper.rb @@ -0,0 +1,18 @@ +module ImportHelper + def github_project_link(path_with_namespace) + link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' + end + + private + + def github_project_url(path_with_namespace) + "#{github_root_url}/#{path_with_namespace}" + end + + def github_root_url + return @github_url if defined?(@github_url) + + provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } + @github_url = provider.fetch('url', 'https://github.com') if provider + end +end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 49b6b79ce35..39474217286 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,25 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def multi_label_name(current_labels, default_label) + # current_labels may be a string from before + if current_labels.is_a?(Array) + if current_labels.count > 1 + "#{current_labels[0]} +#{current_labels.count - 1} more" + else + current_labels[0] + end + elsif current_labels.is_a?(String) + if current_labels.nil? || current_labels.empty? + default_label + else + current_labels + end + else + default_label + end + end + def issuable_json_path(issuable) project = issuable.project diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 4cb8adcebad..afe1e11a0da 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -131,7 +131,7 @@ module IssuesHelper class: "icon emoji-icon emoji-#{unicode}", title: name, data: data - else + else # Emoji icons displayed separately, used for the awards already given # to an issue or merge request. content_tag :img, "", @@ -172,6 +172,18 @@ module IssuesHelper end.to_h end + def due_date_options + options = [ + Issue::AnyDueDate, + Issue::NoDueDate, + Issue::DueThisWeek, + Issue::DueThisMonth, + Issue::Overdue + ] + + options_from_collection_for_select(options, 'name', 'title', params[:due_date]) + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7e00aacceaa..2f164da326c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -52,7 +52,7 @@ module ProjectsHelper link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe end end @@ -216,40 +216,14 @@ module ProjectsHelper end end - def add_contribution_guide_path(project) - if project && !project.repository.contribution_guide - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CONTRIBUTING.md", - commit_message: "Add contribution guide" - ) - end - end - - def add_changelog_path(project) - if project && !project.repository.changelog - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CHANGELOG", - commit_message: "Add changelog" - ) - end - end - - def add_license_path(project) - if project && !project.repository.license - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "LICENSE", - commit_message: "Add license" - ) - end + def add_special_file_path(project, file_name:, commit_message: nil) + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch || 'master', + file_name: file_name, + commit_message: commit_message || "Add #{file_name.downcase}" + ) end def contribution_guide_path(project) @@ -272,7 +246,7 @@ module ProjectsHelper end def license_path(project) - filename_path(project, :license) + filename_path(project, :license_blob) end def version_path(project) @@ -306,6 +280,13 @@ module ProjectsHelper namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') end + def new_license_path + ref = @repository.root_ref if @repository + ref ||= 'master' + + namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE') + end + def last_push_event if current_user current_user.recent_push(@project.id) @@ -335,6 +316,12 @@ module ProjectsHelper @ref || @repository.try(:root_ref) end + def license_short_name(project) + license = Licensee::License.new(project.repository.license_key) + + license.nickname || license.name + end + private def filename_path(project, filename) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 4fc6de59a8b..e951a87a212 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -2,32 +2,29 @@ module SelectsHelper def users_select_tag(id, opts = {}) css_class = "ajax-users-select " css_class << "multiselect " if opts[:multiple] + css_class << "skip_ldap " if opts[:skip_ldap] css_class << (opts[:class] || '') value = opts[:selected] || '' - placeholder = opts[:placeholder] || 'Search for a user' - null_user = opts[:null_user] || false - any_user = opts[:any_user] || false - email_user = opts[:email_user] || false - first_user = opts[:first_user] && current_user ? current_user.username : false - current_user = opts[:current_user] || false - author_id = opts[:author_id] || '' - project = opts[:project] || @project + first_user = opts[:first_user] && current_user ? current_user.username : false html = { class: css_class, data: { - placeholder: placeholder, - null_user: null_user, - any_user: any_user, - email_user: email_user, + placeholder: opts[:placeholder] || 'Search for a user', + null_user: opts[:null_user] || false, + any_user: opts[:any_user] || false, + email_user: opts[:email_user] || false, first_user: first_user, - current_user: current_user, - author_id: author_id + current_user: opts[:current_user] || false, + "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], + author_id: opts[:author_id] || '' } } unless opts[:scope] == :all + project = opts[:project] || @project + if project html['data-project-id'] = project.id elsif @group diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 2f2d2721d6d..630e10ea892 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -8,6 +8,8 @@ module SortingHelper sort_value_oldest_created => sort_title_oldest_created, sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_later => sort_title_milestone_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_due_date_later => sort_title_due_date_later, sort_value_largest_repo => sort_title_largest_repo, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, @@ -50,6 +52,14 @@ module SortingHelper 'Milestone due later' end + def sort_title_due_date_soon + 'Due soon' + end + + def sort_title_due_date_later + 'Due later' + end + def sort_title_name 'Name' end @@ -98,6 +108,14 @@ module SortingHelper 'milestone_due_desc' end + def sort_value_due_date_soon + 'due_date_asc' + end + + def sort_value_due_date_later + 'due_date_desc' + end + def sort_value_name 'name_asc' end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4920ca5af6e..dbedf417fa5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -66,7 +66,7 @@ module TreeHelper ref else project = tree_edit_project(project) - project.repository.next_patch_branch + project.repository.next_branch('patch') end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 55bb4f65270..9dd11d20ea6 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -56,7 +56,7 @@ module Emails { from: sender(sender_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid})") + subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})") } end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index f9650df9a74..cdc40b81ee1 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -38,7 +38,7 @@ module Emails { from: sender(@note.author_id), to: recipient(recipient_id), - subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})") + subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})") } end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 17273598bb8..553cd447971 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -224,12 +224,33 @@ module Ci end end + def trace_length + if raw_trace + raw_trace.length + else + 0 + end + end + def trace=(trace) + recreate_trace_dir + File.write(path_to_trace, trace) + end + + def recreate_trace_dir unless Dir.exists?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end + end + private :recreate_trace_dir - File.write(path_to_trace, trace) + def append_trace(trace_part, offset) + recreate_trace_dir + + File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) + File.open(path_to_trace, 'a') do |f| + f.write(trace_part) + end end def dir_to_trace diff --git a/app/models/commit.rb b/app/models/commit.rb index 7090909006d..562c3ed15b2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -15,8 +15,8 @@ class Commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] # Commits above this size will not be rendered in HTML - DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES) - DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) + DIFF_HARD_LIMIT_FILES = 1000 + DIFF_HARD_LIMIT_LINES = 50000 class << self def decorate(commits, project) @@ -219,6 +219,10 @@ class Commit def revert_branch_name "revert-#{short_id}" end + + def cherry_pick_branch_name + project.repository.next_branch("cherry-pick-#{short_id}", mild: true) + end def revert_description if merged_merge_request @@ -254,6 +258,10 @@ class Commit end.any? { |commit_ref| commit_ref.reverts_commit?(self) } end + def change_type_title + merged_merge_request ? 'merge request' : 'commit' + end + private def repo_changes diff --git a/app/models/group.rb b/app/models/group.rb index 9a04ac70d35..1f8432e3320 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -15,7 +15,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 7365e360de2..bc6e0f98c3c 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -25,4 +25,5 @@ class ProjectHook < WebHook scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } + scope :wiki_page_hooks, -> { where(wiki_page_events: true) } end diff --git a/app/models/issue.rb b/app/models/issue.rb index 3f188e04770..ea1bfb776ee 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -20,7 +20,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Issue < ActiveRecord::Base include InternalId @@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base include Sortable include Taskable + DueDateStruct = Struct.new(:title, :name).freeze + NoDueDate = DueDateStruct.new('No Due Date', '0').freeze + AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze + Overdue = DueDateStruct.new('Overdue', 'overdue').freeze + DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze + DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + ActsAsTaggableOn.strict_case_match = true belongs_to :project @@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base scope :open_for, ->(user) { opened.assigned_to(user) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) } + scope :without_due_date, -> { where(due_date: nil) } + scope :due_before, ->(date) { where('issues.due_date < ?', date) } + scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } + + scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } + scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end + def self.sort(method) + case method.to_s + when 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + else + super + end + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" @@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base end end - def related_branches - project.repository.branch_names.select do |branch| + # All branches containing the current issue's ID, except for + # those with a merge request open referencing the current issue. + def related_branches(current_user) + branches_with_iid = project.repository.branch_names.select do |branch| branch =~ /\A#{iid}-(?!\d+-stable)/i end + + branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch) + + branches_with_iid - branches_with_merge_request end # Reset issue events cache @@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base end def to_branch_name - "#{iid}-#{title.parameterize}" + if self.confidential? + "#{iid}-confidential-issue" + else + "#{iid}-#{title.parameterize}" + end end def can_be_worked_on?(current_user) !self.closed? && !self.project.forked? && - self.related_branches.empty? && + self.related_branches(current_user).empty? && self.closed_by_merge_requests(current_user).empty? end + + def overdue? + due_date.try(:past?) || false + end end diff --git a/app/models/label.rb b/app/models/label.rb index 55c01cae762..60bdce32952 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -113,6 +113,10 @@ class Label < ActiveRecord::Base template end + def text_color + LabelsHelper::text_color_for_bg(self.color) + end + private def label_format_reference(format = :id) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6766e4d1afa..d00919c3b0c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -27,9 +27,6 @@ # merge_commit_sha :string # -require Rails.root.join("app/models/commit") -require Rails.root.join("lib/static_model") - class MergeRequest < ActiveRecord::Base include InternalId include Issuable @@ -605,4 +602,8 @@ class MergeRequest < ActiveRecord::Base def can_be_reverted?(current_user = nil) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end + + def can_be_cherry_picked? + merge_commit + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 33884118595..0580cafdd1b 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -11,8 +11,6 @@ # updated_at :datetime # -require Rails.root.join("app/models/commit") - class MergeRequestDiff < ActiveRecord::Base include Sortable diff --git a/app/models/note.rb b/app/models/note.rb index 87ced65c650..71b4293d57a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -20,7 +20,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Note < ActiveRecord::Base include Gitlab::CurrentSettings diff --git a/app/models/project.rb b/app/models/project.rb index 9471b8a4dd9..7aa21b19e67 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -40,7 +40,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class Project < ActiveRecord::Base include Gitlab::ConfigHelper diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 79efb403058..2c0ae312f1b 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -8,7 +8,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class ProjectImportData < ActiveRecord::Base belongs_to :project diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 0e3fa4a40fe..064ef8e8674 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -183,7 +183,7 @@ class HipchatService < Service title = obj_attr[:title] merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" - merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>" + merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: <b>#{title}</b>" @@ -224,7 +224,7 @@ class HipchatService < Service when "MergeRequest" subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) subject_id = subj_attr[:iid] - subject_desc = "##{subject_id}" + subject_desc = "!#{subject_id}" subject_type = "merge request" title = format_title(subj_attr[:title]) when "Snippet" diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index d89cf6d17b2..fd65027f084 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -60,7 +60,7 @@ class SlackService < Service end def supported_events - %w(push issue merge_request note tag_push build) + %w(push issue merge_request note tag_push build wiki_page) end def execute(data) @@ -90,6 +90,8 @@ class SlackService < Service NoteMessage.new(data) when "build" BuildMessage.new(data) if should_build_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) end opt = {} @@ -133,3 +135,4 @@ require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" require "slack_service/build_message" +require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb index e792c258f73..11fc691022b 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -50,7 +50,7 @@ class SlackService end def merge_request_link - "[merge request ##{merge_request_id}](#{merge_request_url})" + "[merge request !#{merge_request_id}](#{merge_request_url})" end def merge_request_url diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index b15d9a14677..89ba51cb662 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -58,7 +58,7 @@ class SlackService def create_merge_note(merge_request) commented_on_message( - "[merge request ##{merge_request[:iid]}](#{@note_url})", + "[merge request !#{merge_request[:iid]}](#{@note_url})", format_title(merge_request[:title])) end diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb new file mode 100644 index 00000000000..f336d9e7691 --- /dev/null +++ b/app/models/project_services/slack_service/wiki_page_message.rb @@ -0,0 +1,53 @@ +class SlackService + class WikiPageMessage < BaseMessage + attr_reader :user_name + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :wiki_page_url + attr_reader :action + attr_reader :description + + def initialize(params) + @user_name = params[:user][:name] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @wiki_page_url = obj_attr[:url] + @description = obj_attr[:content] + + @action = + case obj_attr[:action] + when "create" + "created" + when "update" + "edited" + end + end + + def attachments + description_message + end + + private + + def message + "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 589756f8531..da751591103 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -228,7 +228,8 @@ class Repository def cache_keys %i(size branch_names tag_names commit_count - readme version contribution_guide changelog license) + readme version contribution_guide changelog + license_blob license_key) end def build_cache @@ -461,27 +462,21 @@ class Repository end end - def license - cache.fetch(:license) do - licenses = tree(:head).blobs.find_all do |file| - file.name =~ /\A(copying|license|licence)/i - end - - preferences = [ - /\Alicen[sc]e\z/i, # LICENSE, LICENCE - /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt - /\Acopying\z/i, # COPYING - /\Acopying\.(?!lesser)/i, # COPYING.txt - /Acopying.lesser/i # COPYING.LESSER - ] + def license_blob + return nil if !exists? || empty? - license = nil - preferences.each do |r| - license = licenses.find { |l| l.name =~ r } - break if license + cache.fetch(:license_blob) do + if licensee_project.license + blob_at_branch(root_ref, licensee_project.matched_file.filename) end + end + end - license + def license_key + return nil if !exists? || empty? + + cache.fetch(:license_key) do + licensee_project.license.try(:key) || 'no-license' end end @@ -549,15 +544,18 @@ class Repository commit(sha) end - def next_patch_branch - patch_branch_ids = self.branch_names.map do |n| - result = n.match(/\Apatch-([0-9]+)\z/) + def next_branch(name, opts={}) + branch_ids = self.branch_names.map do |n| + next 1 if n == name + result = n.match(/\A#{name}-([0-9]+)\z/) result[1].to_i if result end.compact - highest_patch_branch_id = patch_branch_ids.max || 0 + highest_branch_id = branch_ids.max || 0 - "patch-#{highest_patch_branch_id + 1}" + return name if opts[:mild] && 0 == highest_branch_id + + "#{name}-#{highest_branch_id + 1}" end # Remove archives older than 2 hours @@ -760,6 +758,28 @@ class Repository end end + def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) + source_sha = find_branch(base_branch).target + cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) + + return false unless cherry_pick_tree_id + + commit_with_hooks(user, base_branch) do |ref| + committer = user_to_committer(user) + source_sha = Rugged::Commit.create(rugged, + message: commit.message, + author: { + email: commit.author_email, + name: commit.author_name, + time: commit.authored_date + }, + committer: committer, + tree: cherry_pick_tree_id, + parents: [rugged.lookup(source_sha)], + update_ref: ref) + end + end + def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] @@ -774,6 +794,20 @@ class Repository tree_id end + def check_cherry_pick_content(commit, base_branch) + source_sha = find_branch(base_branch).target + args = [commit.id, source_sha] + args << 1 if commit.merge_commit? + + cherry_pick_index = rugged.cherrypick_commit(*args) + return false if cherry_pick_index.conflicts? + + tree_id = cherry_pick_index.write_tree(rugged) + return false unless diff_exists?(source_sha, tree_id) + + tree_id + end + def diff_exists?(sha1, sha2) rugged.diff(sha1, sha2).size > 0 end @@ -925,4 +959,8 @@ class Repository def cache @cache ||= RepositoryCache.new(path_with_namespace) end + + def licensee_project + @licensee_project ||= Licensee.project(path) + end end diff --git a/app/models/service.rb b/app/models/service.rb index 721273250ea..2645b8321d7 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -32,6 +32,7 @@ class Service < ActiveRecord::Base default_value_for :tag_push_events, true default_value_for :note_events, true default_value_for :build_events, true + default_value_for :wiki_page_events, true after_initialize :initialize_properties @@ -53,6 +54,7 @@ class Service < ActiveRecord::Base scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } + scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } default_value_for :category, 'common' @@ -94,7 +96,7 @@ class Service < ActiveRecord::Base end def supported_events - %w(push tag_push issue merge_request) + %w(push tag_push issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user.rb b/app/models/user.rb index 031315debd7..ab48f8f1960 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,7 +63,6 @@ # require 'carrierwave/orm/activerecord' -require 'file_size_validator' class User < ActiveRecord::Base extend Gitlab::ConfigHelper diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 526760779a4..3d5fd9d3ee9 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -29,6 +29,10 @@ class WikiPage # new Page values before writing to the Gollum repository. attr_accessor :attributes + def hook_attrs + attributes + end + def initialize(wiki, page = nil, persisted = false) @wiki = wiki @page = page diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb new file mode 100644 index 00000000000..6b69cb53b2c --- /dev/null +++ b/app/services/commits/change_service.rb @@ -0,0 +1,47 @@ +module Commits + class ChangeService < ::BaseService + class ValidationError < StandardError; end + class ChangeError < StandardError; end + + def execute + @source_project = params[:source_project] || @project + @target_branch = params[:target_branch] + @commit = params[:commit] + @create_merge_request = params[:create_merge_request].present? + + check_push_permissions unless @create_merge_request + commit + rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, + ValidationError, ChangeError => ex + error(ex.message) + end + + def commit + raise NotImplementedError + end + + private + + def check_push_permissions + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) + + unless allowed + raise ValidationError.new('You are not allowed to push into this branch') + end + + true + end + + def create_target_branch(new_branch) + # Temporary branch exists and contains the change commit + return success if repository.find_branch(new_branch) + + result = CreateBranchService.new(@project, current_user) + .execute(new_branch, @target_branch, source_project: @source_project) + + if result[:status] == :error + raise ChangeError, "There was an error creating the source branch: #{result[:message]}" + end + end + end +end diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb new file mode 100644 index 00000000000..f9a4efa7182 --- /dev/null +++ b/app/services/commits/cherry_pick_service.rb @@ -0,0 +1,19 @@ +module Commits + class CherryPickService < ChangeService + def commit + cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch + cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch) + + if cherry_pick_tree_id + create_target_branch(cherry_pick_into) if @create_merge_request + + repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id) + success + else + error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically. + It may have already been cherry-picked, or a more recent commit may have updated some of its content." + raise ChangeError, error_msg + end + end + end +end diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb index a3c950ede1f..c7de9f6f35e 100644 --- a/app/services/commits/revert_service.rb +++ b/app/services/commits/revert_service.rb @@ -1,21 +1,5 @@ module Commits - class RevertService < ::BaseService - class ValidationError < StandardError; end - class ReversionError < StandardError; end - - def execute - @source_project = params[:source_project] || @project - @target_branch = params[:target_branch] - @commit = params[:commit] - @create_merge_request = params[:create_merge_request].present? - - check_push_permissions unless @create_merge_request - commit - rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, - ValidationError, ReversionError => ex - error(ex.message) - end - + class RevertService < ChangeService def commit revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch revert_tree_id = repository.check_revert_content(@commit, @target_branch) @@ -26,34 +10,10 @@ module Commits repository.revert(current_user, @commit, revert_into, revert_tree_id) success else - error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically. + error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically. It may have already been reverted, or a more recent commit may have updated some of its content." - raise ReversionError, error_msg + raise ChangeError, error_msg end end - - private - - def create_target_branch(new_branch) - # Temporary branch exists and contains the revert commit - return success if repository.find_branch(new_branch) - - result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) - - if result[:status] == :error - raise ReversionError, "There was an error creating the source branch: #{result[:message]}" - end - end - - def check_push_permissions - allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) - - unless allowed - raise ValidationError.new('You are not allowed to push into this branch') - end - - true - end end end diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb new file mode 100644 index 00000000000..9162f128602 --- /dev/null +++ b/app/services/wiki_pages/base_service.rb @@ -0,0 +1,27 @@ +module WikiPages + class BaseService < ::BaseService + + def hook_data(page, action) + hook_data = { + object_kind: page.class.name.underscore, + user: current_user.hook_attrs, + project: @project.hook_attrs, + object_attributes: page.hook_attrs, + # DEPRECATED + repository: @project.hook_attrs.slice(:name, :url, :description, :homepage) + } + + page_url = Gitlab::UrlBuilder.build(page) + hook_data[:object_attributes].merge!(url: page_url, action: action) + hook_data + end + + private + + def execute_hooks(page, action = 'create') + page_data = hook_data(page, action) + @project.execute_hooks(page_data, :wiki_page_hooks) + @project.execute_services(page_data, :wiki_page_hooks) + end + end +end diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb new file mode 100644 index 00000000000..988c663b9d0 --- /dev/null +++ b/app/services/wiki_pages/create_service.rb @@ -0,0 +1,13 @@ +module WikiPages + class CreateService < WikiPages::BaseService + def execute + page = WikiPage.new(@project.wiki) + + if page.create(@params) + execute_hooks(page, 'create') + end + + page + end + end +end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb new file mode 100644 index 00000000000..8f6a50da838 --- /dev/null +++ b/app/services/wiki_pages/update_service.rb @@ -0,0 +1,11 @@ +module WikiPages + class UpdateService < WikiPages::BaseService + def execute(page) + if page.update(@params[:content], @params[:format], @params[:message]) + execute_hooks(page, 'update') + end + + page + end + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e320b3d7490..e0d8d16a954 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -218,6 +218,13 @@ .help-block The sampling interval in seconds. Sampled data includes memory usage, retained Ruby objects, file descriptors and so on. + .form-group + = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :metrics_packet_size, class: 'form-control' + .help-block + The amount of points to store in a single UDP packet. More points + results in fewer but larger UDP packets being sent. %fieldset %legend Spam and Anti-bot Protection @@ -286,7 +293,7 @@ = f.check_box :repository_checks_enabled Enable Repository Checks .help-block - GitLab will periodically run + GitLab will periodically run %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. .form-group @@ -294,7 +301,7 @@ = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" .help-block If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. - + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index b05fdbd5552..fe0b9d3a491 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -7,17 +7,17 @@ .form-group = f.label :name, class: 'control-label' .col-sm-10 - = f.text_field :name, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group = f.label :username, class: 'control-label' .col-sm-10 - = f.text_field :username, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group = f.label :email, class: 'control-label' .col-sm-10 - = f.text_field :email, required: true, autocomplete: "off", class: 'form-control' + = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required - if @user.new_record? diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f9ec3a89158..49ab8aad1d5 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -45,6 +45,7 @@ .prepend-top-default - if @todos.any? + .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - @todos.group_by(&:project).each do |group| .panel.panel-default.panel-small.js-todos-list - project = group[0] diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 9639da4cb58..5b7f11440c1 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -26,7 +26,7 @@ - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} %td - = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" + = github_project_link(project.import_source) %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -43,7 +43,7 @@ - @repos.each do |repo| %tr{id: "repo_#{repo.id}"} %td - = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" + = github_project_link(repo.full_name) %td.import-target = repo.full_name %td.import-actions.job-status diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 574e8bfef24..81c7c88fc96 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" + = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index 59db86b08bc..b435067d5a6 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" += "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index c9bf04f514e..41a320d6bd8 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" + = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index b96dd0fd8ab..7a5074a1dc3 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" += "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index 6762fae7f64..fbe506d4f4d 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request ##{@merge_request.iid} was merged" + = "Merge Request #{@merge_request.to_reference} was merged" diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 34dbc60e19b..bfbae01094f 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request ##{@merge_request.iid} was merged" += "Merge Request #{@merge_request.to_reference} was merged" Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index bdcca6e4ab7..d4aad8d1862 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -1,4 +1,4 @@ -New Merge Request #<%= @merge_request.iid %> +New Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 1d1411992a6..8cdab63829e 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,4 +1,4 @@ -New comment for Merge Request <%= @merge_request.iid %> +New comment for Merge Request <%= @merge_request.to_reference %> <%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 961b61d2e76..48b0dd6b121 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -9,4 +9,7 @@ = spinner :javascript - new Activities(); + var activity = new Activities(); + $(document).on('page:restore', function (event) { + activity.reloadActivities() + }) diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index d1191928d4f..a9908eaecca 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -9,7 +9,7 @@ - else .gray-content-block.second-block.center %h3.page-title - This project does not have README yet + This project does not have a README yet - if can?(current_user, :push_code, @project) %p A @@ -18,5 +18,5 @@ distributed with computer software, forming part of its documentation. %p We recommend you to - = link_to "add README", new_readme_path, class: 'underlined-link' + = link_to "add a README", new_readme_path, class: 'underlined-link' file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f8b6fa253c4..fefa652a3da 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -13,7 +13,11 @@ required: true, class: 'form-control new-file-name' .pull-right - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' + .license-selector.js-license-selector.hide + = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} + + .encoding-selector + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-content.code %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 1dd2b5c0af7..0459699432e 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -14,5 +14,5 @@ cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) :javascript - blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) + blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}") new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index b50b2cf3764..99d72aa7935 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -10,7 +10,7 @@ - merge_request = @build.merge_request - if merge_request via - = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request) + = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) #up-build-trace - builds = @build.commit.builds.latest.to_a @@ -110,7 +110,7 @@ = icon('folder-open') Browse - .build-widget + .build-widget.build-controls %h4.title Build ##{@build.id} - if can?(current_user, :update_build, @project) @@ -127,6 +127,9 @@ data: { confirm: 'Are you sure you want to erase this build?' } do = icon('eraser') Erase + - if @build.has_trace? + = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), + class: 'btn btn-sm btn-success', target: '_blank' .clearfix - if @build.duration diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_change.html.haml index 52ca3ed5b14..44ef1fdbbe3 100644 --- a/app/views/projects/commit/_revert.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -1,13 +1,21 @@ -#modal-revert-commit.modal +- case type.to_s +- when 'revert' + - label = 'Revert' + - target_label = 'Revert in branch' +- when 'cherry-pick' + - label = 'Cherry-pick' + - target_label = 'Pick into branch' + +.modal{id: "modal-#{type}-commit"} .modal-dialog .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== Revert this #{revert_commit_type(commit)} + %h3.page-title== #{label} this #{commit.change_type_title} .modal-body - = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do + = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do .form-group.branch - = label_tag 'target_branch', 'Revert in branch', class: 'control-label' + = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch" - if can?(current_user, :push_code, @project) @@ -20,7 +28,7 @@ - else = hidden_field_tag 'create_merge_request', 1 .form-actions - = submit_tag "Revert", class: 'btn btn-create' + = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) @@ -28,4 +36,4 @@ = commit_in_fork_help :javascript - new NewCommitForm($('.js-create-dir-form')) + new NewCommitForm($('.js-#{type}-form')) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0908e830f83..3d7c18a5f58 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -18,6 +18,7 @@ Browse Files - unless @commit.has_been_reverted?(current_user) = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) + = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) %div %p diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 096f7058bd4..e5e3d696035 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -13,4 +13,5 @@ diff_refs: @diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - = render "projects/commit/revert", commit: @commit, title: @commit.title + - %w(revert cherry-pick).each do |type| + = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index e2eb67bef30..c7d8c9a0d15 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -30,5 +30,5 @@ by = commit_author_link(commit, avatar: true, size: 24) .committed_ago - #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + #{time_ago_with_tooltip(commit.committed_date)} = link_to_browse_code(project, commit) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 6ad7b05155a..52d093871b4 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -14,8 +14,10 @@ %p If you already have files you can push them using command line instructions below. %p - Otherwise you can start with - = link_to "adding README", new_readme_path, class: 'underlined-link' + Otherwise you can start with adding a + = link_to "README", new_readme_path, class: 'underlined-link' + or a + = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' file to this project. - if can?(current_user, :push_code, @project) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 7a8009f6da4..9ad86ed71c9 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -48,6 +48,11 @@ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = icon('clock-o') = issue.milestone.title + - if issue.due_date + %span{class: "#{'cred' if issue.overdue?}"} + + = icon('calendar') + = issue.due_date.to_s(:medium) - if issue.labels.any? - issue.labels.each do |label| diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 7125d7d9d1c..8d05060f563 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -86,8 +86,10 @@ = spinner = render 'shared/issuable/sidebar', issuable: @merge_request -- if @merge_request.can_be_reverted? - = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_reverted?(current_user) + = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title +- if @merge_request.can_be_cherry_picked? + = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript var merge_request; diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index fc62bb5bce9..b31ea5e5321 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,7 +1,7 @@ -- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" = render "header_title" %h3.page-title - Edit Merge Request ##{@merge_request.iid} + Edit Merge Request #{@merge_request.to_reference} %hr = render 'form' diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index fc03ee73a3d..8ac653427c9 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests" +- page_title "#{@merge_request.title} (#{merge_request.to_reference}", "Merge Requests" = render "header_title" .merge-request diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 0a99e8c9591..36c275e8be1 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -24,9 +24,9 @@ %li{ class: issue_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' %li - = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request' + = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do = icon('pencil-square-o') Edit diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 3abae9f0bf6..ec4beae9727 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -44,3 +44,8 @@ $('.remove_source_branch_in_progress').hide(); $('.remove_source_branch_widget.failed').show(); }); + - else + %p + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + = render 'projects/merge_requests/widget/merged_buttons' diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 85a3a6ba9e2..56167509af9 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -1,11 +1,14 @@ -- source_branch_exists = local_assigns.fetch(:source_branch_exists, false) -- mr_can_be_reverted = @merge_request.can_be_reverted? +- can_remove_source_branch = local_assigns.fetch(:source_branch_exists, false) && @merge_request.can_remove_source_branch?(current_user) +- mr_can_be_reverted = @merge_request.can_be_reverted?(current_user) +- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked? -- if source_branch_exists || mr_can_be_reverted +- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .btn-group - - if source_branch_exists + - if can_remove_source_branch = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + - if mr_can_be_cherry_picked + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 6e9ecdf7649..aeb7c1d5ee4 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -8,7 +8,9 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference} commented" + = "#{note.author.to_reference}" + - if !note.system + commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') .note-actions diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index b31fcfea763..9fa4127c948 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -4,7 +4,7 @@ - if shared_runners_text.present? = markdown(shared_runners_text, pipeline: 'plain_markdown') - else - GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. + Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com). %hr - if @project.shared_runners_enabled? = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4310f038fc9..d854ac21725 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -36,9 +36,9 @@ %li = link_to 'Changelog', changelog_path(@project) - - if @repository.license + - if @repository.license_blob %li - = link_to 'License', license_path(@project) + = link_to license_short_name(@project), license_path(@project) - if @repository.contribution_guide %li @@ -47,15 +47,15 @@ - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing - = link_to add_changelog_path(@project) do + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do Add Changelog - - unless @repository.license + - unless @repository.license_blob %li.missing - = link_to add_license_path(@project) do + = link_to add_special_file_path(@project, file_name: 'LICENSE') do Add License - unless @repository.contribution_guide %li.missing - = link_to add_contribution_guide_path(@project) do + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do Add Contribution guide - if @repository.commit diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index faeb2b55c6f..333f6533213 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -2,7 +2,7 @@ %h4 = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - .pull-right ##{merge_request.iid} + .pull-right #{merge_request.to_reference} - if merge_request.description.present? .description.term = preserve do diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index b38c5e18efb..9ce5562e667 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -2,4 +2,4 @@ %span.label-name = link_to_label(label, tooltip: false) %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line) + = markdown(label.description, pipeline: :single_line)
\ No newline at end of file diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml new file mode 100644 index 00000000000..dc89e36419c --- /dev/null +++ b/app/views/shared/_labels_row.html.haml @@ -0,0 +1,3 @@ +- labels.each do |label| + %span.label-row + = link_to_label(label, tooltip: false) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index fc935166bf6..4eaf7c2a025 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -62,6 +62,14 @@ %strong Build events %p.light This url will be triggered when a build status changes + - if @service.supported_events.include?("wiki_page") + %div + = form.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = form.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This url will be triggered when a wiki page is created/updated - @service.fields.each do |field| diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index e3a6a5a68b6..d327bd0a96f 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -20,6 +20,11 @@ = sort_title_milestone_soon = link_to page_filter_path(sort: sort_value_milestone_later) do = sort_title_milestone_later + - if controller.controller_name == 'issues' || controller.action_name == 'issues' + = link_to page_filter_path(sort: sort_value_due_date_soon) do + = sort_title_due_date_soon + = link_to page_filter_path(sort: sort_value_due_date_later) do + = sort_title_due_date_later = link_to page_filter_path(sort: sort_value_upvotes) do = sort_title_upvotes = link_to page_filter_path(sort: sort_value_downvotes) do diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 921eaefd79a..fc1101646fb 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -23,6 +23,7 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" + .pull-right = render 'shared/sort_dropdown' @@ -46,9 +47,10 @@ .filter-item.inline = button_tag "Update issues", class: "btn update_selected_issues btn-save" -- if @label - .gray-content-block.second-block - = render "shared/label_row", label: @label + - if !@labels.nil? + .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } + - if @labels.any? + = render "shared/labels_row", labels: @labels :javascript new UsersSelect(); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index bae15b7f844..18b091df39b 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -75,8 +75,9 @@ = f.label :label_ids, "Labels", class: 'control-label' .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } - if has_labels - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } + .issuable-form-select-holder + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - else %span.light No labels yet. @@ -88,9 +89,10 @@ .form-group = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 - - projects = project_options(issuable, current_user, ability: :admin_issue) - = select_tag(:move_to_project_id, projects, include_blank: true, - class: 'select2', data: { placeholder: 'Select project' }) + .issuable-form-select-holder + - projects = project_options(issuable, current_user, ability: :admin_issue) + = select_tag(:move_to_project_id, projects, include_blank: true, + class: 'select2', data: { placeholder: 'Select project' }) %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } @@ -102,13 +104,15 @@ .form-group = f.label :source_branch, class: 'control-label' .col-sm-10 - = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) + .issuable-form-select-holder + = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) .form-group = f.label :target_branch, class: 'control-label' .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) + .issuable-form-select-holder + = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} }) - if @merge_request.new_record? - %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index f722e61eeac..3eaa45258f0 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,9 +1,11 @@ - if params[:label_name].present? - = hidden_field_tag(:label_name, params[:label_name]) + - if params[:label_name].respond_to?('any?') + - params[:label_name].each do |label| + = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %span.dropdown-toggle-text - = h(params[:label_name].presence || "Label") + = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-page-one diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index a6970b7eebb..1d9b09a5ef1 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -4,22 +4,22 @@ - else - page_context_word = 'issues' %li{class: ("active" if params[:state] == 'opened')} - = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do + = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do #{state_filters_text_for(:opened, @project)} - if defined?(type) && type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} - = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do + = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do #{state_filters_text_for(:merged, @project)} %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do #{state_filters_text_for(:closed, @project)} - else %li{class: ("active" if params[:state] == 'closed')} - = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do + = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do #{state_filters_text_for(:closed, @project)} %li{class: ("active" if params[:state] == 'all')} - = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do + = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do #{state_filters_text_for(:all, @project)} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 55d7a38cce2..a50b4b96693 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -10,14 +10,14 @@ = sidebar_gutter_toggle_icon .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - if prev_issuable = prev_issuable_for(issuable) - = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn' + = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Prev - if next_issuable = next_issuable_for(issuable) - = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn' + = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager' - else - %a.btn.btn-default.disabled{href: '#'} + %a.btn.btn-default.issuable-pager.disabled{href: '#'} Next = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| @@ -58,7 +58,7 @@ - if issuable.milestone = issuable.milestone.title - else - No + None .title.hide-collapsed Milestone = icon('spinner spin', class: 'block-loading') @@ -75,6 +75,34 @@ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) + - if issuable.has_attribute?(:due_date) + .block.due_date + .sidebar-collapsed-icon + = icon('calendar') + %span.js-due-date-sidebar-value + = issuable.due_date.try(:to_s, :medium) || 'None' + .title.hide-collapsed + Due date + = icon('spinner spin', class: 'block-loading') + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.bold.hide-collapsed + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + .light None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .selectbox.hide-collapsed + = f.hidden_field :due_date, value: issuable.due_date + .dropdown + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %span.dropdown-toggle-text Due date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title('Due date') + = dropdown_content do + .js-due-date-calendar + - if issuable.project.labels.any? .block.labels .sidebar-collapsed-icon @@ -151,6 +179,7 @@ :javascript new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); - new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); + new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') new Sidebar(); + new DueDateSelect(); diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index e1127b2311c..47b66d44e43 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -23,5 +23,5 @@ - if assignee = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), - class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do + class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') |
