summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorJames Lopez <james@jameslopez.es>2016-04-04 20:03:25 +0200
committerJames Lopez <james@jameslopez.es>2016-04-04 20:03:25 +0200
commit7f7769172e81dc8bfdb037965ec7bf51c95578ec (patch)
tree0a0f8a6f704ddb79edf8cc600a72b4ccf760a4c2 /app
parent4835e680a4624ab8de3316b367b8375bb5a270a0 (diff)
parent531e4bdac8c409a25aa862c644dcab00960c82c4 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee8
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee170
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee25
-rw-r--r--app/assets/javascripts/issues.js.coffee10
-rw-r--r--app/assets/javascripts/labels_select.js.coffee154
-rw-r--r--app/assets/javascripts/lib/animate.js.coffee13
-rw-r--r--app/assets/javascripts/lib/notify.js.coffee30
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee72
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee103
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee55
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee279
-rw-r--r--app/assets/javascripts/sidebar.js.coffee1
-rw-r--r--app/assets/javascripts/todos.js.coffee5
-rw-r--r--app/assets/javascripts/users_select.js.coffee129
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/behaviors.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss68
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss36
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss6
-rw-r--r--app/assets/stylesheets/framework/header.scss32
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss3
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss131
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss30
-rw-r--r--app/assets/stylesheets/pages/awards.scss2
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/commit.scss8
-rw-r--r--app/assets/stylesheets/pages/commits.scss3
-rw-r--r--app/assets/stylesheets/pages/editor.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss21
-rw-r--r--app/assets/stylesheets/pages/lint.scss4
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss135
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss142
-rw-r--r--app/assets/stylesheets/pages/todos.scss8
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/ci/projects_controller.rb10
-rw-r--r--app/controllers/projects/badges_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests_controller.rb25
-rw-r--r--app/controllers/projects/milestones_controller.rb3
-rw-r--r--app/controllers/projects/snippets_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb15
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/events_helper.rb8
-rw-r--r--app/helpers/issuables_helper.rb18
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/search_helper.rb57
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/ability.rb56
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/commit_range.rb4
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/external_issue.rb2
-rw-r--r--app/models/global_milestone.rb1
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/merge_request.rb15
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb16
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/slack_service/issue_message.rb2
-rw-r--r--app/models/repository.rb20
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/issues/move_service.rb28
-rw-r--r--app/services/projects/unlink_fork_service.rb19
-rw-r--r--app/services/system_hooks_service.rb22
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/todo_service.rb26
-rw-r--r--app/uploaders/file_uploader.rb17
-rw-r--r--app/views/admin/application_settings/_form.html.haml7
-rw-r--r--app/views/ci/projects/index.html.haml20
-rw-r--r--app/views/dashboard/todos/_todo.html.haml7
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml18
-rw-r--r--app/views/layouts/_collapse_button.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml18
-rw-r--r--app/views/layouts/_search.html.haml40
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml5
-rw-r--r--app/views/layouts/nav/_group.html.haml10
-rw-r--r--app/views/layouts/nav/_profile.html.haml8
-rw-r--r--app/views/layouts/nav/_project.html.haml17
-rw-r--r--app/views/profiles/notifications/show.html.haml33
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml17
-rw-r--r--app/views/projects/buttons/_fork.html.haml2
-rw-r--r--app/views/projects/diffs/_image.html.haml7
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml24
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml17
-rw-r--r--app/views/projects/notes/_note.html.haml29
-rw-r--r--app/views/projects/notes/discussions/_active.html.haml16
-rw-r--r--app/views/projects/notes/discussions/_commit.html.haml16
-rw-r--r--app/views/projects/notes/discussions/_outdated.html.haml15
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/search/results/_note.html.haml18
-rw-r--r--app/views/shared/issuable/_filter.html.haml14
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml51
-rw-r--r--app/workers/project_cache_worker.rb3
-rw-r--r--app/workers/project_destroy_worker.rb2
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)
&middot; #{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
+ &middot;
- if note.for_commit?
- = link_to project do
- = project.name_with_namespace
- &middot;
- = 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
- &middot;
- %span #{note.noteable_type.titleize} ##{note.noteable.iid}
+ %span #{note.noteable_type.titleize} ##{noteable_identifier}
&middot;
- = 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