diff options
author | James Lopez <james@jameslopez.es> | 2016-04-04 20:03:25 +0200 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2016-04-04 20:03:25 +0200 |
commit | 7f7769172e81dc8bfdb037965ec7bf51c95578ec (patch) | |
tree | 0a0f8a6f704ddb79edf8cc600a72b4ccf760a4c2 /app | |
parent | 4835e680a4624ab8de3316b367b8375bb5a270a0 (diff) | |
parent | 531e4bdac8c409a25aa862c644dcab00960c82c4 (diff) | |
download | gitlab-ce-7f7769172e81dc8bfdb037965ec7bf51c95578ec.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into fix/project-import_url
# Conflicts:
# db/schema.rb
Diffstat (limited to 'app')
119 files changed, 1830 insertions, 784 deletions
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f5e1ca9860d..70fd6f50e9c 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -146,15 +146,11 @@ class Dispatcher when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() - # If we haven't installed a custom shortcut handler, install the default one if not shortcut_handler new Shortcuts() initSearch: -> - opts = $('.search-autocomplete-opts') - path = opts.data('autocomplete-path') - project_id = opts.data('autocomplete-project-id') - project_ref = opts.data('autocomplete-project-ref') - new SearchAutocomplete(path, project_id, project_ref) + # Only when search form is present + new SearchAutocomplete() if $('.search').length diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4b78bcde774..4f032a82e58 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -3,6 +3,10 @@ class GitLabDropdownFilter HAS_VALUE_CLASS = "has-value" constructor: (@input, @options) -> + { + @filterInputBlur = true + } = @options + $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') @@ -33,7 +37,7 @@ class GitLabDropdownFilter blur_field = @shouldBlur e.keyCode search_text = @input.val() - if blur_field + if blur_field and @filterInputBlur @input.blur() if @options.remote @@ -93,27 +97,48 @@ class GitLabDropdown PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + FILTER_INPUT = '.dropdown-input .dropdown-input-field' + constructor: (@el, @options) -> - self = @ @dropdown = $(@el).parent() + + # Set Defaults + { + # If no input is passed create a default one + @filterInput = @getElement(FILTER_INPUT) + @highlight = false + @filterInputBlur = true + @enterCallback = true + } = @options + + self = @ + + # If selector was passed + if _.isString(@filterInput) + @filterInput = @getElement(@filterInput) + search_fields = if @options.search then @options.search.fields else []; if @options.data - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data + # If data is an array + if _.isArray @options.data + @fullData = @options.data + @parseData @options.data + else + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data - @parseData @fullData - } + @parseData @fullData + } - # Init filiterable + # Init filterable if @options.filterable - @input = @dropdown.find('.dropdown-input .dropdown-input-field') - - @filter = new GitLabDropdownFilter @input, + @filter = new GitLabDropdownFilter @filterInput, + filterInputBlur: @filterInputBlur remote: @options.filterRemote query: @options.data keys: @options.search.fields @@ -123,11 +148,14 @@ class GitLabDropdown @parseData data @highlightRow 1 enterCallback: => - @selectFirstRow() + if @enterCallback + @selectFirstRow() # Event listeners + @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden + @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate if @dropdown.find(".dropdown-toggle-page").length @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @@ -143,10 +171,14 @@ class GitLabDropdown selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> - self.rowClicked $(@) + selected = self.rowClicked $(@) if self.options.clicked - self.options.clicked() + self.options.clicked(selected) + + # Finds an element inside wrapper element + getElement: (selector) -> + @dropdown.find selector toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS @@ -176,15 +208,26 @@ class GitLabDropdown @appendMenu(full_html) + shouldPropagate: (e) => + if @options.multiSelect + $target = $(e.target) + if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') + e.stopPropagation() + return false + else + return true + opened: => contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" @remote.execute() if @options.filterable - @dropdown.find(".dropdown-input-field").focus() + @filterInput.focus() - hidden: => + @dropdown.trigger('shown.gl.dropdown') + + hidden: (e) => if @options.filterable @dropdown .find(".dropdown-input-field") @@ -195,6 +238,11 @@ class GitLabDropdown if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS + if @options.hidden + @options.hidden.call(@,e) + + @dropdown.trigger('hidden.gl.dropdown') + # Render the full menu renderMenu: (html) -> @@ -219,20 +267,46 @@ class GitLabDropdown renderItem: (data) -> html = "" + # Divider return "<li class='divider'></li>" if data is "divider" + # Separator is a full-width divider + return "<li class='separator'></li>" if data is "separator" + + # Header + return "<li class='dropdown-header'>#{data.header}</li>" if data.header? + if @options.renderRow # Call the render function html = @options.renderRow(data) else - selected = if @options.isSelected then @options.isSelected(data) else false - url = if @options.url then @options.url(data) else "#" - text = if @options.text then @options.text(data) else "" + if not selected + value = if @options.id then @options.id(data) else data.id + fieldName = @options.fieldName + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") + if field.length + selected = true + + # Set URL + if @options.url? + url = @options.url(data) + else + url = if data.url? then data.url else '#' + + # Set Text + if @options.text? + text = @options.text(data) + else + text = if data.text? then data.text else '' + cssClass = ""; if selected cssClass = "is-active" + if @highlight + text = @highlightTextMatches(text, @filterInput.val()) + html = "<li>" html += "<a href='#{url}' class='#{cssClass}'>" html += text @@ -241,58 +315,68 @@ class GitLabDropdown return html + highlightTextMatches: (text, term) -> + occurrences = fuzzaldrinPlus.match(text, term) + text.split('').map((character, i) -> + if i in occurrences then "<b>#{character}</b>" else character + ).join('') + noResults: -> html = "<li>" - html += "<a href='#' class='dropdown-menu-empty-link is-focused'>" + html += "<a class='dropdown-menu-empty-link is-focused'>" html += "No matching results." html += "</a>" html += "</li>" highlightRow: (index) -> - if @input.val() isnt "" + if @filterInput.val() isnt "" selector = '.dropdown-content li:first-child a' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one .dropdown-content li:first-child a" - $(selector).addClass 'is-focused' + @getElement(selector).addClass 'is-focused' rowClicked: (el) -> fieldName = @options.fieldName - field = @dropdown.parent().find("input[name='#{fieldName}']") + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) + el.removeClass(ACTIVE_CLASS) field.remove() - else - fieldName = @options.fieldName - selectedIndex = el.parent().index() - if @renderedData - selectedObject = @renderedData[selectedIndex] - value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + # Toggle the dropdown label + if @options.toggleLabel + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + else if !value? field.remove() - if @options.multiSelect - oldValue = field.val() - if oldValue - value = "#{oldValue},#{value}" - else + if not @options.multiSelect @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS + @dropdown.parent().find("input[name='#{fieldName}']").remove() # Toggle active class for the tick mark - el.toggleClass "is-active" + el.addClass ACTIVE_CLASS # Toggle the dropdown label if @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) - if value? if !field.length # Create hidden input for form - input = "<input type='hidden' name='#{fieldName}' />" + input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" + if @options.inputId? + input = $(input) + .attr('id', @options.inputId) @dropdown.before input + else + field.val value - @dropdown.parent().find("input[name='#{fieldName}']").val value + return selectedObject selectFirstRow: -> selector = '.dropdown-content li:first-child a' @@ -304,4 +388,6 @@ class GitLabDropdown $.fn.glDropdown = (opts) -> return @.each -> - new GitLabDropdown @, opts + if (!$.data @, 'glDropdown') + $.data(@, 'glDropdown', new GitLabDropdown @, opts) + diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index d6d09b36d8d..2f19513a831 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -1,8 +1,7 @@ class @IssuableContext - constructor: -> + constructor: (currentUser) -> @initParticipants() - - new UsersSelect() + new UsersSelect(currentUser) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $(".issuable-sidebar .inline-update").on "change", "select", -> @@ -10,11 +9,21 @@ class @IssuableContext $(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(this).submit() - $(document).on "click",".edit-link", (e) -> - block = $(@).parents('.block') - block.find('.selectbox').show() - block.find('.value').hide() - block.find('.js-select2').select2("open") + $(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 + $(".right-sidebar").niceScroll() diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 1127b289264..b1479bfb449 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -1,7 +1,6 @@ @Issues = init: -> Issues.initSearch() - Issues.initSelects() Issues.initChecks() $("body").on "ajax:success", ".close_issue, .reopen_issue", -> @@ -17,18 +16,9 @@ $(this).html totalIssues - 1 reload: -> - Issues.initSelects() Issues.initChecks() $('#filter_issue_search').val($('#issue_search').val()) - initSelects: -> - $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true) - $("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true) - $("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true) - $("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true) - $("#milestone_id, #assignee_id, #label_name").on "change", -> - $(this).closest("form").submit() - initChecks: -> $(".check_all_issues").click -> $(".selected_issue").prop("checked", @checked) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index e08648d583b..d1fe116397a 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -4,14 +4,21 @@ class @LabelsSelect $dropdown = $(dropdown) projectId = $dropdown.data('project-id') labelUrl = $dropdown.data('labels') + issueUpdateURL = $dropdown.data('issueUpdate') selectedLabel = $dropdown.data('selected') - if selectedLabel - selectedLabel = selectedLabel.toString().split(',') + if selectedLabel? + selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') defaultLabel = $dropdown.data('default-label') + abilityName = $dropdown.data('ability-name') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() if newLabelField.length $newLabelCreateButton = $('.js-new-label-btn') @@ -21,6 +28,22 @@ class @LabelsSelect # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on 'click', (e) -> + + issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? + if issueUpdateURL + labelHTMLTemplate = _.template( + '<% _.each(labels, function(label){ %> + <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> + <span class="label color-label" style="background-color: <%= label.color %>;"> + <%= label.title %> + </span> + </a> + <% }); %>' + ); + labelNoneHTMLTemplate = _.template('<div class="light">None</div>') + + if newLabelField.length and $dropdown.hasClass 'js-extra-options' + $('.suggest-colors-dropdown a').on "click", (e) -> e.preventDefault() e.stopPropagation() newColorField @@ -57,6 +80,23 @@ class @LabelsSelect # This allows us to enable the button when ready enableLabelCreateButton = -> if newLabelField.val() isnt '' and newColorField.val() isnt '' + $newLabelError.hide() + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + $newLabelCreateButton.enable() else $newLabelCreateButton.disable() @@ -90,41 +130,84 @@ class @LabelsSelect else $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + saveLabelData = -> + selected = $dropdown + .closest('.selectbox') + .find("input[name='#{$dropdown.data('field-name')}']") + .map(-> + @value + ).get() + data = {} + data[abilityName] = {} + data[abilityName].label_ids = selected + if not selected.length + data[abilityName].label_ids = [''] + $loading.fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $.ajax( + type: 'PUT' + url: issueUpdateURL + dataType: 'JSON' + data: data + ).done (data) -> + $loading.fadeOut() + $dropdown.trigger('loaded.gl.dropdown') + $selectbox.hide() + data.issueURLSplit = issueURLSplit + labelCount = 0 + if data.labels.length + template = labelHTMLTemplate(data) + labelCount = data.labels.length + else + template = labelNoneHTMLTemplate() + $value + .removeAttr('style') + .html(template) + $sidebarCollapsedValue.text(labelCount) + + $value + .find('a') + .each((i) -> + setTimeout(=> + glAnimate($(@), 'pulse') + ,200 * i + ) + ) + + $dropdown.glDropdown( data: (term, callback) -> $.ajax( url: labelUrl ).done (data) -> - if showNo - data.unshift( - id: 0 - title: 'No Label' - ) + if $dropdown.hasClass 'js-extra-options' + if showNo + data.unshift( + id: 0 + title: 'No Label' + ) - if showAny - data.unshift( - isAny: true - title: 'Any Label' - ) - - if data.length > 2 - data.splice 2, 0, 'divider' + if showAny + data.unshift( + isAny: true + title: 'Any Label' + ) + if data.length > 2 + data.splice 2, 0, 'divider' callback data + renderRow: (label) -> - if $.isArray(selectedLabel) - selected = '' - $.each selectedLabel, (i, selectedLbl) -> - selectedLbl = selectedLbl.trim() - if selected is '' and label.title is selectedLbl - selected = 'is-active' - else - selected = if label.title is selectedLabel then 'is-active' else '' + selectedClass = '' + if $selectbox.find("input[type='hidden']\ + [name='#{$dropdown.data('field-name')}']\ + [value='#{label.id}']").length + selectedClass = 'is-active' color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" "<li> - <a href='#' class='#{selected}'> + <a href='#' class='#{selectedClass}'> #{color} #{label.title} </a> @@ -133,6 +216,7 @@ class @LabelsSelect search: fields: ['title'] selectable: true + toggleLabel: (selected) -> if selected and selected.title isnt 'Any Label' selected.title @@ -142,15 +226,33 @@ class @LabelsSelect id: (label) -> if label.isAny? '' - else + else if $dropdown.hasClass "js-filter-submit" label.title - clicked: -> + else + label.id + + hidden: -> + $selectbox.hide() + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + if $dropdown.hasClass 'js-multiselect' + 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' if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedLabel = label.title + Issues.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() + else + if $dropdown.hasClass 'js-multiselect' + return + else + saveLabelData() ) diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee new file mode 100644 index 00000000000..8f892b5a2b9 --- /dev/null +++ b/app/assets/javascripts/lib/animate.js.coffee @@ -0,0 +1,13 @@ +((w) -> + + w.glAnimate = ($el, animation, done) -> + $el + .removeClass() + .addClass(animation + ' animated') + .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> + $(this).removeClass() + return + return + return + +) window
\ No newline at end of file diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee new file mode 100644 index 00000000000..3f9ca39912c --- /dev/null +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -0,0 +1,30 @@ +((w) -> + notificationGranted = (message, opts, onclick) -> + notification = new Notification(message, opts) + + if onclick + notification.onclick = onclick + + notifyPermissions = -> + if 'Notification' of window + Notification.requestPermission() + + notifyMe = (message, body, icon, onclick) -> + opts = + body: body + icon: icon + # Let's check if the browser supports notifications + if !('Notification' of window) + # do nothing + else if Notification.permission == 'granted' + # If it's okay let's create a notification + notificationGranted message, opts, onclick + else if Notification.permission != 'denied' + Notification.requestPermission (permission) -> + # If the user accepts, let's create a notification + if permission == 'granted' + notificationGranted message, opts, onclick + + w.notify = notifyMe + w.notifyPermissions = notifyPermissions +) window diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 738ffc8343b..7102a0673e9 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -2,13 +2,18 @@ class @MergeRequestWidget # Initialize MergeRequestWidget behavior # # check_enable - Boolean, whether to check automerge status - # url_to_automerge_check - String, URL to use to check automerge status - # current_status - String, current automerge status - # ci_enable - Boolean, whether a CI service is enabled - # url_to_ci_check - String, URL to use to check CI status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status # + constructor: (@opts) -> - modal = $('#modal_merge_info').modal(show: false) + $('#modal_merge_info').modal(show: false) + @firstCICheck = true + @readyForCICheck = true + clearInterval @fetchBuildStatusInterval + + @pollCIStatus() + notifyPermissions() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -27,18 +32,57 @@ class @MergeRequestWidget dataType: 'json' getMergeStatus: -> - $.get @opts.url_to_automerge_check, (data) -> + $.get @opts.merge_check_url, (data) -> $('.mr-state-widget').replaceWith(data) - getCiStatus: -> - if @opts.ci_enable - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status + ciLabelForStatus: (status) -> + if status == 'success' + 'passed' + else + status + + pollCIStatus: -> + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + + @getCIStatus(true) + + @readyForCICheck = false + ), 5000 + + getCIStatus: (showNotification) -> + _this = @ + $('.ci-widget-fetching').show() + + $.getJSON @opts.ci_status_url, (data) => + @readyForCICheck = true + + if @firstCICheck + @firstCICheck = false + @opts.ci_status = data.status + + if data.status isnt @opts.ci_status + @showCIStatus data.status if data.coverage - this.showCiCoverage data.coverage - , 'json' + @showCICoverage data.coverage + + if showNotification + message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) + message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) + + notify( + "Build #{@ciLabelForStatus(data.status)}", + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit _this.opts.builds_path + ) + + @opts.ci_status = data.status - showCiState: (state) -> + showCIStatus: (state) -> $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -52,7 +96,7 @@ class @MergeRequestWidget $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') - showCiCoverage: (coverage) -> + showCICoverage: (coverage) -> text = 'Coverage ' + coverage + '%' $('.ci_widget:visible .ci-coverage').text(text) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index e17a1adb648..f73127f49f0 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -1,36 +1,65 @@ class @MilestoneSelect - constructor: -> + constructor: (currentProject) -> + if currentProject? + _this = @ + @currentProject = JSON.parse(currentProject) $('.js-milestone-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') milestonesUrl = $dropdown.data('milestones') + issueUpdateURL = $dropdown.data('issueUpdate') selectedMilestone = $dropdown.data('selected') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') + showUpcoming = $dropdown.data('show-upcoming') useId = $dropdown.data('use-id') defaultLabel = $dropdown.data('default-label') + issuableId = $dropdown.data('issuable-id') + abilityName = $dropdown.data('ability-name') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon') + $value = $block.find('.value') + $loading = $block.find('.block-loading').fadeOut() + + if issueUpdateURL + milestoneLinkTemplate = _.template( + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>' + ) + + milestoneLinkNoneTemplate = '<div class="light">None</div>' $dropdown.glDropdown( data: (term, callback) -> $.ajax( url: milestonesUrl ).done (data) -> + extraOptions = [] + if showAny + extraOptions.push( + id: 0 + name: '' + title: 'Any Milestone' + ) + if showNo - data.unshift( - id: '0' + extraOptions.push( + id: -1 + name: 'No Milestone' title: 'No Milestone' ) - if showAny - data.unshift( - isAny: true - title: 'Any Milestone' + if showUpcoming + extraOptions.push( + id: -2 + name: '#upcoming' + title: 'Upcoming' ) - if data.length > 2 - data.splice 2, 0, 'divider' + if extraOptions.length > 2 + extraOptions.push 'divider' - callback(data) + callback(extraOptions.concat(data)) filterable: true search: fields: ['title'] @@ -45,21 +74,51 @@ class @MilestoneSelect milestone.title id: (milestone) -> if !useId - if !milestone.isAny? - milestone.title - else - '' + milestone.name else milestone.id isSelected: (milestone) -> - milestone.title is selectedMilestone - clicked: -> - page = $('body').data 'page' - isIssueIndex = page is 'projects:issues:index' - isMRIndex = page is page is 'projects:merge_requests:index' + milestone.name is selectedMilestone + hidden: -> + $selectbox.hide() - if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + clicked: (selected) -> + if $dropdown.hasClass 'js-filter-bulk-update' + return + + if $dropdown.hasClass('js-filter-submit') + if selected.name? + selectedMilestone = selected.name + else + selectedMilestone = '' Issues.filterResults $dropdown.closest('form') - else if $dropdown.hasClass 'js-filter-submit' - $dropdown.closest('form').submit() + else + selected = $selectbox + .find('input[type="hidden"]') + .val() + data = {} + data[abilityName] = {} + data[abilityName].milestone_id = selected + $loading + .fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $.ajax( + type: 'PUT' + url: issueUpdateURL + data: data + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $loading.fadeOut() + $selectbox.hide() + $value.removeAttr('style') + if data.milestone? + data.milestone.namespace = _this.currentProject.namespace + data.milestone.path = _this.currentProject.path + $value.html(milestoneLinkTemplate(data.milestone)) + $sidebarCollapsedValue.find('span').text(data.milestone.title) + else + $value.html(milestoneLinkNoneTemplate) + $sidebarCollapsedValue.find('span').text('No') ) diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee new file mode 100644 index 00000000000..67403554340 --- /dev/null +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -0,0 +1,55 @@ +class @Sidebar + constructor: (currentUser) -> + @addEventListeners() + + addEventListeners: -> + $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked) + $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden) + $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) + $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) + + sidebarDropdownLoading: (e) -> + $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') + img = $sidebarCollapsedIcon.find('img') + i = $sidebarCollapsedIcon.find('i') + $loading = $('<i class="fa fa-spinner fa-spin"></i>') + if img.length + img.before($loading) + img.hide() + else if i.length + i.before($loading) + i.hide() + + sidebarDropdownLoaded: (e) -> + $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') + img = $sidebarCollapsedIcon.find('img') + $sidebarCollapsedIcon.find('i.fa-spin').remove() + i = $sidebarCollapsedIcon.find('i') + if img.length + img.show() + else + i.show() + + + sidebarCollapseClicked: (e) -> + e.preventDefault() + $block = $(@).closest('.block') + + $('aside') + .find('.gutter-toggle') + .trigger('click') + $editLink = $block.find('.edit-link') + + if $editLink.length + $editLink.trigger('click') + $block.addClass('collapse-after-update') + $('.page-with-sidebar').addClass('with-overlay') + + sidebarDropdownHidden: (e) -> + $block = $(@).closest('.block') + if $block.hasClass('collapse-after-update') + $block.removeClass('collapse-after-update') + $('.page-with-sidebar').removeClass('with-overlay') + $('aside') + .find('.gutter-toggle') + .trigger('click')
\ No newline at end of file diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index c1801365266..030655491bf 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,11 +1,270 @@ class @SearchAutocomplete - constructor: (search_autocomplete_path, project_id, project_ref) -> - project_id = '' unless project_id - project_ref = '' unless project_ref - query = "?project_id=" + project_id + "&project_ref=" + project_ref - - $("#search").autocomplete - source: search_autocomplete_path + query - minLength: 1 - select: (event, ui) -> - location.href = ui.item.url + + KEYCODE = + ESCAPE: 27 + BACKSPACE: 8 + ENTER: 13 + + constructor: (opts = {}) -> + { + @wrap = $('.search') + + @optsEl = @wrap.find('.search-autocomplete-opts') + @autocompletePath = @optsEl.data('autocomplete-path') + @projectId = @optsEl.data('autocomplete-project-id') || '' + @projectRef = @optsEl.data('autocomplete-project-ref') ||Â '' + + } = opts + + # Dropdown Element + @dropdown = @wrap.find('.dropdown') + @dropdownContent = @dropdown.find('.dropdown-content') + + @locationBadgeEl = @getElement('.search-location-badge') + @locationText = @getElement('.location-text') + @scopeInputEl = @getElement('#scope') + @searchInput = @getElement('.search-input') + @projectInputEl = @getElement('#search_project_id') + @groupInputEl = @getElement('#group_id') + @searchCodeInputEl = @getElement('#search_code') + @repositoryInputEl = @getElement('#repository_ref') + @clearInput = @getElement('.js-clear-input') + + @saveOriginalState() + + # Only when user is logged in + @createAutocomplete() if gon.current_user_id + + @searchInput.addClass('disabled') + + @saveTextLength() + + @bindEvents() + + # Finds an element inside wrapper element + getElement: (selector) -> + @wrap.find(selector) + + saveOriginalState: -> + @originalState = @serializeState() + + saveTextLength: -> + @lastTextLength = @searchInput.val().length + + createAutocomplete: -> + @searchInput.glDropdown + filterInputBlur: false + filterable: true + filterRemote: true + highlight: true + enterCallback: false + filterInput: 'input#search' + search: + fields: ['text'] + data: @getData.bind(@) + + getData: (term, callback) -> + _this = @ + + # Do not trigger request if input is empty + return if @searchInput.val() is '' + + # Prevent multiple ajax calls + return if @loadingSuggestions + + @loadingSuggestions = true + + jqXHR = $.get(@autocompletePath, { + project_id: @projectId + project_ref: @projectRef + term: term + }, (response) -> + # Hide dropdown menu if no suggestions returns + if !response.length + _this.disableAutocomplete() + return + + data = [] + + # List results + firstCategory = true + for suggestion in response + + # Add group header before list each group + if lastCategory isnt suggestion.category + data.push 'separator' if !firstCategory + + firstCategory = false if firstCategory + + data.push + header: suggestion.category + + lastCategory = suggestion.category + + data.push + text: suggestion.label + url: suggestion.url + + # Add option to proceed with the search + if data.length + data.push('separator') + data.push + text: "Result name contains \"#{term}\"" + url: "/search?\ + search=#{term}\ + &project_id=#{_this.projectInputEl.val()}\ + &group_id=#{_this.groupInputEl.val()}" + + callback(data) + ).always -> + _this.loadingSuggestions = false + + serializeState: -> + { + # Search Criteria + search_project_id: @projectInputEl.val() + group_id: @groupInputEl.val() + search_code: @searchCodeInputEl.val() + repository_ref: @repositoryInputEl.val() + scope: @scopeInputEl.val() + + # Location badge + _location: @locationText.text() + } + + bindEvents: -> + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'keyup', @onSearchInputKeyUp + @searchInput.on 'click', @onSearchInputClick + @searchInput.on 'focus', @onSearchInputFocus + @searchInput.on 'blur', @onSearchInputBlur + @clearInput.on 'click', @onRemoveLocationClick + + enableAutocomplete: -> + # No need to enable anything if user is not logged in + return if !gon.current_user_id + + _this = @ + @loadingSuggestions = false + + @dropdown.addClass('open') + @searchInput.removeClass('disabled') + + onSearchInputKeyDown: => + # Saves last length of the entered text + @saveTextLength() + + onSearchInputKeyUp: (e) => + switch e.keyCode + when KEYCODE.BACKSPACE + # when trying to remove the location badge + if @lastTextLength is 0 and @badgePresent() + @removeLocationBadge() + + # When removing the last character and no badge is present + if @lastTextLength is 1 + @disableAutocomplete() + + # When removing any character from existin value + if @lastTextLength > 1 + @enableAutocomplete() + + when KEYCODE.ESCAPE + @restoreOriginalState() + + else + # Handle the case when deleting the input value other than backspace + # e.g. Pressing ctrl + backspace or ctrl + x + if @searchInput.val() is '' + @disableAutocomplete() + else + # We should display the menu only when input is not empty + @enableAutocomplete() + + # Avoid falsy value to be returned + return + + onSearchInputClick: (e) => + # Prevents closing the dropdown menu + e.stopImmediatePropagation() + + onSearchInputFocus: => + @wrap.addClass('search-active') + + onRemoveLocationClick: (e) => + e.preventDefault() + @removeLocationBadge() + @searchInput.val('').focus() + @skipBlurEvent = true + + onSearchInputBlur: (e) => + @skipBlurEvent = false + + # We should wait to make sure we are not clearing the input instead + setTimeout( => + return if @skipBlurEvent + + @wrap.removeClass('search-active') + + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() + , 150) + + addLocationBadge: (item) -> + category = if item.category? then "#{item.category}: " else '' + value = if item.value? then item.value else '' + + html = "<span class='location-badge'> + <i class='location-text'>#{category}#{value}</i> + </span>" + @locationBadgeEl.html(html) + @wrap.addClass('has-location-badge') + + restoreOriginalState: -> + inputs = Object.keys @originalState + + for input in inputs + @getElement("##{input}").val(@originalState[input]) + + + if @originalState._location is '' + @locationBadgeEl.empty() + else + @addLocationBadge( + value: @originalState._location + ) + + @dropdown.removeClass 'open' + + badgePresent: -> + @locationBadgeEl.children().length + + resetSearchState: -> + inputs = Object.keys @originalState + + for input in inputs + + # _location isnt a input + break if input is '_location' + + @getElement("##{input}").val('') + + removeLocationBadge: -> + @locationBadgeEl.empty() + + # Reset state + @resetSearchState() + + @wrap.removeClass('has-location-badge') + + disableAutocomplete: -> + @searchInput.addClass('disabled') + @dropdown.removeClass('open') + @restoreMenu() + + restoreMenu: -> + html = "<ul> + <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> + </ul>" + @dropdownContent.html(html) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 860d4f438d0..e1778511240 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) setTimeout ( -> diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index b6b4bd90e6a..ec2df6c5b73 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -6,10 +6,12 @@ class @Todos clearListeners: -> $('.done-todo').off('click') $('.js-todos-mark-all').off('click') + $('.todo').off('click') initBtnListeners: -> $('.done-todo').on('click', @doneClicked) $('.js-todos-mark-all').on('click', @allDoneClicked) + $('.todo').on('click', @goToTodoUrl) doneClicked: (e) => e.preventDefault() @@ -54,3 +56,6 @@ class @Todos updateBadges: (data) -> $('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-done .badge').text data.done_count + + goToTodoUrl: -> + Turbolinks.visit($(this).data('url')) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 84193400890..eee9b6e690e 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,7 +1,9 @@ class @UsersSelect - constructor: -> + constructor: (currentUser) -> @usersPath = "/autocomplete/users.json" @userPath = "/autocomplete/users/:id.json" + if currentUser? + @currentUser = JSON.parse(currentUser) $('.js-user-search').each (i, dropdown) => $dropdown = $(dropdown) @@ -12,6 +14,81 @@ class @UsersSelect firstUser = $dropdown.data('first-user') selectedId = $dropdown.data('selected') defaultLabel = $dropdown.data('default-label') + issueURL = $dropdown.data('issueUpdate') + $selectbox = $dropdown.closest('.selectbox') + $block = $selectbox.closest('.block') + abilityName = $dropdown.data('ability-name') + $value = $block.find('.value') + $collapsedSidebar = $block.find('.sidebar-collapsed-user') + $loading = $block.find('.block-loading').fadeOut() + + $block.on('click', '.js-assign-yourself', (e) => + e.preventDefault() + assignTo(@currentUser.id) + ) + + assignTo = (selected) -> + data = {} + data[abilityName] = {} + data[abilityName].assignee_id = selected + $loading + .fadeIn() + $dropdown.trigger('loading.gl.dropdown') + $.ajax( + type: 'PUT' + dataType: 'json' + url: issueURL + data: data + ).done (data) -> + $dropdown.trigger('loaded.gl.dropdown') + $loading.fadeOut() + $selectbox.hide() + + if data.assignee + user = + name: data.assignee.name + username: data.assignee.username + avatar: data.assignee.avatar_url + else + user = + name: 'Unassigned' + username: '' + avatar: '' + $value.html(assigneeTemplate(user)) + $collapsedSidebar.html(collapsedAssigneeTemplate(user)) + + + collapsedAssigneeTemplate = _.template( + '<% if( avatar ) { %> + <a class="author_link" href="/u/<%= username %>"> + <img width="24" class="avatar avatar-inline s24" alt="" src="<%= avatar %>"> + <span class="author">Toni Boehm</span> + </a> + <% } else { %> + <i class="fa fa-user"></i> + <% } %>' + ) + + assigneeTemplate = _.template( + '<% if (username) { %> + <a class="author_link " href="/u/<%= username %>"> + <% if( avatar ) { %> + <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>"> + <% } %> + <span class="author"><%= name %></span> + <span class="username"> + @<%= username %> + </span> + </a> + <% } else { %> + <span class="assign-yourself"> + No assignee - + <a href="#" class="js-assign-yourself"> + assign yourself + </a> + </span> + <% } %>' + ) $dropdown.glDropdown( data: (term, callback) => @@ -57,20 +134,38 @@ class @UsersSelect fields: ['name', 'username'] selectable: true fieldName: $dropdown.data('field-name') + toggleLabel: (selected) -> if selected && 'id' of selected selected.name else defaultLabel - clicked: -> + + inputId: 'issue_assignee_id' + + hidden: (e) -> + $selectbox.hide() + # display:block overrides the hide-collapse rule + $value.removeAttr('style') + + clicked: (user) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is page is 'projects:merge_requests:index' + if $dropdown.hasClass('js-filter-bulk-update') + return if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + selectedId = user.id Issues.filterResults $dropdown.closest('form') else if $dropdown.hasClass 'js-filter-submit' $dropdown.closest('form').submit() + else + selected = $dropdown + .closest('.selectbox') + .find("input[name='#{$dropdown.data('field-name')}']").val() + assignTo(selected) + renderRow: (user) -> username = if user.username then "@#{user.username}" else "" avatar = if user.avatar_url then user.avatar_url else false @@ -87,17 +182,25 @@ class @UsersSelect if avatar img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" - "<li> - <a href='#' class='dropdown-menu-user-link #{selected}'> - #{img} - <strong class='dropdown-menu-user-full-name'> - #{user.name} - </strong> - <span class='dropdown-menu-user-username'> - #{username} - </span> - </a> - </li>" + # split into three parts so we can remove the username section if nessesary + listWithName = "<li> + <a href='#' class='dropdown-menu-user-link #{selected}'> + #{img} + <strong class='dropdown-menu-user-full-name'> + #{user.name} + </strong>" + + listWithUserName = "<span class='dropdown-menu-user-username'> + #{username} + </span>" + listClosingTags = "</a> + </li>" + + + if username is '' + listWithUserName = '' + + listWithName + listWithUserName + listClosingTags ) $('.ajax-users-select').each (i, select) => diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e2d590f4df4..69b3b6586de 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,6 +10,7 @@ *= require dropzone/basic *= require cal-heatmap *= require cropper.css + *= require animate */ /* diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss index 469f4f296ae..542a53f0377 100644 --- a/app/assets/stylesheets/behaviors.scss +++ b/app/assets/stylesheets/behaviors.scss @@ -13,10 +13,10 @@ // Toggle between two states. .js-toggler-container { - .turn-on { display: block; } + .turn-on { display: block; } .turn-off { display: none; } &.on { - .turn-on { display: none; } + .turn-on { display: none; } .turn-off { display: block; } } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 9b676d759e0..db1a8b1bf78 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -121,7 +121,7 @@ p.time { text-shadow: none; } -.thin_area{ +.thin_area { height: 150px; } @@ -148,7 +148,7 @@ li.note { } } -.wiki_content code, .readme code{ +.wiki_content code, .readme code { background-color: inherit; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2d616fc660c..82dc1acbd01 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -42,7 +42,7 @@ font-size: 15px; text-align: left; border: 1px solid $dropdown-toggle-border-color; - border-radius: 2px; + border-radius: $dropdown-border-radius; outline: 0; text-overflow: ellipsis; white-space: nowrap; @@ -75,12 +75,12 @@ width: 240px; margin-top: 2px; margin-bottom: 0; - padding: 10px; - font-size: 14px; + font-size: 15px; font-weight: normal; + padding: 10px 0; background-color: $dropdown-bg; border: 1px solid $dropdown-border-color; - border-radius: $border-radius-base; + border-radius: $dropdown-border-radius; box-shadow: 0 2px 4px $dropdown-shadow-color; &.is-loading { @@ -101,9 +101,17 @@ li { text-align: left; list-style: none; + padding: 0 10px; } .divider { + height: 1px; + margin: 8px 10px; + padding: 0; + background-color: $dropdown-divider-color; + } + + .separator { width: 100%; height: 1px; margin-top: 8px; @@ -136,6 +144,21 @@ background-color: $dropdown-empty-row-bg; } } + + &.dropdown-menu-user-link { + line-height: 16px; + } + } + + .dropdown-header { + color: $dropdown-header-color; + font-size: 13px; + line-height: 22px; + padding: 0 10px 10px; + } + + .separator + .dropdown-header { + padding-top: 2px; } } @@ -154,6 +177,10 @@ .dropdown-menu-back { display: block; } + + .dropdown-content { + padding: 0 10px; + } } } @@ -167,13 +194,13 @@ } .dropdown-menu-user-link { - padding-top: 7px; + padding-top: 10px; padding-bottom: 7px; } .dropdown-menu-user-full-name { display: block; - font-weight: 600; + font-weight: 500; line-height: 16px; text-overflow: ellipsis; overflow: hidden; @@ -189,7 +216,7 @@ } .dropdown-select { - width: 300px; + width: $dropdown-width; } .dropdown-menu-align-right { @@ -218,20 +245,11 @@ } } -.dropdown-header { - padding-left: 5px; - padding-right: 5px; - color: $dropdown-header-color; - font-size: 13px; - line-height: 22px; -} .dropdown-title { position: relative; - margin-bottom: 10px; - padding-left: 30px; - padding-right: 30px; - padding-bottom: 10px; + padding: 0 0 15px; + margin: 0 10px 10px; font-weight: 600; line-height: 1; text-align: center; @@ -257,21 +275,26 @@ } .dropdown-menu-close { - right: 0; + right: 7px; + width: 20px; + height: 20px; + top: -1px; } .dropdown-menu-back { - left: 0; + left: 7px; + top: 2px; } .dropdown-input { position: relative; margin-bottom: 10px; + padding: 0 10px; .fa { position: absolute; top: 10px; - right: 10px; + right: 20px; color: #c7c7c7; font-size: 12px; pointer-events: none; @@ -281,6 +304,9 @@ display: none; cursor: pointer; pointer-events: all; + right: 22px; + top: 9px; + font-size: 14px; } &.has-value { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ad0e88cda86..a26ace5cc19 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -3,12 +3,10 @@ * */ .file-holder { - border: none; border: 1px solid $border-color; &.readme-holder { - margin-top: 10px; - border-bottom: 0; + margin: $gl-padding-top 0; } table { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b05c5df1bd8..9209347f9bc 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -3,7 +3,7 @@ vertical-align: top; } -@media (min-width: $screen-sm-min) { +@media (min-width: $screen-sm-min) { .issues-filters, .issues_bulk_update { .dropdown-menu-toggle { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4cb4129b71b..54cb5461113 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -6,40 +6,6 @@ input { border-radius: $border-radius-base; } -input[type='search'] { - background-color: white; - padding-left: 10px; -} - -input[type='search'].search-input { - background-repeat: no-repeat; - background-position: 10px; - background-size: 16px; - background-position-x: 30%; - padding-left: 10px; - background-color: $gray-light; - - &.search-input[value=""] { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); - } - - &.search-input::-webkit-input-placeholder { - text-align: center; - } - - &.search-input:-moz-placeholder { /* Firefox 18- */ - text-align: center; - } - - &.search-input::-moz-placeholder { /* Firefox 19+ */ - text-align: center; - } - - &.search-input:-ms-input-placeholder { - text-align: center; - } -} - input[type='text'].danger { background: #f2dede!important; border-color: #d66; @@ -125,7 +91,7 @@ label { } .form-control::-webkit-input-placeholder { - color: #7f8fa4; + color: $gl-placeholder-color; } .input-group { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c83cf881596..fa9038ebaca 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -33,10 +33,15 @@ background: $color; } + .complex-sidebar .nav-primary { + border-right: 1px solid lighten($color, 3%); + } + .sidebar-wrapper { background: $color-darker; .sidebar-user { + border-top: 1px solid lighten($color, 3%); background: $color-darker; color: $color-light; @@ -62,7 +67,6 @@ .count { color: $color-light; - background: $color-dark; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 6a68bb5c115..724980b2208 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -36,7 +36,7 @@ header { padding: 0; .nav > li > a { - color: #7f8fa4; + color: $gl-icon-color; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -62,7 +62,7 @@ header { background-color: #eee; } &.active { - color: #7f8fa4; + color: $gl-icon-color; } } } @@ -81,14 +81,14 @@ header { font-size: 19px; line-height: $header-height; font-weight: normal; - color: #4c4e54; + color: $gl-text-color; overflow: hidden; text-overflow: ellipsis; vertical-align: top; white-space: nowrap; a { - color: #4c4e54; + color: $gl-text-color; &:hover { text-decoration: underline; } @@ -117,37 +117,17 @@ header { } } - .search { - margin-right: 10px; - margin-left: 10px; - margin-top: ($header-height - 36) / 2; - - form { - margin: 0; - padding: 0; - } - - .search-input { - width: 220px; - - &:focus { - @include box-shadow(none); - outline: none; - } - } - } - .impersonation i { color: $red-normal; } } @mixin collapsed-header { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; } .header-collapsed { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; @media (min-width: $screen-md-min) { @include collapsed-header; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 5ea4f9a49db..66180f38a4f 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -107,7 +107,7 @@ } .page-title { - .note_created_ago, .new-issue-link { + .note-created-ago, .new-issue-link { display: none; } } @@ -116,7 +116,7 @@ display: none; } - aside:not(.right-sidebar){ + aside:not(.right-sidebar) { display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 95bdd6d1ea3..fc3b0a422a7 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -100,6 +100,7 @@ > form { display: inline-block; + margin-top: -1px; } .icon-label { @@ -110,7 +111,7 @@ height: 34px; display: inline-block; position: relative; - top: 1px; + top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index e82d052f45a..b2fab387e17 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -51,7 +51,7 @@ padding: 10px 15px; } -.select2-drop{ +.select2-drop { color: #7f8fa4; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 9d188317783..1d49249dd80 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -144,7 +144,7 @@ } a { - padding: 7px 15px; + padding: 7px 12px; font-size: $gl-font-size; line-height: 24px; color: $gray; @@ -169,10 +169,12 @@ } .count { - float: right; - background: #eee; - padding: 0 8px; - @include border-radius(6px); + &:before { + content: '('; + } + &:after { + content: ')'; + } } &.back-link i { @@ -191,6 +193,27 @@ } } +.expand-nav a { + color: $gl-icon-color; + width: 60px; + position: fixed; + top: 0; + left: 0; + font-size: 20px; + background: #fff; + height: 59px; + text-align: center; + line-height: 59px; + border-bottom: 1px solid #eee; + transition-duration: .3s; + outline: none; + z-index: 100; + + &:hover { + text-decoration: none; + } +} + .collapse-nav a { width: $sidebar_width; position: fixed; @@ -210,55 +233,12 @@ } .page-sidebar-collapsed { - padding-left: $sidebar_collapsed_width; - .sidebar-wrapper { - width: $sidebar_collapsed_width; - - .header-logo { - width: $sidebar_collapsed_width; - - a { - padding-left: ($sidebar_collapsed_width - 36) / 2; - - .gitlab-text-container { - display: none; - } - } - } - - .nav-sidebar { - width: $sidebar_collapsed_width; - - li { - width: auto; - - a { - span { - display: none; - } - } - } - } - - .collapse-nav a { - width: $sidebar_collapsed_width; - } - - .sidebar-user { - padding-left: ($sidebar_collapsed_width - 36) / 2; - width: $sidebar_collapsed_width; - - .username { - display: none; - } - } + display: none; } } .page-sidebar-expanded { - padding-left: $sidebar_collapsed_width; - @media (min-width: $screen-md-min) { padding-left: $sidebar_width; } @@ -288,6 +268,10 @@ @media (min-width: $screen-sm-min) { padding-right: $sidebar_collapsed_width; } + + .sidebar-collapsed-icon { + cursor: pointer; + } } .right-sidebar-expanded { @@ -300,4 +284,53 @@ @media (min-width: $screen-md-min) { padding-right: $gutter_width; } + + &.with-overlay { + padding-right: $sidebar_collapsed_width; + } +} + +.complex-sidebar { + display: inline-block; + + .nav-primary { + width: 61px; + float: left; + height: 100vh; + + .nav-sidebar { + width: 60px; + + li a { + width: 60px; + + span { + display: none; + } + } + } + } + + .nav-secondary { + $nav-secondary-width: 168px; + + float: left; + width: $nav-secondary-width; + + .nav-sidebar { + width: $nav-secondary-width; + + li { + width: $nav-secondary-width; + + a { + width: $nav-secondary-width; + + i { + display: none; + } + } + } + } + } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index f63ac033234..c72af5dad0a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -56,8 +56,8 @@ $component-active-bg: $brand-info; //## $input-color: $text-color; -$input-border: #e7e9ed; -$input-border-focus: #7f8fa4; +$input-border: $border-color; +$input-border-focus: $focus-border-color; $legend-color: $text-color; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 61e0dd4d672..98fe794d362 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -11,6 +11,7 @@ $gutter_inner_width: 258px; * UI elements */ $border-color: #efeff1; +$focus-border-color: #3aabf0; $table-border-color: #eef0f2; $background-color: #faf9f9; @@ -26,6 +27,7 @@ $gl-text-orange: #d90; $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; +$gl-icon-color: $gl-placeholder-color; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; @@ -66,7 +68,7 @@ $header-height: 58px; $fixed-layout-width: 1280px; $gl-avatar-size: 40px; $error-exclamation-point: #e62958; -$border-radius-default: 3px; +$border-radius-default: 2px; $btn-transparent-color: #8f8f8f; $ssh-key-icon-color: #8f8f8f; $ssh-key-icon-size: 18px; @@ -166,6 +168,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif /* * Dropdowns */ +$dropdown-border-radius: 2px; +$dropdown-width: 300px; $dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; @@ -176,8 +180,8 @@ $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; -$dropdown-input-focus-border: rgb(58, 171, 240); -$dropdown-input-focus-shadow: rgba(#000, .2); +$dropdown-input-focus-border: $focus-border-color; +$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-toggle-bg: #fff; @@ -193,3 +197,23 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $award-emoji-menu-bg: #fff; $award-emoji-menu-border: #f1f2f4; $award-emoji-new-btn-icon-color: #dcdcdc; + +/* + * Search Box + */ +$search-input-border-color: $dropdown-input-focus-border; +$search-input-focus-shadow-color: $dropdown-input-focus-shadow; +$search-input-width: $dropdown-width; +$location-badge-color: #aaa; +$location-badge-bg: $gray-normal; +$location-icon-color: #e7e9ed; +$location-active-color: $gl-text-color; +$location-active-bg: $search-input-border-color; + +/* + * Notes + */ +$notes-light-color: #8e8e8e; +$notes-action-color: #c3c3c3; +$notes-role-color: #8e8e8e; +$notes-role-border-color: #e4e4e4; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 28994e60baa..37bf38fa65d 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -37,7 +37,7 @@ height: 300px; overflow-y: scroll; - input.emoji-search{ + input.emoji-search { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); background-repeat: no-repeat; background-position: right 5px center; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 2a7b5cfc7fd..67a9d7d2cf7 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -42,7 +42,7 @@ } } - .loading{ + .loading { font-size: 20px; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 971656feb42..082911bd118 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -1,15 +1,15 @@ -.commit-title{ +.commit-title { display: block; } -.commit-author, .commit-committer{ +.commit-author, .commit-committer { display: block; color: #999; font-weight: normal; font-style: italic; } -.commit-author strong, .commit-committer strong{ +.commit-author strong, .commit-committer strong { font-weight: bold; font-style: normal; } @@ -74,7 +74,7 @@ color: $gl-text-red; } } - .edit-file{ + .edit-file { a { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 5e91496679a..8272615768d 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,4 +1,4 @@ -.commits-compare-switch{ +.commits-compare-switch { @include btn-default; @include btn-white; background: image-url("switch_icon.png") no-repeat center center; @@ -93,7 +93,6 @@ li.commit { .commit-row-info { color: $gl-gray; line-height: 24px; - font-size: 13px; a { color: $gl-gray; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 43be5e38ba8..0f0592a0ab8 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,5 +1,5 @@ .file-editor { - #editor{ + #editor { border: none; @include border-radius(0); height: 500px; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 84eefd01cfe..c66efe978cd 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -43,10 +43,6 @@ .md { color: #7f8fa4; font-size: $gl-font-size; - - iframe.twitter-share-button { - vertical-align: bottom; - } } pre { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 5300bb52a1b..88c1b614c74 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -30,6 +30,10 @@ } .issuable-sidebar { + a { + color: inherit; + } + .block { @include clearfix; padding: $gl-padding 0; @@ -89,7 +93,7 @@ } .cross-project-reference { - color: $gl-link-color; + color: inherit; span { white-space: nowrap; @@ -133,6 +137,12 @@ .value { line-height: 1; + + .assign-yourself { + margin-top: 10px; + font-weight: normal; + display: block; + } } .bold { @@ -252,6 +262,15 @@ text-decoration: none; } } + + .dropdown-menu-toggle { + width: 100%; + padding-top: 6px; + } + + .open .dropdown-menu { + width: 100%; + } } .btn-default.gutter-toggle { diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index 6d2bd33b28b..6926448519e 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -1,9 +1,9 @@ .ci-body { - .incorrect-syntax{ + .incorrect-syntax { font-size: 19px; color: red; } - .correct-syntax{ + .correct-syntax { font-size: 19px; color: #47a447; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 777bcbca5c3..403171d4532 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -36,7 +36,7 @@ } } - .login-box{ + .login-box { background: #fafafa; border-radius: 10px; box-shadow: 0 0 2px #ccc; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index cee5c47cfb2..7ff63ca20b6 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -230,3 +230,9 @@ } } } + +.builds { + .table-holder { + overflow-x: scroll; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index daf2651425f..655f88b0c2c 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -71,8 +71,6 @@ } .note-form-actions { - background: #fff; - .note-form-option { margin-top: 8px; margin-left: 30px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4bd2016bdcf..92fcaaeeacf 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -22,7 +22,7 @@ ul.notes { margin-left: 55px; } - .note_created_ago, .note-updated-at { + .note-created-ago, .note-updated-at { white-space: nowrap; } @@ -39,53 +39,6 @@ ul.notes { } } - .discussion-header, - .note-header { - @extend .cgray; - - a:hover { - text-decoration: none; - } - - .avatar { - float: left; - margin-right: 10px; - } - - .discussion-last-update, - .note-last-update { - &:before { - content: "\00b7"; - } - - a { - color: $gl-gray; - - &:hover { - text-decoration: underline; - } - } - } - .author { - color: #4c4e54; - margin-right: 3px; - - &:hover { - color: $gl-link-color; - } - } - .author-username { - } - - .note-role { - float: right; - margin-top: 1px; - border: 1px solid #bbb; - background-color: transparent; - color: $gl-gray; - } - } - .discussion-body { padding-top: 15px; } @@ -198,40 +151,88 @@ ul.notes { border-width: 1px 0; padding-top: 0; vertical-align: top; - &.parallel{ + &.parallel { border-width: 1px; } } } } +.discussion-header, +.note-header { + a { + color: inherit; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } + } + + .author_link { + font-weight: 600; + } +} + +.note-headline-light, +.discussion-headline-light { + color: $notes-light-color; +} + /** * Actions for Discussions/Notes */ -.discussion, -.note { - .discussion-actions, - .note-actions { - float: right; - margin-left: 10px; +.discussion-actions, +.note-actions { + float: right; + margin-left: 10px; + color: $notes-action-color; +} - a { - margin-left: 5px; - color: $gl-gray; +.note-action-button, +.discussion-action-button { + display: inline-block; + margin-left: 10px; + line-height: 24px; - i.fa { - font-size: 16px; - line-height: 16px; - } + .fa { + position: relative; + top: 1px; + font-size: 17px; + } - &:hover { - @extend .cgray; - &.danger { @extend .cred; } - } - } + .fa-trash-o { + top: 0; + font-size: 16px; } } + +.discussion-toggle-button { + line-height: 20px; + font-size: 13px; + + .fa { + margin-right: 3px; + font-size: 10px; + line-height: 18px; + vertical-align: top; + } +} + +.note-role { + position: relative; + top: -2px; + display: inline-block; + padding-left: 4px; + padding-right: 4px; + color: $notes-role-color; + font-size: 12px; + line-height: 20px; + border: 1px solid $notes-role-border-color; + border-radius: $border-radius-base; +} + .diff-file .note .note-actions { right: 0; top: 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 71bde1174ee..4e6aa8cd1a6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -162,7 +162,7 @@ margin-right: 12px; a { - margin: -1px !important; + margin: -1px; } } @@ -222,7 +222,7 @@ padding: 0; background: transparent; border: none; - line-height: 42px; + line-height: 36px; margin: 0; > li + li:before { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index b6e45024644..3c74d25beb0 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -21,3 +21,145 @@ } } +.search { + margin-right: 10px; + margin-left: 10px; + margin-top: ($header-height - 35) / 2; + + form { + @extend .form-control; + margin: 0; + padding: 4px; + width: $search-input-width; + line-height: 24px; + } + + .location-text { + font-style: normal; + } + + .search-input { + border: none; + font-size: 14px; + outline: none; + padding: 0; + margin-left: 5px; + line-height: 25px; + width: 98%; + } + + .location-badge { + line-height: 25px; + padding: 0 5px; + border-radius: $border-radius-default; + font-size: 14px; + font-style: normal; + color: $location-badge-color; + display: inline-block; + background-color: $location-badge-bg; + vertical-align: top; + } + + .search-input-container { + display: -webkit-flex; + display: flex; + position: relative; + } + + .search-location-badge, .search-input-wrap { + // Fallback if flexbox is not supported + display: inline-block; + } + + .search-input-wrap { + width: 100%; + + .search-icon, .clear-icon { + position: absolute; + right: 5px; + top: 0; + color: $location-icon-color; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + .search-icon { + @extend .fa-search; + @include transition(color .15s); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + .clear-icon { + @extend .fa-times; + display: none; + } + + // Rewrite position. Dropdown menu should be relative to .search-input-container + .dropdown { + position: static; + } + + .dropdown-header { + text-transform: uppercase; + font-size: 11px; + } + + // Custom dropdown positioning + .dropdown-menu { + top: 30px; + left: -5px; + padding: 0; + + ul { + padding: 10px 0; + } + } + + .dropdown-content { + max-height: 350px; + } + } + + &.search-active { + form { + @extend .form-control:focus; + border-color: $dropdown-input-focus-border; + box-shadow: 0 0 4px $search-input-focus-shadow-color; + } + + .location-badge { + @include transition(all .15s); + background-color: $location-active-bg; + color: $white-light; + } + + .search-input-wrap { + i { + color: $location-active-color; + } + } + + &.has-location-badge { + .search-icon { + display: none; + } + + .clear-icon { + cursor: pointer; + display: block; + } + } + } + + &.has-location-badge { + .search-input-wrap { + width: 78%; + } + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index f983e9829e6..e83fa9e3d52 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -6,13 +6,19 @@ .navbar-nav { li { .badge.todos-pending-count { - background-color: #7f8fa4; + background-color: $gl-icon-color; margin-top: -5px; font-weight: normal; } } } +.todo { + &:hover { + cursor: pointer; + } +} + .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ed9f6031389..f010436bd36 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :require_two_factor_authentication, :two_factor_grace_period, :gravatar_enabled, - :twitter_sharing_enabled, :sign_in_text, :help_page_text, :home_page_url, diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb index 081e01a75e0..8bf71a1adbb 100644 --- a/app/controllers/ci/projects_controller.rb +++ b/app/controllers/ci/projects_controller.rb @@ -1,11 +1,15 @@ module Ci class ProjectsController < Ci::ApplicationController before_action :project - before_action :authorize_read_project!, except: [:badge] before_action :no_cache, only: [:badge] + before_action :authorize_read_project!, except: [:badge, :index] skip_before_action :authenticate_user!, only: [:badge] protect_from_forgery + def index + redirect_to root_path + end + def show # Temporary compatibility with CI badges pointing to CI project page redirect_to namespace_project_path(project.namespace, project) @@ -35,5 +39,9 @@ module Ci response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end + + def authorize_read_project! + return access_denied! unless can?(current_user, :read_project, project) + end end end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 6ff47c4033a..6d4d4360988 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController before_action :no_cache_headers def build + badge = Gitlab::Badge::Build.new(project, params[:ref]) + respond_to do |format| format.html { render_404 } format.svg do - image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) - send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml') + send_data(badge.data, type: badge.type, disposition: 'inline') end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 877b39c9b1b..6d649e72f84 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController @merge_requests = @issue.referenced_merge_requests(current_user) @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) - respond_with(@issue) + respond_to do |format| + format.html + format.json do + render json: @issue.to_json(include: [:milestone, :labels]) + end + end + end def create @@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController end end format.json do - render json: { - saved: @issue.valid?, - assignee_avatar_url: @issue.assignee.try(:avatar_url) - } + render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) end end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b830d777752..49064f5d505 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html format.json { render json: @merge_request } - format.diff { render text: @merge_request.to_diff(current_user) } - format.patch { render text: @merge_request.to_patch(current_user) } + format.diff { render text: @merge_request.to_diff } + format.patch { render text: @merge_request.to_patch } end end @@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.target_project, @merge_request]) end format.json do - render json: { - saved: @merge_request.valid?, - assignee_avatar_url: @merge_request.assignee.try(:avatar_url) - } + render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }]) end end else @@ -227,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) + ci_commit = @merge_request.ci_commit + if ci_commit + status = ci_commit.status + coverage = ci_commit.try(:coverage) + else + ci_service = @merge_request.source_project.ci_service + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + if ci_service.respond_to?(:commit_coverage) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + end end response = { + title: merge_request.title, + sha: merge_request.last_commit_short_sha, status: status, coverage: coverage } diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b2e974eff17..f7b6d137bde 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -19,13 +19,12 @@ class Projects::MilestonesController < Projects::ApplicationController end @milestones = @milestones.includes(:project) - respond_to do |format| format.html do @milestones = @milestones.page(params[:page]) end format.json do - render json: @milestones + render json: @milestones.to_json(methods: :name) end end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index b578b419a46..6d2901a24a4 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read any snippet - before_action :authorize_read_project_snippet! + before_action :authorize_read_project_snippet!, except: [:new, :create, :index] # Allow write(create) snippet before_action :authorize_create_project_snippet!, only: [:new, :create] @@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end + def authorize_read_project_snippet! + return render_404 unless can?(current_user, :read_project_snippet, @snippet) + end + def authorize_update_project_snippet! return render_404 unless can?(current_user, :update_project_snippet, @snippet) end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 928817ba811..8c3a74c8236 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -71,7 +71,7 @@ class ProjectsController < Projects::ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.unlink_fork + if ::Projects::UnlinkForkService.new(@project, current_user).execute flash[:notice] = 'The fork relationship has been removed.' end end @@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) @suggestions = { - emojis: autocomplete_emojis, + emojis: AwardEmoji.urls, issues: autocomplete.issues, mergerequests: autocomplete.merge_requests, members: participants @@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController ) end - def autocomplete_emojis - Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do - Emoji.emojis.map do |name, emoji| - { - name: name, - path: view_context.image_url("#{emoji["unicode"]}.png") - } - end - end - end - def repo_exists? project.repository_exists? && !project.empty_repo? end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 046286dd9e1..f1df6832bf6 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -243,7 +243,7 @@ class IssuableFinder end def filter_by_upcoming_milestone? - params[:milestone_title] == '#upcoming' + params[:milestone_title] == Milestone::Upcoming.name end def by_milestone(items) @@ -252,7 +252,7 @@ class IssuableFinder items = items.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? upcoming = Milestone.where(project_id: projects).upcoming - items = items.joins(:milestone).where(milestones: { title: upcoming.title }) + items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) }) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 23693629a4c..60a0ff32c9c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -3,10 +3,6 @@ module ApplicationSettingsHelper current_application_settings.gravatar_enabled? end - def twitter_sharing_enabled? - current_application_settings.twitter_sharing_enabled? - end - def signup_enabled? current_application_settings.signup_enabled? end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 316a10b7da3..14697f774cc 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -60,7 +60,7 @@ module DropdownsHelper title_output << content_tag(:span, title) title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do - icon('times') + icon('times', class: 'dropdown-menu-close-icon') end title_output.html_safe diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d3e5e3aa8b9..592bad8ba24 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -214,4 +214,12 @@ module EventsHelper end end end + + def event_row_class(event) + if event.body? + "event-block" + else + "event-inline" + end + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 81df2094392..b14b8218d02 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -16,6 +16,16 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid > ?', issuable.iid).last end + def issuable_json_path(issuable) + project = issuable.project + + if issuable.kind_of?(MergeRequest) + namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json) + else + namespace_project_issue_path(project.namespace, project, issuable.iid, :json) + end + end + def prev_issuable_for(issuable) base_issuable_scope(issuable).where('iid < ?', issuable.iid).first end @@ -37,6 +47,14 @@ module IssuablesHelper end end + def milestone_dropdown_label(milestone_title, default_label = "Milestone") + if milestone_title == Milestone::Upcoming.name + milestone_title = Milestone::Upcoming.title + end + + h(milestone_title.presence || default_label) + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 53c543c28c5..698f90cb27a 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -5,8 +5,10 @@ module NotesHelper end def note_target_fields(note) - hidden_field_tag(:target_type, note.noteable.class.name.underscore) + - hidden_field_tag(:target_id, note.noteable.id) + if note.noteable + hidden_field_tag(:target_type, note.noteable.class.name.underscore) + + hidden_field_tag(:target_id, note.noteable.id) + end end def note_editable?(note) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 494dad0b41e..8a97a74ad73 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,4 +1,5 @@ module SearchHelper + def search_autocomplete_opts(term) return unless current_user @@ -23,45 +24,44 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "Profile settings", url: profile_path }, - { label: "SSH Keys", url: profile_keys_path }, - { label: "Dashboard", url: root_path }, - { label: "Admin Section", url: admin_root_path }, + { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "SSH Keys", url: profile_keys_path }, + { category: "Settings", label: "Dashboard", url: root_path }, + { category: "Settings", label: "Admin Section", url: admin_root_path }, ] end # Autocomplete results for internal help pages def help_autocomplete [ - { label: "help: API Help", url: help_page_path("api", "README") }, - { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, - { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, - { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, - { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, - { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, + { category: "Help", label: "API Help", url: help_page_path("api", "README") }, + { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, + { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, + { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, + { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, + { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, ] end # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -72,7 +72,9 @@ module SearchHelper def groups_autocomplete(term, limit = 5) current_user.authorized_groups.search(term).limit(limit).map do |group| { - label: "group: #{search_result_sanitize(group.name)}", + category: "Groups", + id: group.id, + label: "#{search_result_sanitize(group.name)}", url: group_path(group) } end @@ -83,7 +85,10 @@ module SearchHelper current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { - label: "project: #{search_result_sanitize(p.name_with_namespace)}", + category: "Projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.name_with_namespace)}", url: namespace_project_path(p.namespace, p) } end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 8cbc9eefc7b..826e5f96fa1 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -110,6 +110,10 @@ class Notify < BaseMailer headers['Reply-To'] = address + fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze + headers['References'] ||= '' + headers['References'] << ' ' << fallback_reply_message_id + @reply_by_email = true end diff --git a/app/models/ability.rb b/app/models/ability.rb index fa2345f6faa..c0bf6def7c5 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -27,6 +27,8 @@ class Ability case true when subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) + when subject.is_a?(ProjectSnippet) + anonymous_project_snippet_abilities(subject) when subject.is_a?(CommitStatus) anonymous_commit_status_abilities(subject) when subject.is_a?(Project) || subject.respond_to?(:project) @@ -100,6 +102,14 @@ class Ability end end + def anonymous_project_snippet_abilities(snippet) + if snippet.public? + [:read_project_snippet] + else + [] + end + end + def global_abilities(user) rules = [] rules << :create_group if user.can_create_group @@ -338,24 +348,22 @@ class Ability end end - [:note, :project_snippet].each do |name| - define_method "#{name}_abilities" do |user, subject| - rules = [] - - if subject.author == user - rules += [ - :"read_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end + def note_abilities(user, note) + rules = [] - if subject.respond_to?(:project) && subject.project - rules += project_abilities(user, subject.project) - end + if note.author == user + rules += [ + :read_note, + :update_note, + :admin_note + ] + end - rules + if note.respond_to?(:project) && note.project + rules += project_abilities(user, note.project) end + + rules end def personal_snippet_abilities(user, snippet) @@ -376,6 +384,24 @@ class Ability rules end + def project_snippet_abilities(user, snippet) + rules = [] + + if snippet.author == user || user.admin? + rules += [ + :read_project_snippet, + :update_project_snippet, + :admin_project_snippet + ] + end + + if snippet.public? || (snippet.internal? && !user.external?) || (snippet.private? && snippet.project.team.member?(user)) + rules << :read_project_snippet + end + + rules + end + def group_member_abilities(user, subject) rules = [] target_user = subject.user diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c4879598c4e..052cd874733 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -12,7 +12,6 @@ # updated_at :datetime # home_page_url :string(255) # default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null @@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/app/models/commit.rb b/app/models/commit.rb index d0dbe009d0d..d09876a07d9 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -74,14 +74,14 @@ class Commit # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?<commit>\h{7,40}) }x end def self.link_reference_pattern - super("commit", /(?<commit>\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) end def to_reference(from_project = nil) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 289dbc57287..51673897d98 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -43,14 +43,14 @@ class CommitRange # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?<commit_range>#{STRICT_PATTERN}) }x end def self.link_reference_pattern - super("compare", /(?<commit_range>#{PATTERN})/) + @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/) end # Initialize a CommitRange diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index cf5b2c71675..afa2ca039ae 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links + has_many :todos, as: :target, dependent: :destroy validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -41,7 +42,7 @@ module Issuable scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } - scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) } + scope :non_archived, -> { join_project.where(projects: { archived: false }) } delegate :name, :email, diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 2ca79df0a29..b8585d4e577 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} + @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end def to_reference(_from_project = nil) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 97bd79af083..da7c265a371 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -14,6 +14,7 @@ class GlobalMilestone def initialize(title, milestones) @title = title + @name = title @milestones = milestones end diff --git a/app/models/issue.rb b/app/models/issue.rb index ed960cb39f4..e064b0f8b95 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?<issue>\d+) }x end def self.link_reference_pattern - super("issues", /(?<issue>\d+)/) + @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/label.rb b/app/models/label.rb index 500d5a35521..55c01cae762 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -56,7 +56,7 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ef48207f956..bf185cb5dd8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?<merge_request>\d+) }x end def self.link_reference_pattern - super("merge_requests", /(?<merge_request>\d+)/) + @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/) end # Returns all the merge requests from an ActiveRecord:Relation. @@ -279,7 +280,7 @@ class MergeRequest < ActiveRecord::Base WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze def work_in_progress? - title =~ WIP_REGEX + !!(title =~ WIP_REGEX) end def wipless_title @@ -331,15 +332,15 @@ class MergeRequest < ActiveRecord::Base # Returns the raw diff for this merge request # # see "git diff" - def to_diff(current_user) - target_project.repository.diff_text(target_branch, source_sha) + def to_diff + target_project.repository.diff_text(diff_base_commit.sha, source_sha) end # Returns the commit as a series of email patches. # # see "git format-patch" - def to_patch(current_user) - target_project.repository.format_patch(target_branch, source_sha) + def to_patch + target_project.repository.format_patch(diff_base_commit.sha, source_sha) end def hook_attrs diff --git a/app/models/milestone.rb b/app/models/milestone.rb index bbd59eab9ae..986184dd301 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base end def self.link_reference_pattern - super("milestones", /(?<milestone>\d+)/) + @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/) end def self.upcoming @@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) "[#{escaped_title}](#{url})" diff --git a/app/models/note.rb b/app/models/note.rb index b0c33f2eec5..87ced65c650 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -311,7 +311,7 @@ class Note < ActiveRecord::Base for_merge_request? && for_diff_line? end - def for_project_snippet? + def for_snippet? noteable_type == "Snippet" end diff --git a/app/models/project.rb b/app/models/project.rb index 941e444a4f8..7b1188420ef 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -206,6 +206,8 @@ class Project < ActiveRecord::Base mount_uploader :avatar, AvatarUploader # Scopes + default_scope { where(pending_delete: false) } + scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } @@ -491,7 +493,7 @@ class Project < ActiveRecord::Base end def web_url - Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -612,7 +614,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -951,16 +953,6 @@ class Project < ActiveRecord::Base self.builds_enabled = true end - def unlink_fork - if forked? - forked_from_project.lfs_objects.find_each do |lfs_object| - lfs_object.projects << self - end - - forked_project_link.destroy - end - end - def any_runners?(&block) if runners.active.any?(&block) return true diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 05436cd0f79..eaa5654b9c6 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,7 +20,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index aba37921c09..1ed42c4f3e7 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -21,7 +21,7 @@ class JiraService < IssueTrackerService include HTTParty - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers DEFAULT_API_VERSION = 2 diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 5af24a80609..438ff33fdff 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -22,7 +22,7 @@ class SlackService @issue_url = obj_attr[:url] @action = obj_attr[:action] @state = obj_attr[:state] - @description = obj_attr[:description] + @description = obj_attr[:description] || '' end def attachments diff --git a/app/models/repository.rb b/app/models/repository.rb index c07e8072043..e80c2238402 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -72,7 +72,7 @@ class Repository return @has_visible_content unless @has_visible_content.nil? @has_visible_content = cache.fetch(:has_visible_content?) do - raw_repository.branch_count > 0 + branch_count > 0 end end @@ -173,7 +173,7 @@ class Repository end def branch_names - cache.fetch(:branch_names) { raw_repository.branch_names } + cache.fetch(:branch_names) { branches.map(&:name) } end def tag_names @@ -191,7 +191,7 @@ class Repository end def branch_count - @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + @branch_count ||= cache.fetch(:branch_count) { branches.size } end def tag_count @@ -239,7 +239,7 @@ class Repository def expire_branches_cache cache.expire(:branch_names) - @branches = nil + @local_branches = nil end def expire_cache(branch_name = nil, revision = nil) @@ -335,6 +335,8 @@ class Repository # Runs code just before a repository is deleted. def before_delete + expire_exists_cache + expire_cache if exists? expire_root_ref_cache @@ -612,10 +614,14 @@ class Repository refs_contains_sha('tag', sha) end - def branches - @branches ||= raw_repository.branches + def local_branches + @local_branches ||= rugged.branches.each(:local).map do |branch| + Gitlab::Git::Branch.new(branch.name, branch.target) + end end + alias_method :branches, :local_branches + def tags @tags ||= raw_repository.tags end @@ -818,7 +824,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b9e835a4486..b96e3937281 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?<snippet>\d+) }x end def self.link_reference_pattern - super("snippets", /(?<snippet>\d+)/) + @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/user.rb b/app/models/user.rb index 128ddc2a694..2b0bee2099f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -408,6 +408,8 @@ class User < ActiveRecord::Base end def owns_notification_email + return if self.temp_oauth_email? + self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index a5efb21fab6..82e7090f1ea 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -43,7 +43,7 @@ module Issues def create_new_issue new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, project: @new_project, author: @old_issue.author, - description: unfold_references(@old_issue.description) } + description: rewrite_content(@old_issue.description) } new_params = @old_issue.serializable_hash.merge(new_params) CreateService.new(@new_project, @current_user, new_params).execute @@ -53,7 +53,7 @@ module Issues @old_issue.notes.find_each do |note| new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, - note: unfold_references(new_note.note), + note: rewrite_content(new_note.note), created_at: note.created_at, updated_at: note.updated_at } @@ -61,6 +61,18 @@ module Issues end end + def rewrite_content(content) + return unless content + + rewriters = [Gitlab::Gfm::ReferenceRewriter, + Gitlab::Gfm::UploadsRewriter] + + rewriters.inject(content) do |text, klass| + rewriter = klass.new(text, @old_project, @current_user) + rewriter.rewrite(@new_project) + end + end + def close_issue close_service = CloseService.new(@old_project, @current_user) close_service.execute(@old_issue, notifications: false, system_note: false) @@ -78,20 +90,12 @@ module Issues direction: :to) end - def unfold_references(content) - return unless content - - rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project, - @current_user) - rewriter.rewrite(@new_project) + def mark_as_moved + @old_issue.update(moved_to: @new_issue) end def notify_participants notification_service.issue_moved(@old_issue, @new_issue, @current_user) end - - def mark_as_moved - @old_issue.update(moved_to: @new_issue) - end end end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb new file mode 100644 index 00000000000..315c3e16292 --- /dev/null +++ b/app/services/projects/unlink_fork_service.rb @@ -0,0 +1,19 @@ +module Projects + class UnlinkForkService < BaseService + def execute + return unless @project.forked? + + @project.forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << @project + end + + merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) + + merge_requests.each do |mr| + MergeRequests::CloseService.new(@project, @current_user).execute(mr) + end + + @project.forked_project_link.destroy + end + end +end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index ea2b26ccb52..f0615ec7420 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -95,17 +95,19 @@ class SystemHooksService end def project_member_data(model) + project = model.project || Project.unscoped.find(model.source_id) + { - project_name: model.project.name, - project_path: model.project.path, - project_path_with_namespace: model.project.path_with_namespace, - project_id: model.project.id, - user_username: model.user.username, - user_name: model.user.name, - user_email: model.user.email, - user_id: model.user.id, - access_level: model.human_access, - project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase + project_name: project.name, + project_path: project.path, + project_path_with_namespace: project.path_with_namespace, + project_id: project.id, + user_username: model.user.username, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + access_level: model.human_access, + project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e022a046c48..658b086496f 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -224,7 +224,7 @@ class SystemNoteService # # "Started branch `issue-branch-button-201`" def self.new_issue_branch(issue, project, author, branch) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "Started branch [`#{branch}`](#{link})" diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index f2662922e90..42c5bca90fd 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -123,7 +123,7 @@ class TodoService def handle_note(note, author) # Skip system notes, and notes on project snippet - return if note.system? || note.for_project_snippet? + return if note.system? || note.for_snippet? project = note.project target = note.noteable @@ -170,14 +170,30 @@ class TodoService end def filter_mentioned_users(project, target, author) - mentioned_users = target.mentioned_users.select do |user| - user.can?(:read_project, project) - end - + mentioned_users = target.mentioned_users + mentioned_users = reject_users_without_access(mentioned_users, project, target) mentioned_users.delete(author) mentioned_users.uniq end + def reject_users_without_access(users, project, target) + if target.is_a?(Note) && target.for_issue? + target = target.noteable + end + + if target.is_a?(Issue) + select_users(users, :read_issue, target) + else + select_users(users, :read_project, project) + end + end + + def select_users(users, ability, subject) + users.select do |user| + user.can?(ability.to_sym, subject) + end + end + def pending_todos(user, criteria = {}) valid_keys = [:project_id, :target_id, :target_type, :commit_id] user.todos.pending.where(criteria.slice(*valid_keys)) diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 86d24469e05..1af9e9b0edb 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,14 +1,15 @@ # encoding: utf-8 class FileUploader < CarrierWave::Uploader::Base include UploaderHelper + MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} storage :file attr_accessor :project, :secret - def initialize(project, secret = self.class.generate_secret) + def initialize(project, secret = nil) @project = project - @secret = secret + @secret = secret || self.class.generate_secret end def base_dir @@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def self.generate_secret - SecureRandom.hex - end - def secure_url File.join("/uploads", @secret, file.filename) end + def to_markdown + to_h[:markdown] + end + def to_h filename = image? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") @@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base markdown: markdown } end + + def self.generate_secret + SecureRandom.hex + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0350995d03d..de86dacbb12 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -77,13 +77,6 @@ = f.check_box :gravatar_enabled Gravatar enabled .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :twitter_sharing_enabled do - = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - Twitter enabled - %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter - .form-group = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 = f.number_field :default_projects_limit, class: 'form-control' diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml deleted file mode 100644 index 9c2290bc4a5..00000000000 --- a/app/views/ci/projects/index.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.wiki - %h1 - GitLab CI is now integrated in GitLab UI - %h2 For existing projects - - %p - Check the following pages to find the CI status you're looking for: - - %ul - %li Projects page - shows CI status for each project. - %li Project commits page - show CI status for each commit. - - - - %h2 For new projects - - %p - If you want to enable CI for a new project it is easy as adding - = link_to ".gitlab-ci.yml", "http://doc.gitlab.com/ce/ci/yaml/README.html" - file to your repository diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index e3a4d64df01..aa0aff86d4d 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,4 +1,4 @@ -%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) } +%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' @@ -10,7 +10,10 @@ (removed) %span.todo-label = todo_action_name(todo) - = todo_target_link(todo) + - if todo.target + = todo_target_link(todo) + - else + (removed) · #{time_ago_with_tooltip(todo.created_at)} diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 42c2764e7e2..4d20dd5830e 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,5 +1,5 @@ - if event.visible_to_user?(current_user) - .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} + .event-item{ class: event_row_class(event) } .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 8cf36c711b4..5a2a469ba62 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -7,21 +7,3 @@ = link_to_project event.project - else = event.project_name - -- if !event.project.private? && twitter_sharing_enabled? - .event-body{"data-user-is" => event.author_id} - .event-note - .md - %p - Congratulations! Why not share your accomplishment with the world? - - %a.twitter-share-button{ | - href: "https://twitter.com/share", | - "data-url" => event.project.web_url, | - "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | - "data-size" => "medium", | - "data-related" => "gitlab", | - "data-hashtags" => "gitlab", | - "data-count" => "none"} - Tweet - %script{src: "//platform.twitter.com/widgets.js"} diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml deleted file mode 100644 index 2ed51d87ca1..00000000000 --- a/app/views/layouts/_collapse_button.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if nav_menu_collapsed? - = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close" -- else - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588d..9be36273c7d 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,7 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } = render "layouts/broadcast" + .expand-nav + = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -8,15 +10,19 @@ .gitlab-text-container %h3 GitLab - - if defined?(sidebar) && sidebar - = render "layouts/nav/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' + - primary_sidebar = current_user ? 'dashboard' : 'explore' + + - if defined?(sidebar) && sidebar && sidebar != primary_sidebar + .complex-sidebar + .nav-primary + = render "layouts/nav/#{primary_sidebar}" + .nav-secondary + = render "layouts/nav/#{sidebar}" - else - = render 'layouts/nav/explore' + = render "layouts/nav/#{primary_sidebar}" .collapse-nav - = render partial: 'layouts/collapse_button' + = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar" - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile" do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 54af2c3063c..9d4ab9847a8 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,10 +1,33 @@ -.search - = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" +- if controller.controller_path =~ /^groups/ + - label = 'This group' +- if controller.controller_path =~ /^projects/ + - label = 'This project' + +.search.search-form{class: "#{'has-location-badge' if label.present?}"} + = form_tag search_path, method: :get, class: 'navbar-form' do |f| + .search-input-container + .search-location-badge + - if label.present? + %span.location-badge + %i.location-text + = label + .search-input-wrap + .dropdown{ data: {url: search_autocomplete_path } } + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } + .dropdown-menu.dropdown-select + = dropdown_content do + %ul + %li + %a.is-focused.dropdown-menu-empty-link + Loading... + = dropdown_loading + %i.search-icon + %i.clear-icon.js-clear-input + = hidden_field_tag :group_id, @group.try(:id) - - if @project && @project.persisted? - = hidden_field_tag :project_id, @project.id + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' + - if @project && @project.persisted? - if current_controller?(:issues) = hidden_field_tag :scope, 'issues' - elsif current_controller?(:merge_requests) @@ -21,10 +44,3 @@ = hidden_field_tag :repository_ref, @ref = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } - -:javascript - $('.search-input').on('keyup', function(e) { - if (e.keyCode == 27) { - $('.search-input').blur(); - } - }); diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b93729..22d1d4d8597 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -95,7 +95,7 @@ Spam Logs %span.count= number_with_delimiter(SpamLog.count(:all)) - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = nav_link(controller: :application_settings) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4a0069f18f8..d1a180e4299 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -15,12 +15,12 @@ = icon('dashboard fw') %span Activity - = nav_link(controller: :groups) do + = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups - = nav_link(controller: :milestones) do + = nav_link(path: 'dashboard#milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span @@ -48,7 +48,6 @@ %span Help - %li.separate-item = nav_link(controller: :profile) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 55940741dc0..0b7de9633ec 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -42,7 +34,7 @@ %span Members - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "separate-item" }) do + = nav_link do = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc5..cc119fd64e6 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75e..d0f82b5f57f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,19 +1,4 @@ %ul.nav.nav-sidebar - - if @project.group - = nav_link do - = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to group - - else - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = icon('bookmark fw') @@ -113,7 +98,7 @@ Snippets - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = nav_link(html_options: {class: "#{project_tab_class}"}) do = link_to edit_project_path(@project), title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index de80abd7f4d..3d15c0d932b 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -56,19 +56,20 @@ .prepend-top-default = f.submit 'Update settings', class: "btn btn-create" %hr - %h5 - Groups (#{@group_members.count}) - %div - %ul.bordered-list - - @group_members.each do |group_member| - - notification = Notification.new(group_member) - = render 'settings', type: 'group', membership: group_member, notification: notification - %h5 - Projects (#{@project_members.count}) - %p.account-well - To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. - .append-bottom-default - %ul.bordered-list - - @project_members.each do |project_member| - - notification = Notification.new(project_member) - = render 'settings', type: 'project', membership: project_member, notification: notification +.col-lg-9.col-lg-push-3 + %h5 + Groups (#{@group_members.count}) + %div + %ul.bordered-list + - @group_members.each do |group_member| + - notification = Notification.new(group_member) + = render 'settings', type: 'group', membership: group_member, notification: notification + %h5 + Projects (#{@project_members.count}) + %p.account-well + To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. + .append-bottom-default + %ul.bordered-list + - @project_members.each do |project_member| + - notification = Notification.new(project_member) + = render 'settings', type: 'project', membership: project_member, notification: notification diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index e7c85edff96..1e4c46fca2f 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -3,25 +3,32 @@ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - if can?(current_user, :create_issue, @project) + - can_create_issue = can?(current_user, :create_issue, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - can_create_snippet = can?(current_user, :create_snippet, @project) + + - if can_create_issue %li = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') New merge request - - if can?(current_user, :create_snippet, @project) + + - if can_create_snippet %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') New snippet - - if can?(current_user, :push_code, @project) + - if can_create_issue || merge_project || can_create_snippet %li.divider + + - if can?(current_user, :push_code, @project) %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') @@ -35,13 +42,11 @@ = icon('tags fw') New tag - elsif current_user && current_user.already_forked?(@project) - %li.divider %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') New file - elsif can?(current_user, :fork_project, @project) - %li.divider %li - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 88cbb7c03c5..5fb5fe5af2f 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -12,7 +12,7 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = icon('code-fork fw') Fork - %div.count-with-arrow + = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do %span.arrow %span.count = @project.forks_count diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 8367112a9cb..2731219ccad 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,7 +1,10 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_commit_id = diff_refs.first.id -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +// diff_refs will be nil for orphaned commits (e.g. first commit in repo) +- if diff_refs + - old_commit_id = diff_refs.first.id + - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) + - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 905f6bbbd48..1fe1d98bf13 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -2,7 +2,7 @@ - header_title project_title(@project, "Files", project_files_path(@project)) .file-finder-holder.tree-holder.clearfix - .gray-content-block.top-block + .nav-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'find_file', path: @path %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index ee5b9fd95a8..1dd8f721f7e 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -10,7 +10,7 @@ .merge-request{'data-url' => merge_request_path(@merge_request)} = render "projects/merge_requests/show/mr_title" - .merge-request-details.issuable-details + .merge-request-details.issuable-details{data: {id: @merge_request.project.id}} = render "projects/merge_requests/show/mr_box" .append-bottom-default.mr-source-target.prepend-top-default - if @merge_request.open? diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index b05ab869215..2ec0d20a879 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,15 +1,17 @@ - if @ci_commit .mr-widget-heading - .ci_widget{class: "ci-#{@ci_commit.status}"} - = ci_status_icon(@ci_commit) - %span - Build - = ci_status_label(@ci_commit) - for - = succeed "." do - = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" - %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + - %w[success skipped canceled failed running pending].each do |status| + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" + %span.ci-coverage + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX @@ -43,5 +45,5 @@ :javascript $(function() { - merge_request_widget.getCiStatus(); + merge_request_widget.getCIStatus(false); }); diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a489d4f9b24..2be06aebe6c 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -9,12 +9,17 @@ :javascript var merge_request_widget; - - merge_request_widget = new MergeRequestWidget({ - url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + var opts = { + merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + gitlab_icon: "#{asset_path 'gitlab_logo.png'}", + ci_status: "", + ci_message: "Build {{status}} for \"{{title}}\"", ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.gitlab_merge_status}", - }); + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" + }; + if(typeof merge_request_widget === 'undefined') { + merge_request_widget = new MergeRequestWidget(opts); + } diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 2cf32e6093d..34fe1743f4b 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -5,28 +5,21 @@ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' .timeline-content .note-header + = link_to_member(note.project, note.author, avatar: false) + .inline.note-headline-light + = "#{note.author.to_reference} commented" + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - if note_editable?(note) .note-actions - = link_to '#', title: 'Edit comment', class: 'js-note-edit' do + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil-square-o') - - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'js-note-delete danger' do + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = icon('trash-o') - - - unless note.system - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role.label - = access - - = link_to_member(note.project, note.author, avatar: false) - - %span.author-username - = '@' + note.author.username - - %span.note-last-update - %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'} - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 4f15a99d061..cd8a5f0bd02 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -1,22 +1,20 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + on the diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - %strong on the diff .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 3da2f2060b8..46f2ba4bbcf 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -1,20 +1,22 @@ - note = discussion_notes.first +- commit = note.noteable +- commit_description = commit ? 'commit' : 'a deleted commit' .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion on #{commit_description}" + - if commit + = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-up Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on commit - = link_to(note.noteable.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content - if note.for_diff_line? = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml index 218b0da3977..f8e000b424f 100644 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ b/app/views/projects/notes/discussions/_outdated.html.haml @@ -1,19 +1,18 @@ - note = discussion_notes.first .discussion.js-toggle-container{ class: note.discussion_id } .discussion-header + = link_to_member(@project, note.author, avatar: false) + .inline.discussion-headline-light + = "#{note.author.to_reference} started a discussion" + on the outdated diff .discussion-actions - = link_to "#", class: "js-toggle-button" do + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %i.fa.fa-chevron-down Show/hide discussion - %div - = link_to_member(@project, note.author, avatar: false) - started a discussion on the - %strong outdated diff - %div + .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by = link_to_member(@project, last_note.author, avatar: false) - %span.discussion-last-update - #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} + #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')} .discussion-body.js-toggle-content.hide = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index ba69569b1e7..1c5f8b3928b 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -15,11 +15,11 @@ - if current_user %li - if !on_top_of_branch? - %span.btn.btn-sm.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} + %span.btn.add-to-tree.disabled.has-tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }} = icon('plus') - else %span.dropdown - %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu - if can_edit_tree? diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 5fcba2b7e93..9544e3d3e17 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,24 +1,20 @@ - project = note.project +- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) +- noteable_identifier = note.noteable.try(:iid) || note.noteable.id .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) commented on + = link_to project.name_with_namespace, project + · - if note.for_commit? - = link_to project do - = project.name_with_namespace - · - = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do - Commit #{truncate_sha(note.commit_id)} + = link_to "Commit #{truncate_sha(note.commit_id)}", note_url - else - = link_to project do - = project.name_with_namespace - · - %span #{note.noteable_type.titleize} ##{note.noteable.iid} + %span #{note.noteable_type.titleize} ##{noteable_identifier} · - = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do - = note.noteable.title + = link_to note.noteable.title, note_url .note-search-result .term diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f91ff0e3694..921eaefd79a 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -7,15 +7,15 @@ class: "check_all_issues left" .issues-other-filters .filter-item.inline - - if params[:author_id] + - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", + = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - - if params[:assignee_id] + - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) - = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", + = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter @@ -23,7 +23,6 @@ .filter-item.inline.labels-filter = render "shared/issuable/label_dropdown" - .pull-right = render 'shared/sort_dropdown' @@ -38,11 +37,10 @@ %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", - placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 178223fb463..e2a9e5bfb92 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -14,7 +14,7 @@ - if issuable.is_a?(MergeRequest) %p.help-block .js-wip-explanation - %a.js-toggle-wip{href: ""} + %a.js-toggle-wip{href: "", tabindex: -1} Remove the %code WIP: prefix from the title @@ -22,7 +22,7 @@ %strong Work In Progress merge request to be merged when it's ready. .js-no-wip-explanation - %a.js-toggle-wip{href: ""} + %a.js-toggle-wip{href: "", tabindex: -1} Start the title with %code WIP: to prevent a diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 006a34a11e3..fd5e58c1f1f 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,4 +1,4 @@ -- if params[:label_name] +- if params[:label_name].present? = hidden_field_tag(:label_name, params[:label_name]) .dropdown %button.dropdown-menu-toggle.js-label-select.js-filter-submit{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"}} diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 0434506c8d7..2fcf40ece99 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,7 +1,7 @@ -- if params[:milestone_title] +- if params[:milestone_title].present? = hidden_field_tag(:milestone_title, params[:milestone_title]) -= dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do += dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if @project %ul.dropdown-footer-list - if can? current_user, :admin_milestone, @project diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0e20e86356d..47e544acf52 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -28,6 +28,7 @@ = icon('user') .title.hide-collapsed Assignee + = 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 @@ -39,10 +40,14 @@ %span.username = issuable.assignee.to_reference - else - .light None + %span.assign-yourself + No assignee - + %a.js-assign-yourself{ href: '#' } + assign yourself .selectbox.hide-collapsed - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true) + = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' + = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) .block.milestone .sidebar-collapsed-icon @@ -54,6 +59,7 @@ No .title.hide-collapsed Milestone + = 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 @@ -62,10 +68,10 @@ = issuable.milestone.title - else .light None + .selectbox.hide-collapsed - = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }}) - = hidden_field_tag :issuable_context - = f.submit class: 'btn hide' + = 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.project.labels.any? .block.labels @@ -75,6 +81,7 @@ = issuable.labels.count .title.hide-collapsed Labels + = 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.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } @@ -84,8 +91,31 @@ - else .light None .selectbox.hide-collapsed - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } + - issuable.labels.each do |label| + = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %span.dropdown-toggle-text + Label + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + = dropdown_title("Assign labels") + = dropdown_filter("Search labels") + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels = render "shared/issuable/participants", participants: issuable.participants(current_user) - if current_user @@ -116,5 +146,8 @@ = clipboard_button(clipboard_text: project_ref) :javascript - new Subscription('.subscription'); - new IssuableContext(); + new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); + new LabelsSelect(); + new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); + new Subscription('.subscription') + new Sidebar();
\ No newline at end of file diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 55cb6af232e..ccefd0f71a0 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -5,6 +5,9 @@ class ProjectCacheWorker def perform(project_id) project = Project.find(project_id) + + return unless project.repository.exists? + project.update_repository_size project.update_commit_count diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index d06e4480292..b51c6a266c9 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -5,7 +5,7 @@ class ProjectDestroyWorker def perform(project_id, user_id, params) begin - project = Project.find(project_id) + project = Project.unscoped.find(project_id) rescue ActiveRecord::RecordNotFound return end |