Commit 7f776917 authored by James Lopez's avatar James Lopez

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into fix/project-import_url

# Conflicts:
#	db/schema.rb
parents 4835e680 531e4bda
...@@ -132,8 +132,9 @@ linters: ...@@ -132,8 +132,9 @@ linters:
SpaceAroundOperator: SpaceAroundOperator:
enabled: false enabled: false
# Opening braces should be preceded by a single space.
SpaceBeforeBrace: SpaceBeforeBrace:
enabled: false enabled: true
StringQuotes: StringQuotes:
enabled: false enabled: false
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- Improved Markdown rendering performance !3389 (Yorick Peterse)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
- Preserve time notes/comments have been updated at when moving issue - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
- Expose label description in API (Mariusz Jachimowicz)
- Allow back dating on issues when created through the API
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- Add endpoints to archive or unarchive a project !3372
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu)
- Add default scope to projects to exclude projects pending deletion
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
v 8.6.2 (unreleased) - Fix creation of merge requests for orphaned branches (Stan Hu)
- Comments on confidential issues don't show up in activity feed to non-members - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
v 8.6.4
- Don't attempt to fetch any tags from a forked repo (Stan Hu)
v 8.6.3
- Mentions on confidential issues doesn't create todos for non-members. !3374
- Destroy related todos when an Issue/MR is deleted. !3376
- Fix error 500 when target is nil on todo list. !3376
- Fix copying uploads when moving issue to another project. !3382
- Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432
- Fix raw/rendered diff producing different results on merge requests. !3450
- Fix commit comment alignment (Stan Hu). !3466
- Fix Error 500 when searching for a comment in a project snippet. !3468
- Allow temporary email as notification email. !3477
- Fix issue with dropdowns not selecting values. !3478
- Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280
v 8.6.2
- Fix dropdown alignment. !3298
- Fix issuable sidebar overlaps on tablet. !3299
- Make dropdowns pixel perfect. !3337
- Fix order of steps to prevent PostgreSQL errors when running migration. !3355
- Fix bold text in issuable sidebar. !3358
- Fix error with anonymous token in applications settings. !3362
- Fix the milestone 'upcoming' filter. !3364 + !3368
- Fix comments on confidential issues showing up in activity feed to non-members. !3375
- Fix `NoMethodError` when visiting CI root path at `/ci`. !3377
- Add a tooltip to new branch button in issue page. !3380
- Fix an issue hiding the password form when signed-in with a linked account. !3381
- Add links to CI setup documentation from project settings and builds pages. !3384
- Fix an issue with width of project select dropdown. !3386
- Remove redundant `require`s from Banzai files. !3391
- Fix error 500 with cancel button on issuable edit form. !3392 + !3417
- Fix background when editing a highlighted note. !3423
- Remove tabstop from the WIP toggle links. !3426
- Ensure private project snippets are not viewable by unauthorized people.
- Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
- Fixed issue with notification settings not saving. !3452
v 8.6.1 v 8.6.1
- Add option to reload the schema before restoring a database backup. !2807 - Add option to reload the schema before restoring a database backup. !2807
...@@ -83,6 +128,7 @@ v 8.6.0 ...@@ -83,6 +128,7 @@ v 8.6.0
- Add main language of a project in the list of projects (Tiago Botelho) - Add main language of a project in the list of projects (Tiago Botelho)
- Add #upcoming filter to Milestone filter (Tiago Botelho) - Add #upcoming filter to Milestone filter (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages - Add ability to show archived projects on dashboard, explore and group pages
- Remove fork link closes all merge requests opened on source project (Florent Baldino)
- Move group activity to separate page - Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted - Create external users which are excluded of internal and private projects unless access was explicitly granted
- Continue parameters are checked to ensure redirection goes to the same instance - Continue parameters are checked to ensure redirection goes to the same instance
......
...@@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0' ...@@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-scrollto-rails', '~> 1.4.3'
gem 'jquery-ui-rails', '~> 5.0.0' gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2' gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0' gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1' gem 'net-ssh', '~> 3.0.1'
......
...@@ -126,9 +126,9 @@ GEM ...@@ -126,9 +126,9 @@ GEM
coderay (1.1.0) coderay (1.1.0)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
coffee-rails (4.1.0) coffee-rails (4.1.1)
coffee-script (>= 2.2.0) coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
...@@ -652,7 +652,7 @@ GEM ...@@ -652,7 +652,7 @@ GEM
redis-store (~> 1.1.0) redis-store (~> 1.1.0)
redis-store (1.1.7) redis-store (1.1.7)
redis (>= 2.2) redis (>= 2.2)
request_store (1.2.1) request_store (1.3.0)
rerun (0.11.0) rerun (0.11.0)
listen (~> 3.0) listen (~> 3.0)
responders (2.1.1) responders (2.1.1)
...@@ -1011,7 +1011,7 @@ DEPENDENCIES ...@@ -1011,7 +1011,7 @@ DEPENDENCIES
redcarpet (~> 3.3.3) redcarpet (~> 3.3.3)
redis-namespace redis-namespace
redis-rails (~> 4.0.0) redis-rails (~> 4.0.0)
request_store (~> 1.2.0) request_store (~> 1.3.0)
rerun (~> 0.11.0) rerun (~> 0.11.0)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 1.10.1) rouge (~> 1.10.1)
......
...@@ -146,15 +146,11 @@ class Dispatcher ...@@ -146,15 +146,11 @@ class Dispatcher
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one # If we haven't installed a custom shortcut handler, install the default one
if not shortcut_handler if not shortcut_handler
new Shortcuts() new Shortcuts()
initSearch: -> 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
...@@ -3,6 +3,10 @@ class GitLabDropdownFilter ...@@ -3,6 +3,10 @@ class GitLabDropdownFilter
HAS_VALUE_CLASS = "has-value" HAS_VALUE_CLASS = "has-value"
constructor: (@input, @options) -> constructor: (@input, @options) ->
{
@filterInputBlur = true
} = @options
$inputContainer = @input.parent() $inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear') $clearButton = $inputContainer.find('.js-dropdown-input-clear')
...@@ -33,7 +37,7 @@ class GitLabDropdownFilter ...@@ -33,7 +37,7 @@ class GitLabDropdownFilter
blur_field = @shouldBlur e.keyCode blur_field = @shouldBlur e.keyCode
search_text = @input.val() search_text = @input.val()
if blur_field if blur_field and @filterInputBlur
@input.blur() @input.blur()
if @options.remote if @options.remote
...@@ -93,27 +97,48 @@ class GitLabDropdown ...@@ -93,27 +97,48 @@ class GitLabDropdown
PAGE_TWO_CLASS = "is-page-two" PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active" ACTIVE_CLASS = "is-active"
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
constructor: (@el, @options) -> constructor: (@el, @options) ->
self = @
@dropdown = $(@el).parent() @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 []; search_fields = if @options.search then @options.search.fields else [];
if @options.data if @options.data
# Remote data # If data is an array
@remote = new GitLabDropdownRemote @options.data, { if _.isArray @options.data
dataType: @options.dataType, @fullData = @options.data
beforeSend: @toggleLoading.bind(@) @parseData @options.data
success: (data) => else
@fullData = data # 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 if @options.filterable
@input = @dropdown.find('.dropdown-input .dropdown-input-field') @filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur
@filter = new GitLabDropdownFilter @input,
remote: @options.filterRemote remote: @options.filterRemote
query: @options.data query: @options.data
keys: @options.search.fields keys: @options.search.fields
...@@ -123,11 +148,14 @@ class GitLabDropdown ...@@ -123,11 +148,14 @@ class GitLabDropdown
@parseData data @parseData data
@highlightRow 1 @highlightRow 1
enterCallback: => enterCallback: =>
@selectFirstRow() if @enterCallback
@selectFirstRow()
# Event listeners # Event listeners
@dropdown.on "shown.bs.dropdown", @opened @dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden @dropdown.on "hidden.bs.dropdown", @hidden
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
...@@ -143,10 +171,14 @@ class GitLabDropdown ...@@ -143,10 +171,14 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a" selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) -> @dropdown.on "click", selector, (e) ->
self.rowClicked $(@) selected = self.rowClicked $(@)
if self.options.clicked if self.options.clicked
self.options.clicked() self.options.clicked(selected)
# Finds an element inside wrapper element
getElement: (selector) ->
@dropdown.find selector
toggleLoading: -> toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
...@@ -176,15 +208,26 @@ class GitLabDropdown ...@@ -176,15 +208,26 @@ class GitLabDropdown
@appendMenu(full_html) @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: => opened: =>
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
@remote.execute() @remote.execute()
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").focus() @filterInput.focus()
hidden: => @dropdown.trigger('shown.gl.dropdown')
hidden: (e) =>
if @options.filterable if @options.filterable
@dropdown @dropdown
.find(".dropdown-input-field") .find(".dropdown-input-field")
...@@ -195,6 +238,11 @@ class GitLabDropdown ...@@ -195,6 +238,11 @@ class GitLabDropdown
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
if @options.hidden
@options.hidden.call(@,e)
@dropdown.trigger('hidden.gl.dropdown')
# Render the full menu # Render the full menu
renderMenu: (html) -> renderMenu: (html) ->
...@@ -219,20 +267,46 @@ class GitLabDropdown ...@@ -219,20 +267,46 @@ class GitLabDropdown
renderItem: (data) -> renderItem: (data) ->
html = "" html = ""
# Divider
return "<li class='divider'></li>" if data is "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 if @options.renderRow
# Call the render function # Call the render function
html = @options.renderRow(data) html = @options.renderRow(data)
else else
selected = if @options.isSelected then @options.isSelected(data) else false if not selected
url = if @options.url then @options.url(data) else "#" value = if @options.id then @options.id(data) else data.id
text = if @options.text then @options.text(data) else "" 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 = ""; cssClass = "";
if selected if selected
cssClass = "is-active" cssClass = "is-active"
if @highlight
text = @highlightTextMatches(text, @filterInput.val())
html = "<li>" html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>" html += "<a href='#{url}' class='#{cssClass}'>"
html += text html += text
...@@ -241,58 +315,68 @@ class GitLabDropdown ...@@ -241,58 +315,68 @@ class GitLabDropdown
return html 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: -> noResults: ->
html = "<li>" 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 += "No matching results."
html += "</a>" html += "</a>"
html += "</li>" html += "</li>"
highlightRow: (index) -> highlightRow: (index) ->
if @input.val() isnt "" if @filterInput.val() isnt ""
selector = '.dropdown-content li:first-child a' selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a" selector = ".dropdown-page-one .dropdown-content li:first-child a"
$(selector).addClass 'is-focused' @getElement(selector).addClass 'is-focused'
rowClicked: (el) -> rowClicked: (el) ->
fieldName = @options.fieldName 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) if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS)
field.remove() 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? if !value?
field.remove() field.remove()
if @options.multiSelect if not @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove()
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.toggleClass "is-active" el.addClass ACTIVE_CLASS
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value? if value?
if !field.length if !field.length
# Create hidden input for form # 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 @dropdown.before input
else
field.val value
@dropdown.parent().find("input[name='#{fieldName}']").val value return selectedObject
selectFirstRow: -> selectFirstRow: ->
selector = '.dropdown-content li:first-child a' selector = '.dropdown-content li:first-child a'
...@@ -304,4 +388,6 @@ class GitLabDropdown ...@@ -304,4 +388,6 @@ class GitLabDropdown
$.fn.glDropdown = (opts) -> $.fn.glDropdown = (opts) ->
return @.each -> return @.each ->
new GitLabDropdown @, opts if (!$.data @, 'glDropdown')
$.data(@, 'glDropdown', new GitLabDropdown @, opts)
class @IssuableContext class @IssuableContext
constructor: -> constructor: (currentUser) ->
@initParticipants() @initParticipants()
new UsersSelect(currentUser)
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$(".issuable-sidebar .inline-update").on "change", "select", -> $(".issuable-sidebar .inline-update").on "change", "select", ->
...@@ -10,11 +9,21 @@ class @IssuableContext ...@@ -10,11 +9,21 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit() $(this).submit()
$(document).on "click",".edit-link", (e) -> $(document).off("click", ".edit-link").on "click",".edit-link", (e) ->
block = $(@).parents('.block') $block = $(@).parents('.block')
block.find('.selectbox').show() $selectbox = $block.find('.selectbox')
block.find('.value').hide() if $selectbox.is(':visible')
block.find('.js-select2').select2("open") $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() $(".right-sidebar").niceScroll()
......
@Issues = @Issues =
init: -> init: ->
Issues.initSearch() Issues.initSearch()
Issues.initSelects()
Issues.initChecks() Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", -> $("body").on "ajax:success", ".close_issue, .reopen_issue", ->
...@@ -17,18 +16,9 @@ ...@@ -17,18 +16,9 @@
$(this).html totalIssues - 1 $(this).html totalIssues - 1
reload: -> reload: ->
Issues.initSelects()
Issues.initChecks() Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val()) $('#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: -> initChecks: ->
$(".check_all_issues").click -> $(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked) $(".selected_issue").prop("checked", @checked)
......
...@@ -4,14 +4,21 @@ class @LabelsSelect ...@@ -4,14 +4,21 @@ class @LabelsSelect
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
labelUrl = $dropdown.data('labels') labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected') selectedLabel = $dropdown.data('selected')
if selectedLabel if selectedLabel?
selectedLabel = selectedLabel.toString().split(',') selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
showAny = $dropdown.data('show-any') showAny = $dropdown.data('show-any')
defaultLabel = $dropdown.data('default-label') 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 if newLabelField.length
$newLabelCreateButton = $('.js-new-label-btn') $newLabelCreateButton = $('.js-new-label-btn')
...@@ -21,6 +28,22 @@ class @LabelsSelect ...@@ -21,6 +28,22 @@ class @LabelsSelect
# Suggested colors in the dropdown to chose from pre-chosen colors # Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on 'click', (e) -> $('.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.preventDefault()
e.stopPropagation() e.stopPropagation()
newColorField newColorField
...@@ -57,6 +80,23 @@ class @LabelsSelect ...@@ -57,6 +80,23 @@ class @LabelsSelect
# This allows us to enable the button when ready # This allows us to enable the button when ready
enableLabelCreateButton = -> enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt '' 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() $newLabelCreateButton.enable()
else else
$newLabelCreateButton.disable() $newLabelCreateButton.disable()
...@@ -90,41 +130,84 @@ class @LabelsSelect ...@@ -90,41 +130,84 @@ class @LabelsSelect
else else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click' $('.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( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: labelUrl url: labelUrl
).done (data) -> ).done (data) ->
if showNo if $dropdown.hasClass 'js-extra-options'
data.unshift( if showNo
id: 0 data.unshift(
title: 'No Label' id: 0
) title: 'No Label'
)
if showAny if showAny
data.unshift( data.unshift(
isAny: true isAny: true
title: 'Any Label' title: 'Any Label'
) )
if data.length > 2
data.splice 2, 0, 'divider'
if data.length > 2
data.splice 2, 0, 'divider'
callback data callback data
renderRow: (label) -> renderRow: (label) ->
if $.isArray(selectedLabel) selectedClass = ''
selected = '' if $selectbox.find("input[type='hidden']\
$.each selectedLabel, (i, selectedLbl) -> [name='#{$dropdown.data('field-name')}']\
selectedLbl = selectedLbl.trim() [value='#{label.id}']").length
if selected is '' and label.title is selectedLbl selectedClass = 'is-active'
selected = 'is-active'
else
selected = if label.title is selectedLabel then 'is-active' else ''
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li> "<li>
<a href='#' class='#{selected}'> <a href='#' class='#{selectedClass}'>
#{color} #{color}
#{label.title} #{label.title}
</a> </a>
...@@ -133,6 +216,7 @@ class @LabelsSelect ...@@ -133,6 +216,7 @@ class @LabelsSelect
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label' if selected and selected.title isnt 'Any Label'
selected.title selected.title
...@@ -142,15 +226,33 @@ class @LabelsSelect ...@@ -142,15 +226,33 @@ class @LabelsSelect
id: (label) -> id: (label) ->
if label.isAny? if label.isAny?
'' ''
else else if $dropdown.hasClass "js-filter-submit"
label.title 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' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index' isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedLabel = label.title
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else
if $dropdown.hasClass 'js-multiselect'
return
else
saveLabelData()
) )
((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
((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
...@@ -2,13 +2,18 @@ class @MergeRequestWidget ...@@ -2,13 +2,18 @@ class @MergeRequestWidget
# Initialize MergeRequestWidget behavior # Initialize MergeRequestWidget behavior
# #
# check_enable - Boolean, whether to check automerge status # check_enable - Boolean, whether to check automerge status
# url_to_automerge_check - String, URL to use to check automerge status # merge_check_url - String, URL to use to check automerge status
# current_status - String, current automerge status # ci_status_url - String, URL to use to check CI status
# ci_enable - Boolean, whether a CI service is enabled
# url_to_ci_check - String, URL to use to check CI status
# #
constructor: (@opts) -> 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)-> mergeInProgress: (deleteSourceBranch = false)->
$.ajax $.ajax
...@@ -27,18 +32,57 @@ class @MergeRequestWidget ...@@ -27,18 +32,57 @@ class @MergeRequestWidget
dataType: 'json' dataType: 'json'
getMergeStatus: -> getMergeStatus: ->
$.get @opts.url_to_automerge_check, (data) -> $.get @opts.merge_check_url, (data) ->
$('.mr-state-widget').replaceWith(data) $('.mr-state-widget').replaceWith(data)
getCiStatus: -> ciLabelForStatus: (status) ->
if @opts.ci_enable if status == 'success'
$.get @opts.url_to_ci_check, (data) => 'passed'
this.showCiState data.status 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 if data.coverage
this.showCiCoverage data.coverage @showCICoverage data.coverage
, 'json'
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() $('.ci_widget').hide()
allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"]
if state in allowed_states if state in allowed_states
...@@ -52,7 +96,7 @@ class @MergeRequestWidget ...@@ -52,7 +96,7 @@ class @MergeRequestWidget
$('.ci_widget.ci-error').show() $('.ci_widget.ci-error').show()
@setMergeButtonClass('btn-danger') @setMergeButtonClass('btn-danger')
showCiCoverage: (coverage) -> showCICoverage: (coverage) ->
text = 'Coverage ' + coverage + '%' text = 'Coverage ' + coverage + '%'
$('.ci_widget:visible .ci-coverage').text(text) $('.ci_widget:visible .ci-coverage').text(text)
......
class @MilestoneSelect class @MilestoneSelect
constructor: -> constructor: (currentProject) ->
if currentProject?
_this = @
@currentProject = JSON.parse(currentProject)
$('.js-milestone-select').each (i, dropdown) -> $('.js-milestone-select').each (i, dropdown) ->
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
milestonesUrl = $dropdown.data('milestones') milestonesUrl = $dropdown.data('milestones')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedMilestone = $dropdown.data('selected') selectedMilestone = $dropdown.data('selected')
showNo = $dropdown.data('show-no') showNo = $dropdown.data('show-no')
showAny = $dropdown.data('show-any') showAny = $dropdown.data('show-any')
showUpcoming = $dropdown.data('show-upcoming')
useId = $dropdown.data('use-id') useId = $dropdown.data('use-id')
defaultLabel = $dropdown.data('default-label') 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( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: milestonesUrl url: milestonesUrl
).done (data) -> ).done (data) ->
extraOptions = []
if showAny
extraOptions.push(
id: 0
name: ''
title: 'Any Milestone'
)
if showNo if showNo
data.unshift( extraOptions.push(
id: '0' id: -1
name: 'No Milestone'
title: 'No Milestone' title: 'No Milestone'
) )
if showAny if showUpcoming
data.unshift( extraOptions.push(
isAny: true id: -2
title: 'Any Milestone' name: '#upcoming'
title: 'Upcoming'
) )
if data.length > 2 if extraOptions.length > 2
data.splice 2, 0, 'divider' extraOptions.push 'divider'
callback(data) callback(extraOptions.concat(data))
filterable: true filterable: true
search: search:
fields: ['title'] fields: ['title']
...@@ -45,21 +74,51 @@ class @MilestoneSelect ...@@ -45,21 +74,51 @@ class @MilestoneSelect
milestone.title milestone.title
id: (milestone) -> id: (milestone) ->
if !useId if !useId
if !milestone.isAny? milestone.name
milestone.title
else
''
else else
milestone.id milestone.id
isSelected: (milestone) -> isSelected: (milestone) ->
milestone.title is selectedMilestone milestone.name is selectedMilestone
clicked: -> hidden: ->
page = $('body').data 'page' $selectbox.hide()
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) # 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') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else
$dropdown.closest('form').submit() 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')
) )
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
class @SearchAutocomplete class @SearchAutocomplete
constructor: (search_autocomplete_path, project_id, project_ref) ->
project_id = '' unless project_id KEYCODE =
project_ref = '' unless project_ref ESCAPE: 27
query = "?project_id=" + project_id + "&project_ref=" + project_ref BACKSPACE: 8
ENTER: 13
$("#search").autocomplete
source: search_autocomplete_path + query constructor: (opts = {}) ->
minLength: 1 {
select: (event, ui) -> @wrap = $('.search')
location.href = ui.item.url
@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)
...@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' ...@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = -> toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-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: '/' }) $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( -> setTimeout ( ->
......
...@@ -6,10 +6,12 @@ class @Todos ...@@ -6,10 +6,12 @@ class @Todos
clearListeners: -> clearListeners: ->
$('.done-todo').off('click') $('.done-todo').off('click')
$('.js-todos-mark-all').off('click') $('.js-todos-mark-all').off('click')
$('.todo').off('click')
initBtnListeners: -> initBtnListeners: ->
$('.done-todo').on('click', @doneClicked) $('.done-todo').on('click', @doneClicked)
$('.js-todos-mark-all').on('click', @allDoneClicked) $('.js-todos-mark-all').on('click', @allDoneClicked)
$('.todo').on('click', @goToTodoUrl)
doneClicked: (e) => doneClicked: (e) =>
e.preventDefault() e.preventDefault()
...@@ -54,3 +56,6 @@ class @Todos ...@@ -54,3 +56,6 @@ class @Todos
updateBadges: (data) -> updateBadges: (data) ->
$('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count $('.todos-done .badge').text data.done_count
goToTodoUrl: ->
Turbolinks.visit($(this).data('url'))
class @UsersSelect class @UsersSelect
constructor: -> constructor: (currentUser) ->
@usersPath = "/autocomplete/users.json" @usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json" @userPath = "/autocomplete/users/:id.json"
if currentUser?
@currentUser = JSON.parse(currentUser)
$('.js-user-search').each (i, dropdown) => $('.js-user-search').each (i, dropdown) =>
$dropdown = $(dropdown) $dropdown = $(dropdown)
...@@ -12,6 +14,81 @@ class @UsersSelect ...@@ -12,6 +14,81 @@ class @UsersSelect
firstUser = $dropdown.data('first-user') firstUser = $dropdown.data('first-user')
selectedId = $dropdown.data('selected') selectedId = $dropdown.data('selected')
defaultLabel = $dropdown.data('default-label') 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( $dropdown.glDropdown(
data: (term, callback) => data: (term, callback) =>
...@@ -57,20 +134,38 @@ class @UsersSelect ...@@ -57,20 +134,38 @@ class @UsersSelect
fields: ['name', 'username'] fields: ['name', 'username']
selectable: true selectable: true
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected && 'id' of selected if selected && 'id' of selected
selected.name selected.name
else else
defaultLabel 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' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests: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) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedId = user.id
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else
selected = $dropdown
.closest('.selectbox')
.find("input[name='#{$dropdown.data('field-name')}']").val()
assignTo(selected)
renderRow: (user) -> renderRow: (user) ->
username = if user.username then "@#{user.username}" else "" username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false avatar = if user.avatar_url then user.avatar_url else false
...@@ -87,17 +182,25 @@ class @UsersSelect ...@@ -87,17 +182,25 @@ class @UsersSelect
if avatar if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li> # split into three parts so we can remove the username section if nessesary
<a href='#' class='dropdown-menu-user-link #{selected}'> listWithName = "<li>
#{img} <a href='#' class='dropdown-menu-user-link #{selected}'>
<strong class='dropdown-menu-user-full-name'> #{img}
#{user.name} <strong class='dropdown-menu-user-full-name'>
</strong> #{user.name}
<span class='dropdown-menu-user-username'> </strong>"
#{username}
</span> listWithUserName = "<span class='dropdown-menu-user-username'>
</a> #{username}
</li>" </span>"
listClosingTags = "</a>
</li>"
if username is ''
listWithUserName = ''
listWithName + listWithUserName + listClosingTags
) )
$('.ajax-users-select').each (i, select) => $('.ajax-users-select').each (i, select) =>
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
*= require dropzone/basic *= require dropzone/basic
*= require cal-heatmap *= require cal-heatmap
*= require cropper.css *= require cropper.css
*= require animate
*/ */
/* /*
......
...@@ -13,10 +13,10 @@ ...@@ -13,10 +13,10 @@
// Toggle between two states. // Toggle between two states.
.js-toggler-container { .js-toggler-container {
.turn-on { display: block; } .turn-on { display: block; }
.turn-off { display: none; } .turn-off { display: none; }
&.on { &.on {
.turn-on { display: none; } .turn-on { display: none; }
.turn-off { display: block; } .turn-off { display: block; }
} }
} }
...@@ -121,7 +121,7 @@ p.time { ...@@ -121,7 +121,7 @@ p.time {
text-shadow: none; text-shadow: none;
} }
.thin_area{ .thin_area {
height: 150px; height: 150px;
} }
...@@ -148,7 +148,7 @@ li.note { ...@@ -148,7 +148,7 @@ li.note {
} }
} }
.wiki_content code, .readme code{ .wiki_content code, .readme code {
background-color: inherit; background-color: inherit;
} }
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
font-size: 15px; font-size: 15px;
text-align: left; text-align: left;
border: 1px solid $dropdown-toggle-border-color; border: 1px solid $dropdown-toggle-border-color;
border-radius: 2px; border-radius: $dropdown-border-radius;
outline: 0; outline: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
...@@ -75,12 +75,12 @@ ...@@ -75,12 +75,12 @@
width: 240px; width: 240px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0; margin-bottom: 0;
padding: 10px; font-size: 15px;
font-size: 14px;
font-weight: normal; font-weight: normal;
padding: 10px 0;
background-color: $dropdown-bg; background-color: $dropdown-bg;
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base; border-radius: $dropdown-border-radius;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading { &.is-loading {
...@@ -101,9 +101,17 @@ ...@@ -101,9 +101,17 @@
li { li {
text-align: left; text-align: left;
list-style: none; list-style: none;
padding: 0 10px;
} }
.divider { .divider {
height: 1px;
margin: 8px 10px;
padding: 0;
background-color: $dropdown-divider-color;
}
.separator {
width: 100%; width: 100%;
height: 1px; height: 1px;
margin-top: 8px; margin-top: 8px;
...@@ -136,6 +144,21 @@ ...@@ -136,6 +144,21 @@
background-color: $dropdown-empty-row-bg; 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 @@ ...@@ -154,6 +177,10 @@
.dropdown-menu-back { .dropdown-menu-back {
display: block; display: block;
} }
.dropdown-content {
padding: 0 10px;
}
} }
} }
...@@ -167,13 +194,13 @@ ...@@ -167,13 +194,13 @@
} }
.dropdown-menu-user-link { .dropdown-menu-user-link {
padding-top: 7px; padding-top: 10px;
padding-bottom: 7px; padding-bottom: 7px;
} }
.dropdown-menu-user-full-name { .dropdown-menu-user-full-name {
display: block; display: block;
font-weight: 600; font-weight: 500;
line-height: 16px; line-height: 16px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
...@@ -189,7 +216,7 @@ ...@@ -189,7 +216,7 @@
} }
.dropdown-select { .dropdown-select {
width: 300px; width: $dropdown-width;
} }
.dropdown-menu-align-right { .dropdown-menu-align-right {
...@@ -218,20 +245,11 @@ ...@@ -218,20 +245,11 @@
} }
} }
.dropdown-header {
padding-left: 5px;
padding-right: 5px;
color: $dropdown-header-color;
font-size: 13px;
line-height: 22px;
}
.dropdown-title { .dropdown-title {
position: relative; position: relative;
margin-bottom: 10px; padding: 0 0 15px;
padding-left: 30px; margin: 0 10px 10px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
text-align: center; text-align: center;
...@@ -257,21 +275,26 @@ ...@@ -257,21 +275,26 @@
} }
.dropdown-menu-close { .dropdown-menu-close {
right: 0; right: 7px;
width: 20px;
height: 20px;
top: -1px;
} }
.dropdown-menu-back { .dropdown-menu-back {
left: 0; left: 7px;
top: 2px;
} }
.dropdown-input { .dropdown-input {
position: relative; position: relative;
margin-bottom: 10px; margin-bottom: 10px;
padding: 0 10px;
.fa { .fa {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 20px;
color: #c7c7c7; color: #c7c7c7;
font-size: 12px; font-size: 12px;
pointer-events: none; pointer-events: none;
...@@ -281,6 +304,9 @@ ...@@ -281,6 +304,9 @@
display: none; display: none;
cursor: pointer; cursor: pointer;
pointer-events: all; pointer-events: all;
right: 22px;
top: 9px;
font-size: 14px;
} }
&.has-value { &.has-value {
......
...@@ -3,12 +3,10 @@ ...@@ -3,12 +3,10 @@
* *
*/ */
.file-holder { .file-holder {
border: none;
border: 1px solid $border-color; border: 1px solid $border-color;
&.readme-holder { &.readme-holder {
margin-top: 10px; margin: $gl-padding-top 0;
border-bottom: 0;
} }
table { table {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
vertical-align: top; vertical-align: top;
} }
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.issues-filters, .issues-filters,
.issues_bulk_update { .issues_bulk_update {
.dropdown-menu-toggle { .dropdown-menu-toggle {
......
...@@ -6,40 +6,6 @@ input { ...@@ -6,40 +6,6 @@ input {
border-radius: $border-radius-base; 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 { input[type='text'].danger {
background: #f2dede!important; background: #f2dede!important;
border-color: #d66; border-color: #d66;
...@@ -125,7 +91,7 @@ label { ...@@ -125,7 +91,7 @@ label {
} }
.form-control::-webkit-input-placeholder { .form-control::-webkit-input-placeholder {
color: #7f8fa4; color: $gl-placeholder-color;
} }
.input-group { .input-group {
......
...@@ -33,10 +33,15 @@ ...@@ -33,10 +33,15 @@
background: $color; background: $color;
} }
.complex-sidebar .nav-primary {
border-right: 1px solid lighten($color, 3%);
}
.sidebar-wrapper { .sidebar-wrapper {
background: $color-darker; background: $color-darker;
.sidebar-user { .sidebar-user {
border-top: 1px solid lighten($color, 3%);
background: $color-darker; background: $color-darker;
color: $color-light; color: $color-light;
...@@ -62,7 +67,6 @@ ...@@ -62,7 +67,6 @@
.count { .count {
color: $color-light; color: $color-light;
background: $color-dark;
} }
} }
......
...@@ -36,7 +36,7 @@ header { ...@@ -36,7 +36,7 @@ header {
padding: 0; padding: 0;
.nav > li > a { .nav > li > a {
color: #7f8fa4; color: $gl-icon-color;
font-size: 18px; font-size: 18px;
padding: 0; padding: 0;
margin: ($header-height - 28) / 2 0; margin: ($header-height - 28) / 2 0;
...@@ -62,7 +62,7 @@ header { ...@@ -62,7 +62,7 @@ header {
background-color: #eee; background-color: #eee;
} }
&.active { &.active {
color: #7f8fa4; color: $gl-icon-color;
} }
} }
} }
...@@ -81,14 +81,14 @@ header { ...@@ -81,14 +81,14 @@ header {
font-size: 19px; font-size: 19px;
line-height: $header-height; line-height: $header-height;
font-weight: normal; font-weight: normal;
color: #4c4e54; color: $gl-text-color;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
a { a {
color: #4c4e54; color: $gl-text-color;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
...@@ -117,37 +117,17 @@ header { ...@@ -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 { .impersonation i {
color: $red-normal; color: $red-normal;
} }
} }
@mixin collapsed-header { @mixin collapsed-header {
margin-left: $sidebar_collapsed_width; margin-left: 40px;
} }
.header-collapsed { .header-collapsed {
margin-left: $sidebar_collapsed_width; margin-left: 40px;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
@include collapsed-header; @include collapsed-header;
......
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
} }
.page-title { .page-title {
.note_created_ago, .new-issue-link { .note-created-ago, .new-issue-link {
display: none; display: none;
} }
} }
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
display: none; display: none;
} }
aside:not(.right-sidebar){ aside:not(.right-sidebar) {
display: none; display: none;
} }
......
...@@ -100,6 +100,7 @@ ...@@ -100,6 +100,7 @@
> form { > form {
display: inline-block; display: inline-block;
margin-top: -1px;
} }
.icon-label { .icon-label {
...@@ -110,7 +111,7 @@ ...@@ -110,7 +111,7 @@
height: 34px; height: 34px;
display: inline-block; display: inline-block;
position: relative; position: relative;
top: 1px; top: 2px;
margin-right: $gl-padding-top; margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */ /* Medium devices (desktops, 992px and up) */
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
padding: 10px 15px; padding: 10px 15px;
} }
.select2-drop{ .select2-drop {
color: #7f8fa4; color: #7f8fa4;
} }
......
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
} }
a { a {
padding: 7px 15px; padding: 7px 12px;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 24px; line-height: 24px;
color: $gray; color: $gray;
...@@ -169,10 +169,12 @@ ...@@ -169,10 +169,12 @@
} }
.count { .count {
float: right; &:before {
background: #eee; content: '(';
padding: 0 8px; }
@include border-radius(6px); &:after {
content: ')';
}
} }
&.back-link i { &.back-link i {
...@@ -191,6 +193,27 @@ ...@@ -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 { .collapse-nav a {
width: $sidebar_width; width: $sidebar_width;
position: fixed; position: fixed;
...@@ -210,55 +233,12 @@ ...@@ -210,55 +233,12 @@
} }
.page-sidebar-collapsed { .page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_collapsed_width; display: none;
.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;
}
}
} }
} }
.page-sidebar-expanded { .page-sidebar-expanded {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-left: $sidebar_width; padding-left: $sidebar_width;
} }
...@@ -288,6 +268,10 @@ ...@@ -288,6 +268,10 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width; padding-right: $sidebar_collapsed_width;
} }
.sidebar-collapsed-icon {
cursor: pointer;
}
} }
.right-sidebar-expanded { .right-sidebar-expanded {
...@@ -300,4 +284,53 @@ ...@@ -300,4 +284,53 @@
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; 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;
}
}
}
}
}
} }
...@@ -56,8 +56,8 @@ $component-active-bg: $brand-info; ...@@ -56,8 +56,8 @@ $component-active-bg: $brand-info;
//## //##
$input-color: $text-color; $input-color: $text-color;
$input-border: #e7e9ed; $input-border: $border-color;
$input-border-focus: #7f8fa4; $input-border-focus: $focus-border-color;
$legend-color: $text-color; $legend-color: $text-color;
......
...@@ -11,6 +11,7 @@ $gutter_inner_width: 258px; ...@@ -11,6 +11,7 @@ $gutter_inner_width: 258px;
* UI elements * UI elements
*/ */
$border-color: #efeff1; $border-color: #efeff1;
$focus-border-color: #3aabf0;
$table-border-color: #eef0f2; $table-border-color: #eef0f2;
$background-color: #faf9f9; $background-color: #faf9f9;
...@@ -26,6 +27,7 @@ $gl-text-orange: #d90; ...@@ -26,6 +27,7 @@ $gl-text-orange: #d90;
$gl-link-color: #3084bb; $gl-link-color: #3084bb;
$gl-dark-link-color: #333; $gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f; $gl-placeholder-color: #8f8f8f;
$gl-icon-color: $gl-placeholder-color;
$gl-gray: $gl-text-color; $gl-gray: $gl-text-color;
$gl-header-color: $gl-title-color; $gl-header-color: $gl-title-color;
...@@ -66,7 +68,7 @@ $header-height: 58px; ...@@ -66,7 +68,7 @@ $header-height: 58px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$error-exclamation-point: #e62958; $error-exclamation-point: #e62958;
$border-radius-default: 3px; $border-radius-default: 2px;
$btn-transparent-color: #8f8f8f; $btn-transparent-color: #8f8f8f;
$ssh-key-icon-color: #8f8f8f; $ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px; $ssh-key-icon-size: 18px;
...@@ -166,6 +168,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif ...@@ -166,6 +168,8 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
/* /*
* Dropdowns * Dropdowns
*/ */
$dropdown-border-radius: 2px;
$dropdown-width: 300px;
$dropdown-bg: #fff; $dropdown-bg: #fff;
$dropdown-link-color: #555; $dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover; $dropdown-link-hover-bg: $row-hover;
...@@ -176,8 +180,8 @@ $dropdown-divider-color: rgba(#000, .1); ...@@ -176,8 +180,8 @@ $dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494; $dropdown-header-color: #959494;
$dropdown-title-btn-color: #bfbfbf; $dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555; $dropdown-input-color: #555;
$dropdown-input-focus-border: rgb(58, 171, 240); $dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba(#000, .2); $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6); $dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff; $dropdown-toggle-bg: #fff;
...@@ -193,3 +197,23 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; ...@@ -193,3 +197,23 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
$award-emoji-menu-bg: #fff; $award-emoji-menu-bg: #fff;
$award-emoji-menu-border: #f1f2f4; $award-emoji-menu-border: #f1f2f4;
$award-emoji-new-btn-icon-color: #dcdcdc; $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;
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
height: 300px; height: 300px;
overflow-y: scroll; overflow-y: scroll;
input.emoji-search{ input.emoji-search {
background-image: url("data:image/png;base64,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"); background-image: url("data:image/png;base64,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");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 5px center; background-position: right 5px center;
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
} }
} }
.loading{ .loading {
font-size: 20px; font-size: 20px;
} }
......
.commit-title{ .commit-title {
display: block; display: block;
} }
.commit-author, .commit-committer{ .commit-author, .commit-committer {
display: block; display: block;
color: #999; color: #999;
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
.commit-author strong, .commit-committer strong{ .commit-author strong, .commit-committer strong {
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
color: $gl-text-red; color: $gl-text-red;
} }
} }
.edit-file{ .edit-file {
a { a {
color: $gl-text-color; color: $gl-text-color;
} }
......
.commits-compare-switch{ .commits-compare-switch {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
background: image-url("switch_icon.png") no-repeat center center; background: image-url("switch_icon.png") no-repeat center center;
...@@ -93,7 +93,6 @@ li.commit { ...@@ -93,7 +93,6 @@ li.commit {
.commit-row-info { .commit-row-info {
color: $gl-gray; color: $gl-gray;
line-height: 24px; line-height: 24px;
font-size: 13px;
a { a {
color: $gl-gray; color: $gl-gray;
......
.file-editor { .file-editor {
#editor{ #editor {
border: none; border: none;
@include border-radius(0); @include border-radius(0);
height: 500px; height: 500px;
......
...@@ -43,10 +43,6 @@ ...@@ -43,10 +43,6 @@
.md { .md {
color: #7f8fa4; color: #7f8fa4;
font-size: $gl-font-size; font-size: $gl-font-size;
iframe.twitter-share-button {
vertical-align: bottom;
}
} }
pre { pre {
......
...@@ -30,6 +30,10 @@ ...@@ -30,6 +30,10 @@
} }
.issuable-sidebar { .issuable-sidebar {
a {
color: inherit;
}
.block { .block {
@include clearfix; @include clearfix;
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -89,7 +93,7 @@ ...@@ -89,7 +93,7 @@
} }
.cross-project-reference { .cross-project-reference {
color: $gl-link-color; color: inherit;
span { span {
white-space: nowrap; white-space: nowrap;
...@@ -133,6 +137,12 @@ ...@@ -133,6 +137,12 @@
.value { .value {
line-height: 1; line-height: 1;
.assign-yourself {
margin-top: 10px;
font-weight: normal;
display: block;
}
} }
.bold { .bold {
...@@ -252,6 +262,15 @@ ...@@ -252,6 +262,15 @@
text-decoration: none; text-decoration: none;
} }
} }
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
}
.open .dropdown-menu {
width: 100%;
}
} }
.btn-default.gutter-toggle { .btn-default.gutter-toggle {
......
.ci-body { .ci-body {
.incorrect-syntax{ .incorrect-syntax {
font-size: 19px; font-size: 19px;
color: red; color: red;
} }
.correct-syntax{ .correct-syntax {
font-size: 19px; font-size: 19px;
color: #47a447; color: #47a447;
} }
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
} }
} }
.login-box{ .login-box {
background: #fafafa; background: #fafafa;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 2px #ccc; box-shadow: 0 0 2px #ccc;
......
...@@ -230,3 +230,9 @@ ...@@ -230,3 +230,9 @@
} }
} }
} }
.builds {
.table-holder {
overflow-x: scroll;
}
}
...@@ -71,8 +71,6 @@ ...@@ -71,8 +71,6 @@
} }
.note-form-actions { .note-form-actions {
background: #fff;
.note-form-option { .note-form-option {
margin-top: 8px; margin-top: 8px;
margin-left: 30px; margin-left: 30px;
......
...@@ -22,7 +22,7 @@ ul.notes { ...@@ -22,7 +22,7 @@ ul.notes {
margin-left: 55px; margin-left: 55px;
} }
.note_created_ago, .note-updated-at { .note-created-ago, .note-updated-at {
white-space: nowrap; white-space: nowrap;
} }
...@@ -39,53 +39,6 @@ ul.notes { ...@@ -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 { .discussion-body {
padding-top: 15px; padding-top: 15px;
} }
...@@ -198,40 +151,88 @@ ul.notes { ...@@ -198,40 +151,88 @@ ul.notes {
border-width: 1px 0; border-width: 1px 0;
padding-top: 0; padding-top: 0;
vertical-align: top; vertical-align: top;
&.parallel{ &.parallel {
border-width: 1px; 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 * Actions for Discussions/Notes
*/ */
.discussion, .discussion-actions,
.note { .note-actions {
.discussion-actions, float: right;
.note-actions { margin-left: 10px;
float: right; color: $notes-action-color;
margin-left: 10px; }
a { .note-action-button,
margin-left: 5px; .discussion-action-button {
color: $gl-gray; display: inline-block;
margin-left: 10px;
line-height: 24px;
i.fa { .fa {
font-size: 16px; position: relative;
line-height: 16px; top: 1px;
} font-size: 17px;
}
&:hover { .fa-trash-o {
@extend .cgray; top: 0;
&.danger { @extend .cred; } 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 { .diff-file .note .note-actions {
right: 0; right: 0;
top: 0; top: 0;
......
...@@ -162,7 +162,7 @@ ...@@ -162,7 +162,7 @@
margin-right: 12px; margin-right: 12px;
a { a {
margin: -1px !important; margin: -1px;
} }
} }
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
padding: 0; padding: 0;
background: transparent; background: transparent;
border: none; border: none;
line-height: 42px; line-height: 36px;
margin: 0; margin: 0;
> li + li:before { > li + li:before {
......
...@@ -21,3 +21,145 @@ ...@@ -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%;
}
}
}
...@@ -6,13 +6,19 @@ ...@@ -6,13 +6,19 @@
.navbar-nav { .navbar-nav {
li { li {
.badge.todos-pending-count { .badge.todos-pending-count {
background-color: #7f8fa4; background-color: $gl-icon-color;
margin-top: -5px; margin-top: -5px;
font-weight: normal; font-weight: normal;
} }
} }
} }
.todo {
&:hover {
cursor: pointer;
}
}
.todo-item { .todo-item {
.todo-title { .todo-title {
@include str-truncated(calc(100% - 174px)); @include str-truncated(calc(100% - 174px));
......
...@@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:require_two_factor_authentication, :require_two_factor_authentication,
:two_factor_grace_period, :two_factor_grace_period,
:gravatar_enabled, :gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text, :sign_in_text,
:help_page_text, :help_page_text,
:home_page_url, :home_page_url,
......
module Ci module Ci
class ProjectsController < Ci::ApplicationController class ProjectsController < Ci::ApplicationController
before_action :project before_action :project
before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge] before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index]
skip_before_action :authenticate_user!, only: [:badge] skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery protect_from_forgery
def index
redirect_to root_path
end
def show def show
# Temporary compatibility with CI badges pointing to CI project page # Temporary compatibility with CI badges pointing to CI project page
redirect_to namespace_project_path(project.namespace, project) redirect_to namespace_project_path(project.namespace, project)
...@@ -35,5 +39,9 @@ module Ci ...@@ -35,5 +39,9 @@ module Ci
response.headers["Pragma"] = "no-cache" response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
end end
def authorize_read_project!
return access_denied! unless can?(current_user, :read_project, project)
end
end end
end end
...@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController ...@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers before_action :no_cache_headers
def build def build
badge = Gitlab::Badge::Build.new(project, params[:ref])
respond_to do |format| respond_to do |format|
format.html { render_404 } format.html { render_404 }
format.svg do format.svg do
image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) send_data(badge.data, type: badge.type, disposition: 'inline')
send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml')
end end
end end
end end
......
...@@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -68,7 +68,13 @@ class Projects::IssuesController < Projects::ApplicationController
@merge_requests = @issue.referenced_merge_requests(current_user) @merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) @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 end
def create def create
...@@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -107,10 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
format.json do format.json do
render json: { render json: @issue.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
saved: @issue.valid?,
assignee_avatar_url: @issue.assignee.try(:avatar_url)
}
end end
end end
end end
......
...@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: @merge_request } format.json { render json: @merge_request }
format.diff { render text: @merge_request.to_diff(current_user) } format.diff { render text: @merge_request.to_diff }
format.patch { render text: @merge_request.to_patch(current_user) } format.patch { render text: @merge_request.to_patch }
end end
end end
...@@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -154,10 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request]) @merge_request.target_project, @merge_request])
end end
format.json do format.json do
render json: { render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
saved: @merge_request.valid?,
assignee_avatar_url: @merge_request.assignee.try(:avatar_url)
}
end end
end end
else else
...@@ -227,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -227,14 +224,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def ci_status def ci_status
ci_service = @merge_request.source_project.ci_service ci_commit = @merge_request.ci_commit
status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) 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) if ci_service.respond_to?(:commit_coverage)
coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
end
end end
response = { response = {
title: merge_request.title,
sha: merge_request.last_commit_short_sha,
status: status, status: status,
coverage: coverage coverage: coverage
} }
......
...@@ -19,13 +19,12 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -19,13 +19,12 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
respond_to do |format| respond_to do |format|
format.html do format.html do
@milestones = @milestones.page(params[:page]) @milestones = @milestones.page(params[:page])
end end
format.json do format.json do
render json: @milestones render json: @milestones.to_json(methods: :name)
end end
end end
end end
......
...@@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -3,7 +3,7 @@ class Projects::SnippetsController < Projects::ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read any snippet # Allow read any snippet
before_action :authorize_read_project_snippet! before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
# Allow write(create) snippet # Allow write(create) snippet
before_action :authorize_create_project_snippet!, only: [:new, :create] before_action :authorize_create_project_snippet!, only: [:new, :create]
...@@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -81,6 +81,10 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id]) @snippet ||= @project.snippets.find(params[:id])
end end
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
end
def authorize_update_project_snippet! def authorize_update_project_snippet!
return render_404 unless can?(current_user, :update_project_snippet, @snippet) return render_404 unless can?(current_user, :update_project_snippet, @snippet)
end end
......
...@@ -71,7 +71,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -71,7 +71,7 @@ class ProjectsController < Projects::ApplicationController
def remove_fork def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project) 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.' flash[:notice] = 'The fork relationship has been removed.'
end end
end end
...@@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
emojis: autocomplete_emojis, emojis: AwardEmoji.urls,
issues: autocomplete.issues, issues: autocomplete.issues,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
members: participants members: participants
...@@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController
) )
end 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? def repo_exists?
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
......
...@@ -243,7 +243,7 @@ class IssuableFinder ...@@ -243,7 +243,7 @@ class IssuableFinder
end end
def filter_by_upcoming_milestone? def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming' params[:milestone_title] == Milestone::Upcoming.name
end end
def by_milestone(items) def by_milestone(items)
...@@ -252,7 +252,7 @@ class IssuableFinder ...@@ -252,7 +252,7 @@ class IssuableFinder
items = items.where(milestone_id: [-1, nil]) items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone? elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming 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 else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......
...@@ -3,10 +3,6 @@ module ApplicationSettingsHelper ...@@ -3,10 +3,6 @@ module ApplicationSettingsHelper
current_application_settings.gravatar_enabled? current_application_settings.gravatar_enabled?
end end
def twitter_sharing_enabled?
current_application_settings.twitter_sharing_enabled?
end
def signup_enabled? def signup_enabled?
current_application_settings.signup_enabled? current_application_settings.signup_enabled?
end end
......
...@@ -60,7 +60,7 @@ module DropdownsHelper ...@@ -60,7 +60,7 @@ module DropdownsHelper
title_output << content_tag(:span, title) title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do 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 end
title_output.html_safe title_output.html_safe
......
...@@ -214,4 +214,12 @@ module EventsHelper ...@@ -214,4 +214,12 @@ module EventsHelper
end end
end end
end end
def event_row_class(event)
if event.body?
"event-block"
else
"event-inline"
end
end
end end
...@@ -16,6 +16,16 @@ module IssuablesHelper ...@@ -16,6 +16,16 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end 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) def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end end
...@@ -37,6 +47,14 @@ module IssuablesHelper ...@@ -37,6 +47,14 @@ module IssuablesHelper
end end
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 private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -5,8 +5,10 @@ module NotesHelper ...@@ -5,8 +5,10 @@ module NotesHelper
end end
def note_target_fields(note) def note_target_fields(note)
hidden_field_tag(:target_type, note.noteable.class.name.underscore) + if note.noteable
hidden_field_tag(:target_id, note.noteable.id) hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
hidden_field_tag(:target_id, note.noteable.id)
end
end end
def note_editable?(note) def note_editable?(note)
......
module SearchHelper module SearchHelper
def search_autocomplete_opts(term) def search_autocomplete_opts(term)
return unless current_user return unless current_user
...@@ -23,45 +24,44 @@ module SearchHelper ...@@ -23,45 +24,44 @@ module SearchHelper
# Autocomplete results for various settings pages # Autocomplete results for various settings pages
def default_autocomplete def default_autocomplete
[ [
{ label: "Profile settings", url: profile_path }, { category: "Settings", label: "Profile settings", url: profile_path },
{ label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ label: "Dashboard", url: root_path }, { category: "Settings", label: "Dashboard", url: root_path },
{ label: "Admin Section", url: admin_root_path }, { category: "Settings", label: "Admin Section", url: admin_root_path },
] ]
end end
# Autocomplete results for internal help pages # Autocomplete results for internal help pages
def help_autocomplete def help_autocomplete
[ [
{ label: "help: API Help", url: help_page_path("api", "README") }, { category: "Help", label: "API Help", url: help_page_path("api", "README") },
{ label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
{ label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
{ label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") }, { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
] ]
end end
# Autocomplete results for the current project, if it's defined # Autocomplete results for the current project, if it's defined
def project_autocomplete def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref if @project && @project.repository.exists? && @project.repository.root_ref
prefix = search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref ref = @ref || @project.repository.root_ref
[ [
{ label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
{ label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) },
{ label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
{ label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, { category: "Current Project", label: "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: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
] ]
else else
[] []
...@@ -72,7 +72,9 @@ module SearchHelper ...@@ -72,7 +72,9 @@ module SearchHelper
def groups_autocomplete(term, limit = 5) def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group| 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) url: group_path(group)
} }
end end
...@@ -83,7 +85,10 @@ module SearchHelper ...@@ -83,7 +85,10 @@ module SearchHelper
current_user.authorized_projects.search_by_title(term). current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p| 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) url: namespace_project_path(p.namespace, p)
} }
end end
......
...@@ -110,6 +110,10 @@ class Notify < BaseMailer ...@@ -110,6 +110,10 @@ class Notify < BaseMailer
headers['Reply-To'] = address 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 @reply_by_email = true
end end
......
...@@ -27,6 +27,8 @@ class Ability ...@@ -27,6 +27,8 @@ class Ability
case true case true
when subject.is_a?(PersonalSnippet) when subject.is_a?(PersonalSnippet)
anonymous_personal_snippet_abilities(subject) anonymous_personal_snippet_abilities(subject)
when subject.is_a?(ProjectSnippet)
anonymous_project_snippet_abilities(subject)
when subject.is_a?(CommitStatus) when subject.is_a?(CommitStatus)
anonymous_commit_status_abilities(subject) anonymous_commit_status_abilities(subject)
when subject.is_a?(Project) || subject.respond_to?(:project) when subject.is_a?(Project) || subject.respond_to?(:project)
...@@ -100,6 +102,14 @@ class Ability ...@@ -100,6 +102,14 @@ class Ability
end end
end end
def anonymous_project_snippet_abilities(snippet)
if snippet.public?
[:read_project_snippet]
else
[]
end
end
def global_abilities(user) def global_abilities(user)
rules = [] rules = []
rules << :create_group if user.can_create_group rules << :create_group if user.can_create_group
...@@ -338,24 +348,22 @@ class Ability ...@@ -338,24 +348,22 @@ class Ability
end end
end end
[:note, :project_snippet].each do |name| def note_abilities(user, note)
define_method "#{name}_abilities" do |user, subject| rules = []
rules = []
if subject.author == user
rules += [
:"read_#{name}",
:"update_#{name}",
:"admin_#{name}"
]
end
if subject.respond_to?(:project) && subject.project if note.author == user
rules += project_abilities(user, subject.project) rules += [
end :read_note,
:update_note,
:admin_note
]
end
rules if note.respond_to?(:project) && note.project
rules += project_abilities(user, note.project)
end end
rules
end end
def personal_snippet_abilities(user, snippet) def personal_snippet_abilities(user, snippet)
...@@ -376,6 +384,24 @@ class Ability ...@@ -376,6 +384,24 @@ class Ability
rules rules
end 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) def group_member_abilities(user, subject)
rules = [] rules = []
target_user = subject.user target_user = subject.user
......
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
# updated_at :datetime # updated_at :datetime
# home_page_url :string(255) # home_page_url :string(255)
# default_branch_protection :integer default(2) # default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text # restricted_visibility_levels :text
# version_check_enabled :boolean default(TRUE) # version_check_enabled :boolean default(TRUE)
# max_attachment_size :integer default(10), not null # max_attachment_size :integer default(10), not null
...@@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'], signin_enabled: Settings.gitlab['signin_enabled'],
twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'], sign_in_text: Settings.extra['sign_in_text'],
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
......
...@@ -74,14 +74,14 @@ class Commit ...@@ -74,14 +74,14 @@ class Commit
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})? (?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit>\h{7,40}) (?<commit>\h{7,40})
}x }x
end end
def self.link_reference_pattern def self.link_reference_pattern
super("commit", /(?<commit>\h{7,40})/) @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
end end
def to_reference(from_project = nil) def to_reference(from_project = nil)
......
...@@ -43,14 +43,14 @@ class CommitRange ...@@ -43,14 +43,14 @@ class CommitRange
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ @reference_pattern ||= %r{
(?:#{Project.reference_pattern}#{reference_prefix})? (?:#{Project.reference_pattern}#{reference_prefix})?
(?<commit_range>#{STRICT_PATTERN}) (?<commit_range>#{STRICT_PATTERN})
}x }x
end end
def self.link_reference_pattern def self.link_reference_pattern
super("compare", /(?<commit_range>#{PATTERN})/) @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/)
end end
# Initialize a CommitRange # Initialize a CommitRange
......
...@@ -19,6 +19,7 @@ module Issuable ...@@ -19,6 +19,7 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
...@@ -41,7 +42,7 @@ module Issuable ...@@ -41,7 +42,7 @@ module Issuable
scope :join_project, -> { joins(:project) } scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(: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, delegate :name,
:email, :email,
......
...@@ -31,7 +31,7 @@ class ExternalIssue ...@@ -31,7 +31,7 @@ class ExternalIssue
# Pattern used to extract `JIRA-123` issue references from text # Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern 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 end
def to_reference(_from_project = nil) def to_reference(_from_project = nil)
......
...@@ -14,6 +14,7 @@ class GlobalMilestone ...@@ -14,6 +14,7 @@ class GlobalMilestone
def initialize(title, milestones) def initialize(title, milestones)
@title = title @title = title
@name = title
@milestones = milestones @milestones = milestones
end end
......
...@@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base ...@@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ @reference_pattern ||= %r{
(#{Project.reference_pattern})? (#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+) #{Regexp.escape(reference_prefix)}(?<issue>\d+)
}x }x
end end
def self.link_reference_pattern def self.link_reference_pattern
super("issues", /(?<issue>\d+)/) @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end end
def to_reference(from_project = nil) def to_reference(from_project = nil)
......
...@@ -56,7 +56,7 @@ class Label < ActiveRecord::Base ...@@ -56,7 +56,7 @@ class Label < ActiveRecord::Base
# This pattern supports cross-project references. # This pattern supports cross-project references.
# #
def self.reference_pattern def self.reference_pattern
%r{ @reference_pattern ||= %r{
(#{Project.reference_pattern})? (#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)} #{Regexp.escape(reference_prefix)}
(?: (?:
......
...@@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -135,6 +135,7 @@ class MergeRequest < ActiveRecord::Base
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) } 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 :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) }
...@@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -149,14 +150,14 @@ class MergeRequest < ActiveRecord::Base
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ @reference_pattern ||= %r{
(#{Project.reference_pattern})? (#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+) #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)
}x }x
end end
def self.link_reference_pattern def self.link_reference_pattern
super("merge_requests", /(?<merge_request>\d+)/) @link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end end
# Returns all the merge requests from an ActiveRecord:Relation. # Returns all the merge requests from an ActiveRecord:Relation.
...@@ -279,7 +280,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -279,7 +280,7 @@ class MergeRequest < ActiveRecord::Base
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def work_in_progress? def work_in_progress?
title =~ WIP_REGEX !!(title =~ WIP_REGEX)
end end
def wipless_title def wipless_title
...@@ -331,15 +332,15 @@ class MergeRequest < ActiveRecord::Base ...@@ -331,15 +332,15 @@ class MergeRequest < ActiveRecord::Base
# Returns the raw diff for this merge request # Returns the raw diff for this merge request
# #
# see "git diff" # see "git diff"
def to_diff(current_user) def to_diff
target_project.repository.diff_text(target_branch, source_sha) target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end end
# Returns the commit as a series of email patches. # Returns the commit as a series of email patches.
# #
# see "git format-patch" # see "git format-patch"
def to_patch(current_user) def to_patch
target_project.repository.format_patch(target_branch, source_sha) target_project.repository.format_patch(diff_base_commit.sha, source_sha)
end end
def hook_attrs def hook_attrs
......
...@@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base ...@@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base
end end
def self.link_reference_pattern def self.link_reference_pattern
super("milestones", /(?<milestone>\d+)/) @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/)
end end
def self.upcoming def self.upcoming
...@@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base ...@@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base
def to_reference(from_project = nil) def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]") 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) url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
"[#{escaped_title}](#{url})" "[#{escaped_title}](#{url})"
......
...@@ -311,7 +311,7 @@ class Note < ActiveRecord::Base ...@@ -311,7 +311,7 @@ class Note < ActiveRecord::Base
for_merge_request? && for_diff_line? for_merge_request? && for_diff_line?
end end
def for_project_snippet? def for_snippet?
noteable_type == "Snippet" noteable_type == "Snippet"
end end
......
...@@ -206,6 +206,8 @@ class Project < ActiveRecord::Base ...@@ -206,6 +206,8 @@ class Project < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader mount_uploader :avatar, AvatarUploader
# Scopes # Scopes
default_scope { where(pending_delete: false) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
...@@ -491,7 +493,7 @@ class Project < ActiveRecord::Base ...@@ -491,7 +493,7 @@ class Project < ActiveRecord::Base
end end
def web_url 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 end
def web_url_without_protocol def web_url_without_protocol
...@@ -612,7 +614,7 @@ class Project < ActiveRecord::Base ...@@ -612,7 +614,7 @@ class Project < ActiveRecord::Base
if avatar.present? if avatar.present?
[gitlab_config.url, avatar.url].join [gitlab_config.url, avatar.url].join
elsif avatar_in_git 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
end end
...@@ -951,16 +953,6 @@ class Project < ActiveRecord::Base ...@@ -951,16 +953,6 @@ class Project < ActiveRecord::Base
self.builds_enabled = true self.builds_enabled = true
end 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) def any_runners?(&block)
if runners.active.any?(&block) if runners.active.any?(&block)
return true return true
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
# #
class GitlabIssueTrackerService < IssueTrackerService 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 prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
include HTTParty include HTTParty
include Gitlab::Application.routes.url_helpers include Gitlab::Routing.url_helpers
DEFAULT_API_VERSION = 2 DEFAULT_API_VERSION = 2
......
...@@ -22,7 +22,7 @@ class SlackService ...@@ -22,7 +22,7 @@ class SlackService
@issue_url = obj_attr[:url] @issue_url = obj_attr[:url]
@action = obj_attr[:action] @action = obj_attr[:action]
@state = obj_attr[:state] @state = obj_attr[:state]
@description = obj_attr[:description] @description = obj_attr[:description] || ''
end end
def attachments def attachments
......
...@@ -72,7 +72,7 @@ class Repository ...@@ -72,7 +72,7 @@ class Repository
return @has_visible_content unless @has_visible_content.nil? return @has_visible_content unless @has_visible_content.nil?
@has_visible_content = cache.fetch(:has_visible_content?) do @has_visible_content = cache.fetch(:has_visible_content?) do
raw_repository.branch_count > 0 branch_count > 0
end end
end end
...@@ -173,7 +173,7 @@ class Repository ...@@ -173,7 +173,7 @@ class Repository
end end
def branch_names def branch_names
cache.fetch(:branch_names) { raw_repository.branch_names } cache.fetch(:branch_names) { branches.map(&:name) }
end end
def tag_names def tag_names
...@@ -191,7 +191,7 @@ class Repository ...@@ -191,7 +191,7 @@ class Repository
end end
def branch_count def branch_count
@branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } @branch_count ||= cache.fetch(:branch_count) { branches.size }
end end
def tag_count def tag_count
...@@ -239,7 +239,7 @@ class Repository ...@@ -239,7 +239,7 @@ class Repository
def expire_branches_cache def expire_branches_cache
cache.expire(:branch_names) cache.expire(:branch_names)
@branches = nil @local_branches = nil
end end
def expire_cache(branch_name = nil, revision = nil) def expire_cache(branch_name = nil, revision = nil)
...@@ -335,6 +335,8 @@ class Repository ...@@ -335,6 +335,8 @@ class Repository
# Runs code just before a repository is deleted. # Runs code just before a repository is deleted.
def before_delete def before_delete
expire_exists_cache
expire_cache if exists? expire_cache if exists?
expire_root_ref_cache expire_root_ref_cache
...@@ -612,10 +614,14 @@ class Repository ...@@ -612,10 +614,14 @@ class Repository
refs_contains_sha('tag', sha) refs_contains_sha('tag', sha)
end end
def branches def local_branches
@branches ||= raw_repository.branches @local_branches ||= rugged.branches.each(:local).map do |branch|
Gitlab::Git::Branch.new(branch.name, branch.target)
end
end end
alias_method :branches, :local_branches
def tags def tags
@tags ||= raw_repository.tags @tags ||= raw_repository.tags
end end
...@@ -818,7 +824,7 @@ class Repository ...@@ -818,7 +824,7 @@ class Repository
end end
def fetch_ref(source_path, source_ref, target_ref) 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) Gitlab::Popen.popen(args, path_to_repo)
end end
......
...@@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base ...@@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
def self.reference_pattern def self.reference_pattern
%r{ @reference_pattern ||= %r{
(#{Project.reference_pattern})? (#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<snippet>\d+) #{Regexp.escape(reference_prefix)}(?<snippet>\d+)
}x }x
end end
def self.link_reference_pattern def self.link_reference_pattern
super("snippets", /(?<snippet>\d+)/) @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end end
def to_reference(from_project = nil) def to_reference(from_project = nil)
......
...@@ -408,6 +408,8 @@ class User < ActiveRecord::Base ...@@ -408,6 +408,8 @@ class User < ActiveRecord::Base
end end
def owns_notification_email 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) self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end end
......
...@@ -43,7 +43,7 @@ module Issues ...@@ -43,7 +43,7 @@ module Issues
def create_new_issue def create_new_issue
new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, new_params = { id: nil, iid: nil, label_ids: [], milestone: nil,
project: @new_project, author: @old_issue.author, 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) new_params = @old_issue.serializable_hash.merge(new_params)
CreateService.new(@new_project, @current_user, new_params).execute CreateService.new(@new_project, @current_user, new_params).execute
...@@ -53,7 +53,7 @@ module Issues ...@@ -53,7 +53,7 @@ module Issues
@old_issue.notes.find_each do |note| @old_issue.notes.find_each do |note|
new_note = note.dup new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue, 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, created_at: note.created_at,
updated_at: note.updated_at } updated_at: note.updated_at }
...@@ -61,6 +61,18 @@ module Issues ...@@ -61,6 +61,18 @@ module Issues
end end
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 def close_issue
close_service = CloseService.new(@old_project, @current_user) close_service = CloseService.new(@old_project, @current_user)
close_service.execute(@old_issue, notifications: false, system_note: false) close_service.execute(@old_issue, notifications: false, system_note: false)
...@@ -78,20 +90,12 @@ module Issues ...@@ -78,20 +90,12 @@ module Issues
direction: :to) direction: :to)
end end
def unfold_references(content) def mark_as_moved
return unless content @old_issue.update(moved_to: @new_issue)
rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
@current_user)
rewriter.rewrite(@new_project)
end end
def notify_participants def notify_participants
notification_service.issue_moved(@old_issue, @new_issue, @current_user) notification_service.issue_moved(@old_issue, @new_issue, @current_user)
end end
def mark_as_moved
@old_issue.update(moved_to: @new_issue)
end
end end
end end
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
...@@ -95,17 +95,19 @@ class SystemHooksService ...@@ -95,17 +95,19 @@ class SystemHooksService
end end
def project_member_data(model) def project_member_data(model)
project = model.project || Project.unscoped.find(model.source_id)
{ {
project_name: model.project.name, project_name: project.name,
project_path: model.project.path, project_path: project.path,
project_path_with_namespace: model.project.path_with_namespace, project_path_with_namespace: project.path_with_namespace,
project_id: model.project.id, project_id: project.id,
user_username: model.user.username, user_username: model.user.username,
user_name: model.user.name, user_name: model.user.name,
user_email: model.user.email, user_email: model.user.email,
user_id: model.user.id, user_id: model.user.id,
access_level: model.human_access, access_level: model.human_access,
project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase project_visibility: Project.visibility_levels.key(project.visibility_level_field).downcase
} }
end end
......
...@@ -224,7 +224,7 @@ class SystemNoteService ...@@ -224,7 +224,7 @@ class SystemNoteService
# #
# "Started branch `issue-branch-button-201`" # "Started branch `issue-branch-button-201`"
def self.new_issue_branch(issue, project, author, branch) 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) link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})" body = "Started branch [`#{branch}`](#{link})"
......
...@@ -123,7 +123,7 @@ class TodoService ...@@ -123,7 +123,7 @@ class TodoService
def handle_note(note, author) def handle_note(note, author)
# Skip system notes, and notes on project snippet # 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 project = note.project
target = note.noteable target = note.noteable
...@@ -170,14 +170,30 @@ class TodoService ...@@ -170,14 +170,30 @@ class TodoService
end end
def filter_mentioned_users(project, target, author) def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users.select do |user| mentioned_users = target.mentioned_users
user.can?(:read_project, project) mentioned_users = reject_users_without_access(mentioned_users, project, target)
end
mentioned_users.delete(author) mentioned_users.delete(author)
mentioned_users.uniq mentioned_users.uniq
end 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 = {}) def pending_todos(user, criteria = {})
valid_keys = [:project_id, :target_id, :target_type, :commit_id] valid_keys = [:project_id, :target_id, :target_type, :commit_id]
user.todos.pending.where(criteria.slice(*valid_keys)) user.todos.pending.where(criteria.slice(*valid_keys))
......
# encoding: utf-8 # encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base class FileUploader < CarrierWave::Uploader::Base
include UploaderHelper include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
storage :file storage :file
attr_accessor :project, :secret attr_accessor :project, :secret
def initialize(project, secret = self.class.generate_secret) def initialize(project, secret = nil)
@project = project @project = project
@secret = secret @secret = secret || self.class.generate_secret
end end
def base_dir def base_dir
...@@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base ...@@ -23,14 +24,14 @@ class FileUploader < CarrierWave::Uploader::Base
File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) File.join(base_dir, 'tmp', @project.path_with_namespace, @secret)
end end
def self.generate_secret
SecureRandom.hex
end
def secure_url def secure_url
File.join("/uploads", @secret, file.filename) File.join("/uploads", @secret, file.filename)
end end
def to_markdown
to_h[:markdown]
end
def to_h def to_h
filename = image? ? self.file.basename : self.file.filename filename = image? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]") escaped_filename = filename.gsub("]", "\\]")
...@@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base ...@@ -45,4 +46,8 @@ class FileUploader < CarrierWave::Uploader::Base
markdown: markdown markdown: markdown
} }
end end
def self.generate_secret
SecureRandom.hex
end
end end
...@@ -76,13 +76,6 @@ ...@@ -76,13 +76,6 @@
= f.label :gravatar_enabled do = f.label :gravatar_enabled do
= f.check_box :gravatar_enabled = f.check_box :gravatar_enabled
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 .form-group
= f.label :default_projects_limit, class: 'control-label col-sm-2' = f.label :default_projects_limit, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
.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
%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 .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
(removed) (removed)
%span.todo-label %span.todo-label
= todo_action_name(todo) = 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)} &middot; #{time_ago_with_tooltip(todo.created_at)}
......
- if event.visible_to_user?(current_user) - 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 .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
......
...@@ -7,21 +7,3 @@ ...@@ -7,21 +7,3 @@
= link_to_project event.project = link_to_project event.project
- else - else
= event.project_name = 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"}
- 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"
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
= render "layouts/broadcast" = render "layouts/broadcast"
.expand-nav
= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo .header-logo
%a#logo %a#logo
...@@ -8,15 +10,19 @@ ...@@ -8,15 +10,19 @@
.gitlab-text-container .gitlab-text-container
%h3 GitLab %h3 GitLab
- if defined?(sidebar) && sidebar - primary_sidebar = current_user ? 'dashboard' : 'explore'
= render "layouts/nav/#{sidebar}"
- elsif current_user - if defined?(sidebar) && sidebar && sidebar != primary_sidebar
= render 'layouts/nav/dashboard' .complex-sidebar
.nav-primary
= render "layouts/nav/#{primary_sidebar}"
.nav-secondary
= render "layouts/nav/#{sidebar}"
- else - else
= render 'layouts/nav/explore' = render "layouts/nav/#{primary_sidebar}"
.collapse-nav .collapse-nav
= render partial: 'layouts/collapse_button' = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar"
- if current_user - if current_user
= link_to current_user, class: 'sidebar-user', title: "Profile" do = link_to current_user, class: 'sidebar-user', title: "Profile" do
= image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
......
.search - if controller.controller_path =~ /^groups/
= form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - label = 'This group'
= search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" - 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) = hidden_field_tag :group_id, @group.try(:id)
- if @project && @project.persisted? = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
= hidden_field_tag :project_id, @project.id
- if @project && @project.persisted?
- if current_controller?(:issues) - if current_controller?(:issues)
= hidden_field_tag :scope, 'issues' = hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests) - elsif current_controller?(:merge_requests)
...@@ -21,10 +44,3 @@ ...@@ -21,10 +44,3 @@
= hidden_field_tag :repository_ref, @ref = hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test' = 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 } .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();
}
});
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
Spam Logs Spam Logs
%span.count= number_with_delimiter(SpamLog.count(:all)) %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 = link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw') = icon('cogs fw')
%span %span
......
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
= icon('dashboard fw') = icon('dashboard fw')
%span %span
Activity Activity
= nav_link(controller: :groups) do = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do
= link_to dashboard_groups_path, title: 'Groups' do = link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw') = icon('group fw')
%span %span
Groups Groups
= nav_link(controller: :milestones) do = nav_link(path: 'dashboard#milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do = link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw') = icon('clock-o fw')
%span %span
...@@ -48,7 +48,6 @@ ...@@ -48,7 +48,6 @@
%span %span
Help Help
%li.separate-item
= nav_link(controller: :profile) do = nav_link(controller: :profile) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw') = icon('user fw')
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment