Commit 4e0e490f authored by Yorick Peterse's avatar Yorick Peterse

Merge CE master as of commit badb35

parents 8df7028f badb3533
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)
- The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse)
- All service classes (those residing in app/services) are now instrumented (Yorick Peterse)
- Developers can now add custom tags to transactions (Yorick Peterse)
- Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse)
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea). - All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Add setting for customizing the list of trusted proxies !3524
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389 (Yorick Peterse) - 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)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings - Expose project badges in project settings
- 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) - Expose label description in API (Mariusz Jachimowicz)
- Allow back dating on issues when created through the API - API: Ability to update a group (Robert Schilling)
- API: Ability to move issues (Robert Schilling)
- Fix Error 500 after renaming a project path (Stan Hu) - Fix Error 500 after renaming a project path (Stan Hu)
- Fix a bug whith trailing slash in teamcity_url (Charles May)
- Allow back dating on issues when created or updated through the API
- Allow back dating on issue notes when created through the API
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling) - API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530 - Allow SAML to handle external users based on user's information !3530
- Allow Omniauth providers to be marked as `external` !3657
- Add endpoints to archive or unarchive a project !3372 - Add endpoints to archive or unarchive a project !3372
- Fix a bug whith trailing slash in bamboo_url
- 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) - Handle nil descriptions in Slack issue messages (Stan Hu)
- API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling)
- API: Ability to star and unstar a project (Robert Schilling)
- Add default scope to projects to exclude projects pending deletion - Add default scope to projects to exclude projects pending deletion
- Allow to close merge requests which source projects(forks) are deleted.
- Ensure empty recipients are rejected in BuildsEmailService - Ensure empty recipients are rejected in BuildsEmailService
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling) - API: Fix milestone filtering by `iid` (Robert Schilling)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups - Better errors handling when creating milestones inside groups
- Fix high CPU usage when PostReceive receives refs/merge-requests/<id>
- Hide `Create a group` help block when creating a new project in a group
- 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) - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Decouple membership and notifications
- Fix creation of merge requests for orphaned branches (Stan Hu) - Fix creation of merge requests for orphaned branches (Stan Hu)
- API: Ability to retrieve a single tag (Robert Schilling)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - 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) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
- Fix admin/projects when using visibility levels on search (PotHix) - Fix admin/projects when using visibility levels on search (PotHix)
- Build status notifications - Build status notifications
- Update GitLab Pages to 0.2.2: fixes content-type of predefined 404 page - Update GitLab Pages to 0.2.2: fixes content-type of predefined 404 page
- API: Expose user location (Robert Schilling) - API: Expose user location (Robert Schilling)
- API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600 - Update number of Todos in the sidebar when it's marked as "Done". !3600
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
- Add ability to sync to remote mirrors. !249 - Add ability to sync to remote mirrors. !249
- API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
- Diffs load at the correct point when linking from from number
- Selected diff rows highlight
v 8.6.6
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- Project switcher uses new dropdown styling
v 8.6.5 v 8.6.5
- Fix importing from GitHub Enterprise. !3529 - Fix importing from GitHub Enterprise. !3529
......
...@@ -295,9 +295,9 @@ group :development, :test do ...@@ -295,9 +295,9 @@ group :development, :test do
gem 'teaspoon', '~> 1.1.0' gem 'teaspoon', '~> 1.1.0'
gem 'teaspoon-jasmine', '~> 2.2.0' gem 'teaspoon-jasmine', '~> 2.2.0'
gem 'spring', '~> 1.6.4' gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.38.0', require: false gem 'rubocop', '~> 0.38.0', require: false
......
...@@ -793,10 +793,10 @@ GEM ...@@ -793,10 +793,10 @@ GEM
spinach (>= 0.4) spinach (>= 0.4)
spinach-rerun-reporter (0.0.2) spinach-rerun-reporter (0.0.2)
spinach (~> 0.8) spinach (~> 0.8)
spring (1.6.4) spring (1.7.1)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
spring-commands-spinach (1.0.0) spring-commands-spinach (1.1.0)
spring (>= 0.9.1) spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2) spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1) spring (>= 0.9.1)
...@@ -1063,9 +1063,9 @@ DEPENDENCIES ...@@ -1063,9 +1063,9 @@ DEPENDENCIES
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2) spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.6.4) spring (~> 1.7.0)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0) spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2) spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.6.0) sprockets (~> 3.6.0)
state_machines-activerecord (~> 0.3.0) state_machines-activerecord (~> 0.3.0)
......
...@@ -23,7 +23,17 @@ ...@@ -23,7 +23,17 @@
#= require cal-heatmap #= require cal-heatmap
#= require turbolinks #= require turbolinks
#= require autosave #= require autosave
#= require bootstrap #= require bootstrap/affix
#= require bootstrap/alert
#= require bootstrap/button
#= require bootstrap/collapse
#= require bootstrap/dropdown
#= require bootstrap/modal
#= require bootstrap/scrollspy
#= require bootstrap/tab
#= require bootstrap/transition
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2 #= require select2
#= require raphael #= require raphael
#= require g.raphael #= require g.raphael
...@@ -42,6 +52,7 @@ ...@@ -42,6 +52,7 @@
#= require shortcuts_issuable #= require shortcuts_issuable
#= require shortcuts_network #= require shortcuts_network
#= require jquery.nicescroll #= require jquery.nicescroll
#= require date.format
#= require_tree . #= require_tree .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper #= require cropper
...@@ -166,7 +177,7 @@ $ -> ...@@ -166,7 +177,7 @@ $ ->
$('.trigger-submit').on 'change', -> $('.trigger-submit').on 'change', ->
$(@).parents('form').submit() $(@).parents('form').submit()
$('abbr.timeago, .js-timeago').timeago() gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false)
# Flash # Flash
if (flash = $(".flash-container")).length > 0 if (flash = $(".flash-container")).length > 0
......
...@@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> ...@@ -29,7 +29,11 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
e.preventDefault() e.preventDefault()
$form = $(e.target).closest('form') $form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable() $submit_button = $form.find('input[type=submit], button[type=submit]')
return if $submit_button.attr('disabled')
$submit_button.disable()
$form.submit() $form.submit()
# If the user tabs to a submit button on a `js-quick-submit` form, display a # If the user tabs to a submit button on a `js-quick-submit` form, display a
......
...@@ -35,4 +35,18 @@ $.fn.requiresInput = -> ...@@ -35,4 +35,18 @@ $.fn.requiresInput = ->
$form.on 'change input', fieldSelector, requireInput $form.on 'change input', fieldSelector, requireInput
$ -> $ ->
$('form.js-requires-input').requiresInput() $form = $('form.js-requires-input')
$form.requiresInput()
# Hide or Show the help block when creating a new project
# based on the option selected
hideOrShowHelpBlock = (form) ->
selected = $('.js-select-namespace option:selected')
if selected.length and selected.data('options-parent') is 'groups'
return form.find('.help-block').hide()
else if selected.length
form.find('.help-block').show()
hideOrShowHelpBlock($form)
$('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form)
...@@ -28,26 +28,26 @@ class Dispatcher ...@@ -28,26 +28,26 @@ class Dispatcher
new Todos() new Todos()
when 'projects:milestones:new', 'projects:milestones:edit' when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode() new ZenMode()
new DropzoneInput($('.milestone-form')) new GLForm($('.milestone-form'))
when 'groups:milestones:new' when 'groups:milestones:new'
new ZenMode() new ZenMode()
when 'projects:compare:show' when 'projects:compare:show'
new Diff() new Diff()
when 'projects:issues:new','projects:issues:edit' when 'projects:issues:new','projects:issues:edit'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.issue-form')) new GLForm($('.issue-form'))
new IssuableForm($('.issue-form')) new IssuableForm($('.issue-form'))
when 'projects:merge_requests:new', 'projects:merge_requests:edit' when 'projects:merge_requests:new', 'projects:merge_requests:edit'
new Diff() new Diff()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.merge-request-form')) new GLForm($('.merge-request-form'))
new IssuableForm($('.merge-request-form')) new IssuableForm($('.merge-request-form'))
when 'projects:tags:new' when 'projects:tags:new'
new ZenMode() new ZenMode()
new DropzoneInput($('.tag-form')) new GLForm($('.tag-form'))
when 'projects:releases:edit' when 'projects:releases:edit'
new ZenMode() new ZenMode()
new DropzoneInput($('.release-form')) new GLForm($('.release-form'))
when 'projects:merge_requests:show' when 'projects:merge_requests:show'
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
...@@ -141,7 +141,7 @@ class Dispatcher ...@@ -141,7 +141,7 @@ class Dispatcher
new Wikis() new Wikis()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ZenMode() new ZenMode()
new DropzoneInput($('.wiki-form')) new GLForm($('.wiki-form'))
when 'snippets' when 'snippets'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show' new ZenMode() if path[2] == 'show'
......
...@@ -15,11 +15,13 @@ class @DropzoneInput ...@@ -15,11 +15,13 @@ class @DropzoneInput
project_uploads_path = window.project_uploads_path or null project_uploads_path = window.project_uploads_path or null
max_file_size = gon.max_file_size or 10 max_file_size = gon.max_file_size or 10
form_textarea = $(form).find("textarea.markdown-area") form_textarea = $(form).find(".js-gfm-input")
form_textarea.wrap "<div class=\"div-dropzone\"></div>" form_textarea.wrap "<div class=\"div-dropzone\"></div>"
form_textarea.on 'paste', (event) => form_textarea.on 'paste', (event) =>
handlePaste(event) handlePaste(event)
$mdArea = $(form_textarea).closest('.md-area')
$(form).setupMarkdownPreview() $(form).setupMarkdownPreview()
form_dropzone = $(form).find('.div-dropzone') form_dropzone = $(form).find('.div-dropzone')
...@@ -49,17 +51,16 @@ class @DropzoneInput ...@@ -49,17 +51,16 @@ class @DropzoneInput
$(".div-dropzone-alert").alert "close" $(".div-dropzone-alert").alert "close"
dragover: -> dragover: ->
form_textarea.addClass "div-dropzone-focus" $mdArea.addClass 'is-dropzone-hover'
form.find(".div-dropzone-hover").css "opacity", 0.7 form.find(".div-dropzone-hover").css "opacity", 0.7
return return
dragleave: -> dragleave: ->
form_textarea.removeClass "div-dropzone-focus" $mdArea.removeClass 'is-dropzone-hover'
form.find(".div-dropzone-hover").css "opacity", 0 form.find(".div-dropzone-hover").css "opacity", 0
return return
drop: -> drop: ->
form_textarea.removeClass "div-dropzone-focus"
form.find(".div-dropzone-hover").css "opacity", 0 form.find(".div-dropzone-hover").css "opacity", 0
form_textarea.focus() form_textarea.focus()
return return
......
...@@ -122,7 +122,9 @@ class GitLabDropdown ...@@ -122,7 +122,9 @@ class GitLabDropdown
FILTER_INPUT = '.dropdown-input .dropdown-input-field' FILTER_INPUT = '.dropdown-input .dropdown-input-field'
constructor: (@el, @options) -> constructor: (@el, @options) ->
@dropdown = $(@el).parent() self = @
selector = $(@el).data "target"
@dropdown = if selector? then $(selector) else $(@el).parent()
# Set Defaults # Set Defaults
{ {
......
class @GLForm
constructor: (@form) ->
@textarea = @form.find('textarea.js-gfm-input')
# Before we start, we should clean up any previous data for this form
@destroy()
# Setup the form
@setupForm()
@form.data 'gl-form', @
destroy: ->
# Clean form listeners
@clearEventListeners()
@form.data 'gl-form', null
setupForm: ->
isNewForm = @form.is(':not(.gfm-form)')
@form.removeClass 'js-new-note-form'
if isNewForm
@form.find('.div-dropzone').remove()
@form.addClass('gfm-form')
disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button')
# remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup()
new DropzoneInput(@form)
autosize(@textarea)
# form and textarea event listeners
@addEventListeners()
# hide discard button
@form.find('.js-note-discard').hide()
@form.show()
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
addEventListeners: ->
@textarea.on 'focus', ->
$(@).closest('.md-area').addClass 'is-focused'
@textarea.on 'blur', ->
$(@).closest('.md-area').removeClass 'is-focused'
...@@ -10,6 +10,9 @@ class @Issue ...@@ -10,6 +10,9 @@ class @Issue
@initTaskList() @initTaskList()
@initIssueBtnEventListeners() @initIssueBtnEventListeners()
@initMergeRequests()
@initRelatedBranches()
initTaskList: -> initTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('enable') $('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
...@@ -69,3 +72,23 @@ class @Issue ...@@ -69,3 +72,23 @@ class @Issue
type: 'PATCH' type: 'PATCH'
url: $('form.js-issuable-update').attr('action') url: $('form.js-issuable-update').attr('action')
data: patchData data: patchData
initMergeRequests: ->
$container = $('#merge-requests')
$.getJSON($container.data('url'))
.error ->
new Flash('Failed to load referenced merge requests', 'alert')
.success (data) ->
if 'html' of data
$container.html(data.html)
initRelatedBranches: ->
$container = $('#related-branches')
$.getJSON($container.data('url'))
.error ->
new Flash('Failed to load related branches', 'alert')
.success (data) ->
if 'html' of data
$container.html(data.html)
...@@ -34,7 +34,7 @@ class @LabelsSelect ...@@ -34,7 +34,7 @@ class @LabelsSelect
labelHTMLTemplate = _.template( labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %> '<% _.each(labels, function(label){ %>
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
<span class="label color-label" style="background-color: <%= label.color %>;"> <span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;">
<%= label.title %> <%= label.title %>
</span> </span>
</a> </a>
...@@ -165,6 +165,8 @@ class @LabelsSelect ...@@ -165,6 +165,8 @@ class @LabelsSelect
.html(template) .html(template)
$sidebarCollapsedValue.text(labelCount) $sidebarCollapsedValue.text(labelCount)
$('.has-tooltip', $value).tooltip(container: 'body')
$value $value
.find('a') .find('a')
.each((i) -> .each((i) ->
...@@ -218,7 +220,7 @@ class @LabelsSelect ...@@ -218,7 +220,7 @@ class @LabelsSelect
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label' if selected and selected.title?
selected.title selected.title
else else
defaultLabel defaultLabel
......
((w) ->
w.gl ?= {}
w.gl.utils ?= {}
w.gl.utils.formatDate = (datetime) ->
dateFormat(datetime, 'mmm d, yyyy h:MMtt Z')
w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) ->
$timeagoEls.each( ->
$el = $(@)
$el.attr('title', gl.utils.formatDate($el.attr('datetime')))
)
$timeagoEls.timeago() if setTimeago
) window
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
notificationGranted = (message, opts, onclick) -> notificationGranted = (message, opts, onclick) ->
notification = new Notification(message, opts) notification = new Notification(message, opts)
# Hide the notification after X amount of seconds
setTimeout ->
notification.close()
, 8000
if onclick if onclick
notification.onclick = onclick notification.onclick = onclick
......
...@@ -85,8 +85,10 @@ class @MergeRequestTabs ...@@ -85,8 +85,10 @@ class @MergeRequestTabs
scrollToElement: (container) -> scrollToElement: (container) ->
if window.location.hash if window.location.hash
$el = $("div#{container} #{window.location.hash}") navBarHeight = $('.navbar-gitlab').outerHeight()
$('body').scrollTo($el.offset().top) if $el.length
$el = $("#{container} #{window.location.hash}")
$.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length
# Activate a tab based on the current action # Activate a tab based on the current action
activateTab: (action) -> activateTab: (action) ->
...@@ -142,7 +144,7 @@ class @MergeRequestTabs ...@@ -142,7 +144,7 @@ class @MergeRequestTabs
url: "#{source}.json" url: "#{source}.json"
success: (data) => success: (data) =>
document.querySelector("div#commits").innerHTML = data.html document.querySelector("div#commits").innerHTML = data.html
$('.js-timeago').timeago() gl.utils.localTimeAgo($('.js-timeago', 'div#commits'))
@commitsLoaded = true @commitsLoaded = true
@scrollToElement("#commits") @scrollToElement("#commits")
...@@ -152,12 +154,38 @@ class @MergeRequestTabs ...@@ -152,12 +154,38 @@ class @MergeRequestTabs
@_get @_get
url: "#{source}.json" + @_location.search url: "#{source}.json" + @_location.search
success: (data) => success: (data) =>
document.querySelector("div#diffs").innerHTML = data.html $('#diffs').html data.html
$('.js-timeago').timeago() gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
$('div#diffs .js-syntax-highlight').syntaxHighlight() $('#diffs .js-syntax-highlight').syntaxHighlight()
@expandViewContainer() if @diffViewType() is 'parallel' @expandViewContainer() if @diffViewType() is 'parallel'
@diffsLoaded = true @diffsLoaded = true
@scrollToElement("#diffs") @scrollToElement("#diffs")
@highlighSelectedLine()
$(document)
.off 'click', '.diff-line-num a'
.on 'click', '.diff-line-num a', (e) =>
e.preventDefault()
window.location.hash = $(e.currentTarget).attr 'href'
@highlighSelectedLine()
@scrollToElement("#diffs")
highlighSelectedLine: ->
$('.hll').removeClass 'hll'
locationHash = window.location.hash
if locationHash isnt ''
hashClassString = ".#{locationHash.replace('#', '')}"
$diffLine = $(locationHash)
if $diffLine.is ':not(tr)'
$diffLine = $("td#{locationHash}, td#{hashClassString}")
else
$diffLine = $('td', $diffLine)
$diffLine.addClass 'hll'
diffLineTop = $diffLine.offset().top
navBarHeight = $('.navbar-gitlab').outerHeight()
loadBuilds: (source) -> loadBuilds: (source) ->
return if @buildsLoaded return if @buildsLoaded
...@@ -166,7 +194,7 @@ class @MergeRequestTabs ...@@ -166,7 +194,7 @@ class @MergeRequestTabs
url: "#{source}.json" url: "#{source}.json"
success: (data) => success: (data) =>
document.querySelector("div#builds").innerHTML = data.html document.querySelector("div#builds").innerHTML = data.html
$('.js-timeago').timeago() gl.utils.localTimeAgo($('.js-timeago', 'div#builds'))
@buildsLoaded = true @buildsLoaded = true
@scrollToElement("#builds") @scrollToElement("#builds")
......
...@@ -12,10 +12,19 @@ class @MergeRequestWidget ...@@ -12,10 +12,19 @@ class @MergeRequestWidget
@readyForCICheck = true @readyForCICheck = true
clearInterval @fetchBuildStatusInterval clearInterval @fetchBuildStatusInterval
@clearEventListeners()
@addEventListeners()
@pollCIStatus() @pollCIStatus()
notifyPermissions() notifyPermissions()
setOpts: (@opts) -> clearEventListeners: ->
$(document).off 'page:change.merge_request'
addEventListeners: ->
$(document).on 'page:change.merge_request', =>
if $('body').data('page') isnt 'projects:merge_requests:show'
clearInterval @fetchBuildStatusInterval
@clearEventListeners()
mergeInProgress: (deleteSourceBranch = false)-> mergeInProgress: (deleteSourceBranch = false)->
$.ajax $.ajax
...@@ -49,7 +58,7 @@ class @MergeRequestWidget ...@@ -49,7 +58,7 @@ class @MergeRequestWidget
$('.mr-state-widget').replaceWith(data) $('.mr-state-widget').replaceWith(data)
ciLabelForStatus: (status) -> ciLabelForStatus: (status) ->
if status == 'success' if status is 'success'
'passed' 'passed'
else else
status status
...@@ -78,18 +87,28 @@ class @MergeRequestWidget ...@@ -78,18 +87,28 @@ class @MergeRequestWidget
@opts.ci_status = data.status @opts.ci_status = data.status
return return
if data.status isnt @opts.ci_status if data.status isnt @opts.ci_status and data.status?
@showCIStatus data.status @showCIStatus data.status
if data.coverage if data.coverage
@showCICoverage data.coverage @showCICoverage data.coverage
if showNotification if showNotification
message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) status = @ciLabelForStatus(data.status)
if status is "preparing"
title = @opts.ci_title.preparing
status = status.charAt(0).toUpperCase() + status.slice(1);
message = @opts.ci_message.preparing.replace('{{status}}', status)
else
title = @opts.ci_title.normal
message = @opts.ci_message.normal.replace('{{status}}', status)
title = title.replace('{{status}}', status)
message = message.replace('{{sha}}', data.sha) message = message.replace('{{sha}}', data.sha)
message = message.replace('{{title}}', data.title) message = message.replace('{{title}}', data.title)
notify( notify(
"Build #{@ciLabelForStatus(data.status)}", title,
message, message,
@opts.gitlab_icon, @opts.gitlab_icon,
-> ->
...@@ -109,6 +128,8 @@ class @MergeRequestWidget ...@@ -109,6 +128,8 @@ class @MergeRequestWidget
@setMergeButtonClass('btn-danger') @setMergeButtonClass('btn-danger')
when "running", "pending" when "running", "pending"
@setMergeButtonClass('btn-warning') @setMergeButtonClass('btn-warning')
when "success"
@setMergeButtonClass('btn-create')
else else
$('.ci_widget.ci-error').show() $('.ci_widget.ci-error').show()
@setMergeButtonClass('btn-danger') @setMergeButtonClass('btn-danger')
...@@ -118,4 +139,6 @@ class @MergeRequestWidget ...@@ -118,4 +139,6 @@ class @MergeRequestWidget
$('.ci_widget:visible .ci-coverage').text(text) $('.ci_widget:visible .ci-coverage').text(text)
setMergeButtonClass: (css_class) -> setMergeButtonClass: (css_class) ->
$('.accept_merge_request').removeClass("btn-create").addClass(css_class) $('.accept_merge_request')
.removeClass('btn-danger btn-warning btn-create')
.addClass(css_class)
...@@ -163,9 +163,15 @@ class @Notes ...@@ -163,9 +163,15 @@ class @Notes
else if @isNewNote(note) else if @isNewNote(note)
@note_ids.push(note.id) @note_ids.push(note.id)
$('ul.main-notes-list') $notesList = $('ul.main-notes-list')
$notesList
.append(note.html) .append(note.html)
.syntaxHighlight() .syntaxHighlight()
# Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false)
@initTaskList() @initTaskList()
@updateNotesCount(1) @updateNotesCount(1)
...@@ -217,6 +223,8 @@ class @Notes ...@@ -217,6 +223,8 @@ class @Notes
# append new note to all matching discussions # append new note to all matching discussions
discussionContainer.append note_html discussionContainer.append note_html
gl.utils.localTimeAgo($('.js-timeago', note_html), false)
@updateNotesCount(1) @updateNotesCount(1)
### ###
...@@ -275,32 +283,10 @@ class @Notes ...@@ -275,32 +283,10 @@ class @Notes
show the form show the form
### ###
setupNoteForm: (form) -> setupNoteForm: (form) ->
disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button") new GLForm form
form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove()
# hide discard button
form.find('.js-note-discard').hide()
# setup preview buttons
previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text") textarea = form.find(".js-note-text")
textarea.on "input", ->
if $(this).val().trim() isnt ""
previewButton.removeClass("turn-off").addClass "turn-on"
else
previewButton.removeClass("turn-on").addClass "turn-off"
textarea.on 'focus', ->
$(this).closest('.md-area').addClass 'is-focused'
textarea.on 'blur', ->
$(this).closest('.md-area').removeClass 'is-focused'
autosize(textarea)
new Autosave textarea, [ new Autosave textarea, [
"Note" "Note"
form.find("#note_commit_id").val() form.find("#note_commit_id").val()
...@@ -309,11 +295,6 @@ class @Notes ...@@ -309,11 +295,6 @@ class @Notes
form.find("#note_noteable_id").val() form.find("#note_noteable_id").val()
] ]
# remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
form.show()
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -345,7 +326,9 @@ class @Notes ...@@ -345,7 +326,9 @@ class @Notes
updateNote: (_xhr, note, _status) => updateNote: (_xhr, note, _status) =>
# Convert returned HTML to a jQuery object so we can modify it further # Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html) $html = $(note.html)
$('.js-timeago', $html).timeago()
gl.utils.localTimeAgo($('.js-timeago', $html))
$html.syntaxHighlight() $html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable') $html.find('.js-task-list-container').taskList('enable')
...@@ -365,34 +348,15 @@ class @Notes ...@@ -365,34 +348,15 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.addClass "is-editting" note.addClass "is-editting"
form = note.find(".note-edit-form") form = note.find(".note-edit-form")
isNewForm = form.is(':not(.gfm-form)')
if isNewForm
form.addClass('gfm-form')
form.addClass('current-note-edit-form') form.addClass('current-note-edit-form')
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
# Setup markdown form new GLForm form
if isNewForm
GitLab.GfmAutoComplete.setup()
new DropzoneInput(form)
textarea = form.find("textarea")
textarea.focus()
if isNewForm form.find(".js-note-text").focus()
autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
# modify it, so let's clear it and re-set it!
value = textarea.val()
textarea.val ""
textarea.val value
if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button")
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
...@@ -549,6 +513,9 @@ class @Notes ...@@ -549,6 +513,9 @@ class @Notes
removeDiscussionNoteForm: (form)-> removeDiscussionNoteForm: (form)->
row = form.closest("tr") row = form.closest("tr")
glForm = form.data 'gl-form'
glForm.destroy()
form.find(".js-note-text").data("autosave").reset() form.find(".js-note-text").data("autosave").reset()
# show the reply button (will only work for replies) # show the reply button (will only work for replies)
...@@ -560,7 +527,6 @@ class @Notes ...@@ -560,7 +527,6 @@ class @Notes
# only remove the form # only remove the form
form.remove() form.remove()
cancelDiscussionForm: (e) => cancelDiscussionForm: (e) =>
e.preventDefault() e.preventDefault()
form = $(e.target).closest(".js-discussion-note-form") form = $(e.target).closest(".js-discussion-note-form")
......
...@@ -18,8 +18,11 @@ class @Profile ...@@ -18,8 +18,11 @@ class @Profile
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide() $(this).find('.loading-gif').hide()
$('.update-notifications').on 'ajax:complete', -> $('.update-notifications').on 'ajax:success', (e, data) ->
$(this).find('.btn-save').enable() if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@bindEvents() @bindEvents()
......
...@@ -37,19 +37,20 @@ class @Project ...@@ -37,19 +37,20 @@ class @Project
$('.update-notification').on 'click', (e) -> $('.update-notification').on 'click', (e) ->
e.preventDefault() e.preventDefault()
notification_level = $(@).data 'notification-level' notification_level = $(@).data 'notification-level'
$('#notification_level').val(notification_level) label = $(@).data 'notification-title'
$('#notification_setting_level').val(notification_level)
$('#notification-form').submit() $('#notification-form').submit()
label = null
switch notification_level
when 0 then label = ' Disabled '
when 1 then label = ' Participating '
when 2 then label = ' Watching '
when 3 then label = ' Global '
when 4 then label = ' On Mention '
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>") $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active' $(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active' $(@).parent().addClass 'active'
$('#notification-form').on 'ajax:success', (e, data) ->
if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@projectSelectDropdown() @projectSelectDropdown()
projectSelectDropdown: -> projectSelectDropdown: ->
......
class @ProjectSelect class @ProjectSelect
constructor: -> constructor: ->
$('.js-projects-dropdown-toggle').each (i, dropdown) ->
$dropdown = $(dropdown)
$dropdown.glDropdown(
filterable: true
filterRemote: true
search:
fields: ['name_with_namespace']
data: (term, callback) ->
finalCallback = (projects) ->
callback projects
if @includeGroups
projectsCallback = (projects) ->
groupsCallback = (groups) ->
data = groups.concat(projects)
finalCallback(data)
Api.groups term, false, groupsCallback
else
projectsCallback = finalCallback
if @groupId
Api.groupProjects @groupId, term, projectsCallback
else
Api.projects term, @orderBy, projectsCallback
url: (project) ->
project.web_url
text: (project) ->
project.name_with_namespace
)
$('.ajax-project-select').each (i, select) -> $('.ajax-project-select').each (i, select) ->
@groupId = $(select).data('group-id') @groupId = $(select).data('group-id')
@includeGroups = $(select).data('include-groups') @includeGroups = $(select).data('include-groups')
......
...@@ -59,6 +59,8 @@ class @Todos ...@@ -59,6 +59,8 @@ class @Todos
goToTodoUrl: (e)-> goToTodoUrl: (e)->
todoLink = $(this).data('url') todoLink = $(this).data('url')
return unless todoLink
if e.metaKey if e.metaKey
e.preventDefault() e.preventDefault()
window.open(todoLink,'_blank') window.open(todoLink,'_blank')
......
/** /**
* Styles that apply to all GFM related forms. * Styles that apply to all GFM related forms.
*/ */
.issue-form, .merge-request-form, .wiki-form {
.description {
height: 16em;
border-top-left-radius: 0;
}
}
.wiki-form {
.description {
height: 26em;
}
}
.milestone-form {
.description {
height: 14em;
}
}
.gfm-commit, .gfm-commit_range { .gfm-commit, .gfm-commit_range {
font-family: $monospace_font; font-family: $monospace_font;
......
...@@ -69,6 +69,7 @@ header { ...@@ -69,6 +69,7 @@ header {
} }
.header-content { .header-content {
position: relative;
height: $header-height; height: $header-height;
padding-right: 20px; padding-right: 20px;
...@@ -76,6 +77,10 @@ header { ...@@ -76,6 +77,10 @@ header {
padding-right: 0; padding-right: 0;
} }
.dropdown-menu {
margin-top: -5px;
}
.title { .title {
margin: 0; margin: 0;
font-size: 19px; font-size: 19px;
......
.div-dropzone-wrapper { .div-dropzone-wrapper {
.div-dropzone { .div-dropzone {
position: relative; position: relative;
margin-bottom: -5px;
.div-dropzone-focus {
border-color: #66afe9 !important;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important;
outline: 0 !important;
}
.div-dropzone-hover { .div-dropzone-hover {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-top: -0.5em; margin-top: -11.5px;
margin-left: -0.6em; margin-left: -15px;
opacity: 0; opacity: 0;
font-size: 50px; font-size: 30px;
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out;
pointer-events: none; pointer-events: none;
} }
......
...@@ -58,12 +58,12 @@ ...@@ -58,12 +58,12 @@
.nav-search { .nav-search {
display: inline-block; display: inline-block;
width: 50%; width: 100%;
padding: 11px 0; padding: 11px 0;
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 100%; width: 50%;
} }
} }
......
...@@ -43,7 +43,6 @@ ...@@ -43,7 +43,6 @@
@import "bootstrap/modals"; @import "bootstrap/modals";
@import "bootstrap/tooltip"; @import "bootstrap/tooltip";
@import "bootstrap/popovers"; @import "bootstrap/popovers";
@import "bootstrap/carousel";
// Utility classes // Utility classes
.clearfix { .clearfix {
......
...@@ -250,14 +250,6 @@ a > code { ...@@ -250,14 +250,6 @@ a > code {
* Textareas intended for GFM * Textareas intended for GFM
* *
*/ */
.js-gfm-input {
font-family: $monospace_font;
color: $gl-text-color;
}
.md-preview {
}
.strikethrough { .strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
......
...@@ -28,6 +28,7 @@ $gl-link-color: #3084bb; ...@@ -28,6 +28,7 @@ $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-icon-color: $gl-placeholder-color;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color; $gl-gray: $gl-text-color;
$gl-header-color: $gl-title-color; $gl-header-color: $gl-title-color;
...@@ -149,6 +150,7 @@ $light-grey-header: #faf9f9; ...@@ -149,6 +150,7 @@ $light-grey-header: #faf9f9;
*/ */
$gl-primary: $blue-normal; $gl-primary: $blue-normal;
$gl-success: $green-normal; $gl-success: $green-normal;
$gl-success-focus: rgba($gl-success, .4);
$gl-info: $blue-normal; $gl-info: $blue-normal;
$gl-warning: $orange-normal; $gl-warning: $orange-normal;
$gl-danger: $red-normal; $gl-danger: $red-normal;
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #557;
border-color: darken(#557, 15%);
}
.diff-line-num.new, .line_content.new { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #49483e;
border-color: darken(#49483e, 15%);
}
.diff-line-num.new, .line_content.new { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #174652;
border-color: darken(#174652, 15%);
}
.diff-line-num.new, .line_content.new { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #ddd8c5;
border-color: darken(#ddd8c5, 15%);
}
.diff-line-num.new, .line_content.new { .diff-line-num.new, .line_content.new {
@include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4);
} }
......
...@@ -21,6 +21,12 @@ ...@@ -21,6 +21,12 @@
// Diff line // Diff line
.line_holder { .line_holder {
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #f8eec7;
border-color: darken(#f8eec7, 15%);
}
.diff-line-num { .diff-line-num {
&.old { &.old {
background-color: $line-number-old; background-color: $line-number-old;
......
...@@ -41,8 +41,17 @@ ...@@ -41,8 +41,17 @@
word-wrap: break-word; word-wrap: break-word;
.md { .md {
color: #7f8fa4; color: $gl-grayish-blue;
font-size: $gl-font-size; font-size: $gl-font-size;
.label {
color: $gl-text-color;
font-size: inherit;
}
iframe.twitter-share-button {
vertical-align: bottom;
}
} }
pre { pre {
......
...@@ -263,6 +263,12 @@ ...@@ -263,6 +263,12 @@
} }
} }
.dropdown-content {
a:hover {
color: inherit;
}
}
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 100%; width: 100%;
padding-top: 6px; padding-top: 6px;
...@@ -316,3 +322,9 @@ ...@@ -316,3 +322,9 @@
color: #8c8c8c; color: #8c8c8c;
} }
} }
.issuable-form-padding-top {
@media (min-width: $screen-sm-min) {
padding-top: 7px;
}
}
...@@ -79,19 +79,30 @@ ...@@ -79,19 +79,30 @@
color: $white-light; color: $white-light;
} }
@mixin labels-mobile {
@media (max-width: $screen-xs-min) {
display: block;
width: 100%;
margin-left: 0;
padding: 10px 0;
}
}
.manage-labels-list { .manage-labels-list {
.prepend-left-10 { .prepend-left-10, .prepend-description-left {
display: inline-block; display: inline-block;
width: 40%; width: 40%;
vertical-align: middle; vertical-align: middle;
@media (max-width: $screen-xs-min) { @include labels-mobile;
display: block; }
width: 100%;
margin-left: 0; .prepend-description-left {
padding: 10px 0; width: 57%;
}
@include labels-mobile;
} }
.pull-info-right { .pull-info-right {
......
...@@ -142,6 +142,7 @@ ...@@ -142,6 +142,7 @@
overflow: hidden; overflow: hidden;
font-size: 90%; font-size: 90%;
margin: 0 3px; margin: 0 3px;
word-break: break-all;
} }
.mr-list { .mr-list {
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
} }
.note-textarea { .note-textarea {
display: block;
padding: 10px 0; padding: 10px 0;
font-family: $regular_font; font-family: $regular_font;
border: 0; border: 0;
...@@ -63,7 +64,7 @@ ...@@ -63,7 +64,7 @@
&.is-focused { &.is-focused {
border-color: $focus-border-color; border-color: $focus-border-color;
box-shadow: 0 0 2px rgba(#000, .2), box-shadow: 0 0 2px $black-transparent,
0 0 4px rgba($focus-border-color, .4); 0 0 4px rgba($focus-border-color, .4);
.comment-toolbar, .comment-toolbar,
...@@ -71,12 +72,35 @@ ...@@ -71,12 +72,35 @@
border-color: $focus-border-color; border-color: $focus-border-color;
} }
} }
&.is-dropzone-hover {
border-color: $gl-success;
box-shadow: 0 0 2px $black-transparent,
0 0 4px $gl-success-focus;
.comment-toolbar,
.nav-links {
border-color: $gl-success;
}
}
p {
code {
white-space: normal;
}
pre {
code {
white-space: pre;
}
}
}
} }
} }
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
background-color: #fff; background-color: $white-light;
} }
.note-edit-form { .note-edit-form {
......
...@@ -81,9 +81,15 @@ ul.notes { ...@@ -81,9 +81,15 @@ ul.notes {
@include md-typography; @include md-typography;
// On diffs code should wrap nicely and not overflow // On diffs code should wrap nicely and not overflow
pre { p {
code { code {
white-space: pre; white-space: normal;
}
pre {
code {
white-space: pre;
}
} }
} }
...@@ -112,6 +118,10 @@ ul.notes { ...@@ -112,6 +118,10 @@ ul.notes {
margin: 10px 0; margin: 10px 0;
} }
} }
a {
word-break: break-all;
}
} }
.note-header { .note-header {
...@@ -127,7 +137,7 @@ ul.notes { ...@@ -127,7 +137,7 @@ ul.notes {
margin-right: 10px; margin-right: 10px;
} }
.line_content { .line_content {
white-space: pre-wrap; white-space: pre;
} }
} }
...@@ -145,19 +155,27 @@ ul.notes { ...@@ -145,19 +155,27 @@ ul.notes {
background: $background-color; background: $background-color;
color: $text-color; color: $text-color;
} }
&.notes_line2 { &.notes_line2 {
text-align: center; text-align: center;
padding: 10px 0; padding: 10px 0;
border-left: 1px solid #ddd !important; border-left: 1px solid #ddd !important;
} }
&.notes_content { &.notes_content {
background-color: #fff; background-color: $background-color;
border-width: 1px 0; border-width: 1px 0;
padding: 0; padding: 0;
vertical-align: top; vertical-align: top;
white-space: normal;
&.parallel { &.parallel {
border-width: 1px; border-width: 1px;
} }
.notes {
background-color: $white-light;
}
} }
} }
} }
......
...@@ -34,6 +34,11 @@ ...@@ -34,6 +34,11 @@
color: #7f8fa4; color: #7f8fa4;
font-size: $gl-font-size; font-size: $gl-font-size;
.label {
color: $gl-text-color;
font-size: inherit;
}
p { p {
color: #5c5d5e; color: #5c5d5e;
} }
......
class Groups::NotificationSettingsController < Groups::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(group)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
...@@ -60,6 +60,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -60,6 +60,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
continue_login_process continue_login_process
end end
rescue Gitlab::OAuth::SignupDisabledError
handle_signup_error
end end
def omniauth_error def omniauth_error
...@@ -92,16 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -92,16 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
continue_login_process continue_login_process
end end
rescue Gitlab::OAuth::SignupDisabledError rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider']) handle_signup_error
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if current_application_settings.signup_enabled?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
flash[:notice] = message
redirect_to new_user_session_path
end end
def handle_service_ticket provider, ticket def handle_service_ticket provider, ticket
...@@ -122,6 +115,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -122,6 +115,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end end
end end
def handle_signup_error
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if current_application_settings.signup_enabled?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
flash[:notice] = message
redirect_to new_user_session_path
end
def oauth def oauth
@oauth ||= request.env['omniauth.auth'] @oauth ||= request.env['omniauth.auth']
end end
......
class Profiles::NotificationsController < Profiles::ApplicationController class Profiles::NotificationsController < Profiles::ApplicationController
def show def show
@user = current_user @user = current_user
@notification = current_user.notification @group_notifications = current_user.notification_settings.for_groups
@project_members = current_user.project_members @project_notifications = current_user.notification_settings.for_projects
@group_members = current_user.group_members
end end
def update def update
type = params[:notification_type] if current_user.update_attributes(user_params)
flash[:notice] = "Notification settings saved"
@saved = if type == 'global' else
current_user.update_attributes(user_params) flash[:alert] = "Failed to save new settings"
elsif type == 'group'
group_member = current_user.group_members.find(params[:notification_id])
group_member.notification_level = params[:notification_level]
group_member.save
else
project_member = current_user.project_members.find(params[:notification_id])
project_member.notification_level = params[:notification_level]
project_member.save
end
respond_to do |format|
format.html do
if @saved
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
end
redirect_back_or_default(default: profile_notifications_path)
end
format.js
end end
redirect_back_or_default(default: profile_notifications_path)
end end
def user_params def user_params
......
...@@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -3,7 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions include IssuableActions
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show] before_action :issue,
only: [:edit, :update, :show, :referenced_merge_requests, :related_branches]
# Allow read any issue # Allow read any issue
before_action :authorize_read_issue!, only: [:show] before_action :authorize_read_issue!, only: [:show]
...@@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -17,9 +18,6 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow issues bulk update # Allow issues bulk update
before_action :authorize_admin_issues!, only: [:bulk_update] before_action :authorize_admin_issues!, only: [:bulk_update]
# Cross-reference merge requests
before_action :closed_by_merge_requests, only: [:show]
respond_to :html respond_to :html
def index def index
...@@ -71,8 +69,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -71,8 +69,6 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue @noteable = @issue
@merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -124,15 +120,39 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -124,15 +120,39 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def referenced_merge_requests
@merge_requests = @issue.referenced_merge_requests(current_user)
@closed_by_merge_requests = @issue.closed_by_merge_requests(current_user)
respond_to do |format|
format.json do
render json: {
html: view_to_html_string('projects/issues/_merge_requests')
}
end
end
end
def related_branches
merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches -
merge_requests.map(&:source_branch)
respond_to do |format|
format.json do
render json: {
html: view_to_html_string('projects/issues/_related_branches')
}
end
end
end
def bulk_update def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end end
def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end
protected protected
def issue def issue
......
...@@ -252,6 +252,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -252,6 +252,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
status = "preparing" if status.nil?
response = { response = {
title: merge_request.title, title: merge_request.title,
sha: merge_request.last_commit_short_sha, sha: merge_request.last_commit_short_sha,
......
...@@ -39,8 +39,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -39,8 +39,7 @@ class Projects::NotesController < Projects::ApplicationController
def destroy def destroy
if note.editable? if note.editable?
note.destroy Notes::DeleteService.new(project, current_user).execute(note)
note.reset_events_cache
end end
respond_to do |format| respond_to do |format|
......
class Projects::NotificationSettingsController < Projects::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(project)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
...@@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController ...@@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController
end end
def archive def archive
RepositoryArchiveCacheWorker.perform_async
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
head :ok head :ok
rescue => ex rescue => ex
......
...@@ -102,14 +102,18 @@ class ProjectsController < Projects::ApplicationController ...@@ -102,14 +102,18 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
if current_user
@membership = @project.team.find_member(current_user.id)
if @membership
@notification_setting = current_user.notification_settings_for(@project)
end
end
if @project.repository_exists? if @project.repository_exists?
if @project.empty_repo? if @project.empty_repo?
render 'projects/empty' render 'projects/empty'
else else
if current_user
@membership = @project.team.find_member(current_user.id)
end
render :show render :show
end end
else else
......
...@@ -187,7 +187,7 @@ module ApplicationHelper ...@@ -187,7 +187,7 @@ module ApplicationHelper
element = content_tag :time, time.to_s, element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
datetime: time.to_time.getutc.iso8601, datetime: time.to_time.getutc.iso8601,
title: time.in_time_zone.to_s(:medium), title: time.to_time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' } data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js unless skip_js
......
...@@ -3,8 +3,16 @@ module NamespacesHelper ...@@ -3,8 +3,16 @@ module NamespacesHelper
groups = current_user.owned_groups + current_user.masters_groups groups = current_user.owned_groups + current_user.masters_groups
users = [current_user.namespace] users = [current_user.namespace]
group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ] data_attr_group = { 'data-options-parent' => 'groups' }
users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ] data_attr_users = { 'data-options-parent' => 'users' }
group_opts = [
"Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] }
]
users_opts = [
"Users", users.sort_by(&:human_name).map { |u| [display_path ? u.path : u.human_name, u.id, data_attr_users] }
]
options = [] options = []
options << group_opts options << group_opts
......
module NotificationsHelper module NotificationsHelper
include IconsHelper include IconsHelper
def notification_icon(notification) def notification_icon_class(level)
if notification.disabled? case level.to_sym
icon('volume-off', class: 'ns-mute') when :disabled
elsif notification.participating? 'microphone-slash'
icon('volume-down', class: 'ns-part') when :participating
elsif notification.watch? 'volume-up'
icon('volume-up', class: 'ns-watch') when :watch
else 'eye'
icon('circle-o', class: 'ns-default') when :mention
'at'
when :global
'globe'
end end
end end
def notification_list_item(notification_level, user_membership) def notification_icon(level, text = nil)
case notification_level icon("#{notification_icon_class(level)} fw", text: text)
when Notification::N_DISABLED
update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash')
when Notification::N_PARTICIPATING
update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up')
when Notification::N_WATCH
update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye')
when Notification::N_MENTION
update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at')
when Notification::N_GLOBAL
update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe')
else
# do nothing
end
end end
def update_notification_link(notification_level, user_membership, title, icon) def notification_title(level)
content_tag(:li, class: active_level_for(user_membership, notification_level)) do case level.to_sym
link_to '#', class: 'update-notification', data: { notification_level: notification_level } do when :participating
icon("#{icon} fw", text: title) 'Participate'
end when :mention
'On mention'
else
level.to_s.titlecase
end end
end end
def notification_label(user_membership) def notification_list_item(level, setting)
Notification.new(user_membership).to_s title = notification_title(level)
end
data = {
notification_level: level,
notification_title: title
}
def active_level_for(user_membership, level) content_tag(:li, class: ('active' if setting.level == level)) do
'active' if user_membership.notification_level == level link_to '#', class: 'update-notification', data: data do
notification_icon(level, title)
end
end
end end
end end
...@@ -65,21 +65,14 @@ module ProjectsHelper ...@@ -65,21 +65,14 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner)) link_to(simple_sanitize(owner.name), user_path(owner))
end end
project_link = link_to project_path(project), { class: "project-item-select-holder" } do project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
link_output = simple_sanitize(project.name)
if current_user if current_user
link_output += project_select_tag :project_path, project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
class: "project-item-select js-projects-dropdown",
data: { include_groups: false, order_by: 'last_activity_at' }
end
link_output
end end
project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user
full_title = namespace_link + ' / ' + project_link full_title = "#{namespace_link} / #{project_link}".html_safe
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
full_title full_title
end end
......
...@@ -20,6 +20,8 @@ module TodosHelper ...@@ -20,6 +20,8 @@ module TodosHelper
end end
def todo_target_path(todo) def todo_target_path(todo)
return unless todo.target.present?
anchor = dom_id(todo.note) if todo.note.present? anchor = dom_id(todo.note) if todo.note.present?
if todo.for_commit? if todo.for_commit?
......
...@@ -150,13 +150,11 @@ class Commit ...@@ -150,13 +150,11 @@ class Commit
end end
def hook_attrs(with_changed_files: false) def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace
data = { data = {
id: id, id: id,
message: safe_message, message: safe_message,
timestamp: committed_date.xmlschema, timestamp: committed_date.xmlschema,
url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}", url: Gitlab::UrlBuilder.build(self),
author: { author: {
name: author_name, name: author_name,
email: author_email email: author_email
......
# == Notifiable concern
#
# Contains notification functionality
#
module Notifiable
extend ActiveSupport::Concern
included do
validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true
end
def notification
@notification ||= Notification.new(self)
end
end
...@@ -32,6 +32,7 @@ class Group < Namespace ...@@ -32,6 +32,7 @@ class Group < Namespace
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy` # We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`. # here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, dependent: :destroy, foreign_key: 'entity_id' has_many :audit_events, -> { where(entity_type: Group) }, dependent: :destroy, foreign_key: 'entity_id'
has_many :notification_settings, dependent: :destroy, as: :source
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
class Member < ActiveRecord::Base class Member < ActiveRecord::Base
include Sortable include Sortable
include Notifiable
include Gitlab::Access include Gitlab::Access
attr_accessor :raw_invite_token attr_accessor :raw_invite_token
...@@ -57,12 +56,15 @@ class Member < ActiveRecord::Base ...@@ -57,12 +56,15 @@ class Member < ActiveRecord::Base
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite? after_create :send_invite, if: :invite?
after_create :create_notification_setting, unless: :invite?
after_create :post_create_hook, unless: :invite? after_create :post_create_hook, unless: :invite?
after_update :post_update_hook, unless: :invite? after_update :post_update_hook, unless: :invite?
after_destroy :post_destroy_hook, unless: :invite? after_destroy :post_destroy_hook, unless: :invite?
delegate :name, :username, :email, to: :user, prefix: true delegate :name, :username, :email, to: :user, prefix: true
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self class << self
def find_by_invite_token(invite_token) def find_by_invite_token(invite_token)
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
...@@ -163,6 +165,14 @@ class Member < ActiveRecord::Base ...@@ -163,6 +165,14 @@ class Member < ActiveRecord::Base
send_invite send_invite
end end
def create_notification_setting
user.notification_settings.find_or_create_for(source)
end
def notification_setting
@notification_setting ||= user.notification_settings_for(source)
end
private private
def send_invite def send_invite
......
...@@ -24,7 +24,6 @@ class GroupMember < Member ...@@ -24,7 +24,6 @@ class GroupMember < Member
# Make sure group member points only to group as it source # Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE default_value_for :source_type, SOURCE_TYPE
default_value_for :notification_level, Notification::N_GLOBAL
validates_format_of :source_type, with: /\ANamespace\z/ validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
......
...@@ -27,7 +27,6 @@ class ProjectMember < Member ...@@ -27,7 +27,6 @@ class ProjectMember < Member
# Make sure project member points only to project as it source # Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE default_value_for :source_type, SOURCE_TYPE
default_value_for :notification_level, Notification::N_GLOBAL
validates_format_of :source_type, with: /\AProject\z/ validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
......
...@@ -131,7 +131,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -131,7 +131,7 @@ class MergeRequest < ActiveRecord::Base
validates :target_project, presence: true validates :target_project, presence: true
validates :target_branch, presence: true validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds? validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches validate :validate_branches, unless: :allow_broken
validate :validate_fork validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
...@@ -222,7 +222,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -222,7 +222,7 @@ class MergeRequest < ActiveRecord::Base
end end
if opened? || reopened? if opened? || reopened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any? if similar_mrs.any?
errors.add :validate_branches, errors.add :validate_branches,
...@@ -349,7 +349,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -349,7 +349,7 @@ class MergeRequest < ActiveRecord::Base
def hook_attrs def hook_attrs
attrs = { attrs = {
source: source_project.hook_attrs, source: source_project.try(:hook_attrs),
target: target_project.hook_attrs, target: target_project.hook_attrs,
last_commit: nil, last_commit: nil,
work_in_progress: work_in_progress? work_in_progress: work_in_progress?
......
class Notification
#
# Notification levels
#
N_DISABLED = 0
N_PARTICIPATING = 1
N_WATCH = 2
N_GLOBAL = 3
N_MENTION = 4
attr_accessor :target
class << self
def notification_levels
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH]
end
def options_with_labels
{
disabled: N_DISABLED,
participating: N_PARTICIPATING,
watch: N_WATCH,
mention: N_MENTION,
global: N_GLOBAL
}
end
def project_notification_levels
[N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL]
end
end
def initialize(target)
@target = target
end
def disabled?
target.notification_level == N_DISABLED
end
def participating?
target.notification_level == N_PARTICIPATING
end
def watch?
target.notification_level == N_WATCH
end
def global?
target.notification_level == N_GLOBAL
end
def mention?
target.notification_level == N_MENTION
end
def level
target.notification_level
end
def to_s
case level
when N_DISABLED
'Disabled'
when N_PARTICIPATING
'Participating'
when N_WATCH
'Watching'
when N_MENTION
'On mention'
when N_GLOBAL
'Global'
else
# do nothing
end
end
end
class NotificationSetting < ActiveRecord::Base
enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
default_value_for :level, NotificationSetting.levels[:global]
belongs_to :user
belongs_to :source, polymorphic: true
validates :user, presence: true
validates :source, presence: true
validates :level, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
scope :for_groups, -> { where(source_type: 'Namespace') }
scope :for_projects, -> { where(source_type: 'Project') }
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
unless setting.persisted?
setting.save
end
setting
end
end
...@@ -168,6 +168,7 @@ class Project < ActiveRecord::Base ...@@ -168,6 +168,7 @@ class Project < ActiveRecord::Base
has_many :pages_domains, dependent: :destroy has_many :pages_domains, dependent: :destroy
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :audit_events, as: :entity, dependent: :destroy has_many :audit_events, as: :entity, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
...@@ -440,9 +441,15 @@ class Project < ActiveRecord::Base ...@@ -440,9 +441,15 @@ class Project < ActiveRecord::Base
end end
if forked? if forked?
RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
else else
RepositoryImportWorker.perform_async(self.id) job_id = RepositoryImportWorker.perform_async(self.id)
end
if job_id
Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
else
Rails.logger.error "Import job failed to start for #{path_with_namespace}"
end end
end end
......
...@@ -82,17 +82,17 @@ class BambooService < CiService ...@@ -82,17 +82,17 @@ class BambooService < CiService
end end
def build_info(sha) def build_info(sha)
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
if username.blank? && password.blank? if username.blank? && password.blank?
@response = HTTParty.get(parsed_url.to_s, verify: false) @response = HTTParty.get(url, verify: false)
else else
get_url = "#{url}&os_authType=basic" url << '&os_authType=basic'
auth = { auth = {
username: username, username: username,
password: password, password: password
} }
@response = HTTParty.get(get_url, verify: false, basic_auth: auth) @response = HTTParty.get(url, verify: false, basic_auth: auth)
end end
end end
...@@ -101,11 +101,11 @@ class BambooService < CiService ...@@ -101,11 +101,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0' if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page. # If actual build link can't be determined, send user to build summary page.
"#{bamboo_url}/browse/#{build_key}" URI.join(bamboo_url, "/browse/#{build_key}").to_s
else else
# If actual build link is available, go to build result page. # If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key'] result_key = @response['results']['results']['result']['planResultKey']['key']
"#{bamboo_url}/browse/#{result_key}" URI.join(bamboo_url, "/browse/#{result_key}").to_s
end end
end end
...@@ -134,7 +134,7 @@ class BambooService < CiService ...@@ -134,7 +134,7 @@ class BambooService < CiService
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
# Bamboo requires a GET and does not take any data. # Bamboo requires a GET and does not take any data.
self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
verify: false) self.class.get(url, verify: false)
end end
end end
...@@ -23,7 +23,7 @@ class BuildsEmailService < Service ...@@ -23,7 +23,7 @@ class BuildsEmailService < Service
prop_accessor :recipients prop_accessor :recipients
boolean_accessor :add_pusher boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_builds boolean_accessor :notify_only_broken_builds
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
def initialize_properties def initialize_properties
if properties.nil? if properties.nil?
...@@ -87,10 +87,14 @@ class BuildsEmailService < Service ...@@ -87,10 +87,14 @@ class BuildsEmailService < Service
end end
def all_recipients(data) def all_recipients(data)
all_recipients = recipients.split(',').compact.reject(&:blank?) all_recipients = []
unless recipients.blank?
all_recipients += recipients.split(',').compact.reject(&:blank?)
end
if add_pusher? && data[:user][:email] if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}" all_recipients << data[:user][:email]
end end
all_recipients all_recipients
......
...@@ -85,13 +85,15 @@ class TeamcityService < CiService ...@@ -85,13 +85,15 @@ class TeamcityService < CiService
end end
def build_info(sha) def build_info(sha)
url = URI.parse("#{teamcity_url}/httpAuth/app/rest/builds/"\ url = URI.join(
"branch:unspecified:any,number:#{sha}") teamcity_url,
"/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
).to_s
auth = { auth = {
username: username, username: username,
password: password, password: password
} }
@response = HTTParty.get("#{url}", verify: false, basic_auth: auth) @response = HTTParty.get(url, verify: false, basic_auth: auth)
end end
def build_page(sha, ref) def build_page(sha, ref)
...@@ -100,12 +102,14 @@ class TeamcityService < CiService ...@@ -100,12 +102,14 @@ class TeamcityService < CiService
if @response.code != 200 if @response.code != 200
# If actual build link can't be determined, # If actual build link can't be determined,
# send user to build summary page. # send user to build summary page.
"#{teamcity_url}/viewLog.html?buildTypeId=#{build_type}" URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
else else
# If actual build link is available, go to build result page. # If actual build link is available, go to build result page.
built_id = @response['build']['id'] built_id = @response['build']['id']
"#{teamcity_url}/viewLog.html?buildId=#{built_id}"\ URI.join(
"&buildTypeId=#{build_type}" teamcity_url,
"/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
).to_s
end end
end end
...@@ -140,12 +144,13 @@ class TeamcityService < CiService ...@@ -140,12 +144,13 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref]) branch = Gitlab::Git.ref_name(data[:ref])
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", self.class.post(
body: "<build branchName=\"#{branch}\">"\ URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
"<buildType id=\"#{build_type}\"/>"\ body: "<build branchName=\"#{branch}\">"\
'</build>', "<buildType id=\"#{build_type}\"/>"\
headers: { 'Content-type' => 'application/xml' }, '</build>',
basic_auth: auth headers: { 'Content-type' => 'application/xml' },
) basic_auth: auth
)
end end
end end
...@@ -307,6 +307,8 @@ class Repository ...@@ -307,6 +307,8 @@ class Repository
# This ensures this particular cache is flushed after the first commit to a # This ensures this particular cache is flushed after the first commit to a
# new repository. # new repository.
expire_emptiness_caches if empty? expire_emptiness_caches if empty?
expire_branch_count_cache
expire_tag_count_cache
end end
def expire_branch_cache(branch_name = nil) def expire_branch_cache(branch_name = nil)
...@@ -1075,9 +1077,9 @@ class Repository ...@@ -1075,9 +1077,9 @@ class Repository
end end
def main_language def main_language
unless empty? return if empty? || rugged.head_unborn?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end Linguist::Repository.new(rugged, rugged.head.target_id).language
end end
def avatar def avatar
......
...@@ -145,6 +145,7 @@ class User < ActiveRecord::Base ...@@ -145,6 +145,7 @@ class User < ActiveRecord::Base
has_many :spam_logs, dependent: :destroy has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
# #
# Validations # Validations
...@@ -159,7 +160,7 @@ class User < ActiveRecord::Base ...@@ -159,7 +160,7 @@ class User < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validates :notification_level, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? } validate :unique_email, if: ->(user) { user.email_changed? }
...@@ -192,6 +193,13 @@ class User < ActiveRecord::Base ...@@ -192,6 +193,13 @@ class User < ActiveRecord::Base
# Note: When adding an option, it MUST go on the end of the array. # Note: When adding an option, it MUST go on the end of the array.
enum project_view: [:readme, :activity, :files] enum project_view: [:readme, :activity, :files]
# Notification level
# Note: When adding an option, it MUST go on the end of the array.
#
# TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813
# Because user.notification_disabled? is much better than user.disabled?
enum notification_level: [:disabled, :participating, :watch, :global, :mention]
alias_attribute :private_token, :authentication_token alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
...@@ -381,10 +389,6 @@ class User < ActiveRecord::Base ...@@ -381,10 +389,6 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}" "#{self.class.reference_prefix}#{username}"
end end
def notification
@notification ||= Notification.new(self)
end
def generate_password def generate_password
if self.force_random_password if self.force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(8) self.password = self.password_confirmation = Devise.friendly_token.first(8)
...@@ -863,6 +867,10 @@ class User < ActiveRecord::Base ...@@ -863,6 +867,10 @@ class User < ActiveRecord::Base
end end
end end
def notification_settings_for(source)
notification_settings.find_or_initialize_by(source: source)
end
private private
def projects_union def projects_union
......
...@@ -3,7 +3,7 @@ module Issues ...@@ -3,7 +3,7 @@ module Issues
def hook_data(issue, action) def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user) issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) issue_url = Gitlab::UrlBuilder.build(issue)
issue_data[:object_attributes].merge!(url: issue_url, action: action) issue_data[:object_attributes].merge!(url: issue_url, action: action)
issue_data issue_data
end end
......
...@@ -20,8 +20,7 @@ module MergeRequests ...@@ -20,8 +20,7 @@ module MergeRequests
def hook_data(merge_request, action) def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user) hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
hook_data[:object_attributes][:url] = merge_request_url
hook_data[:object_attributes][:action] = action hook_data[:object_attributes][:action] = action
hook_data hook_data
end end
......
module Notes
class DeleteService < BaseService
def execute(note)
note.destroy
note.reset_events_cache
end
end
end
...@@ -253,8 +253,8 @@ class NotificationService ...@@ -253,8 +253,8 @@ class NotificationService
def project_watchers(project) def project_watchers(project)
project_members = project_member_notification(project) project_members = project_member_notification(project)
users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) users_with_project_level_global = project_member_notification(project, :global)
users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) users_with_group_level_global = group_member_notification(project, :global)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
...@@ -264,18 +264,16 @@ class NotificationService ...@@ -264,18 +264,16 @@ class NotificationService
end end
def project_member_notification(project, notification_level=nil) def project_member_notification(project, notification_level=nil)
project_members = project.project_members
if notification_level if notification_level
project_members.where(notification_level: notification_level).pluck(:user_id) project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
else else
project_members.pluck(:user_id) project.notification_settings.pluck(:user_id)
end end
end end
def group_member_notification(project, notification_level) def group_member_notification(project, notification_level)
if project.group if project.group
project.group.group_members.where(notification_level: notification_level).pluck(:user_id) project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
else else
[] []
end end
...@@ -284,13 +282,13 @@ class NotificationService ...@@ -284,13 +282,13 @@ class NotificationService
def users_with_global_level_watch(ids) def users_with_global_level_watch(ids)
User.where( User.where(
id: ids, id: ids,
notification_level: Notification::N_WATCH notification_level: NotificationSetting.levels[:watch]
).pluck(:id) ).pluck(:id)
end end
# Build a list of users based on project notifcation settings # Build a list of users based on project notifcation settings
def select_project_member_setting(project, global_setting, users_global_level_watch) def select_project_member_setting(project, global_setting, users_global_level_watch)
users = project_member_notification(project, Notification::N_WATCH) users = project_member_notification(project, :watch)
# If project setting is global, add to watch list if global setting is watch # If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id| global_setting.each do |user_id|
...@@ -304,7 +302,7 @@ class NotificationService ...@@ -304,7 +302,7 @@ class NotificationService
# Build a list of users based on group notification settings # Build a list of users based on group notification settings
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
uids = group_member_notification(project, Notification::N_WATCH) uids = group_member_notification(project, :watch)
# Group setting is watch, add to users list if user is not project member # Group setting is watch, add to users list if user is not project member
users = [] users = []
...@@ -331,40 +329,46 @@ class NotificationService ...@@ -331,40 +329,46 @@ class NotificationService
# Remove users with disabled notifications from array # Remove users with disabled notifications from array
# Also remove duplications and nil recipients # Also remove duplications and nil recipients
def reject_muted_users(users, project = nil) def reject_muted_users(users, project = nil)
reject_users(users, :disabled?, project) reject_users(users, :disabled, project)
end end
# Remove users with notification level 'Mentioned' # Remove users with notification level 'Mentioned'
def reject_mention_users(users, project = nil) def reject_mention_users(users, project = nil)
reject_users(users, :mention?, project) reject_users(users, :mention, project)
end end
# Reject users which method_name from notification object returns true. # Reject users which has certain notification level
# #
# Example: # Example:
# reject_users(users, :watch?, project) # reject_users(users, :watch, project)
# #
def reject_users(users, method_name, project = nil) def reject_users(users, level, project = nil)
level = level.to_s
unless NotificationSetting.levels.keys.include?(level)
raise 'Invalid notification level'
end
users = users.to_a.compact.uniq users = users.to_a.compact.uniq
users = users.reject(&:blocked?) users = users.reject(&:blocked?)
users.reject do |user| users.reject do |user|
next user.notification.send(method_name) unless project next user.notification_level == level unless project
member = project.project_members.find_by(user_id: user.id) setting = user.notification_settings_for(project)
if !member && project.group if !setting && project.group
member = project.group.group_members.find_by(user_id: user.id) setting = user.notification_settings_for(project.group)
end end
# reject users who globally set mention notification and has no membership # reject users who globally set mention notification and has no setting per project/group
next user.notification.send(method_name) unless member next user.notification_level == level unless setting
# reject users who set mention notification in project # reject users who set mention notification in project
next true if member.notification.send(method_name) next true if setting.level == level
# reject users who have N_MENTION in project and disabled in global settings # reject users who have mention level in project and disabled in global settings
member.notification.global? && user.notification.send(method_name) setting.global? && user.notification_level == level
end end
end end
......
...@@ -26,7 +26,9 @@ module Projects ...@@ -26,7 +26,9 @@ module Projects
GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
ensure ensure
@project.update_column(:pushes_since_gc, 0) Gitlab::Metrics.measure(:reset_pushes_since_gc) do
@project.update_column(:pushes_since_gc, 0)
end
end end
def needed? def needed?
...@@ -34,14 +36,18 @@ module Projects ...@@ -34,14 +36,18 @@ module Projects
end end
def increment! def increment!
@project.increment!(:pushes_since_gc) Gitlab::Metrics.measure(:increment_pushes_since_gc) do
@project.increment!(:pushes_since_gc)
end
end end
private private
def try_obtain_lease def try_obtain_lease
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease.try_obtain lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end end
end end
end end
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
This will create milestone in every selected project This will create milestone in every selected project
%hr %hr
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| = form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row .row
- if @milestone.errors.any? - if @milestone.errors.any?
#error_explanation #error_explanation
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
.clearfix .clearfix
.error-alert .error-alert
.form-group .form-group
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render 'layouts/search' = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm.visible-xs %li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
...@@ -45,6 +45,8 @@ ...@@ -45,6 +45,8 @@
%h1.title= title %h1.title= title
= yield :header_content
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
- if @project && !@project.empty_repo? - if @project && !@project.empty_repo?
......
...@@ -17,4 +17,12 @@ ...@@ -17,4 +17,12 @@
- content_for :scripts_body do - content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user = render "layouts/init_auto_complete" if current_user
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
= dropdown_title("Go to a project")
= dropdown_filter("Search your projects")
= dropdown_content
= dropdown_loading
= render template: "layouts/application" = render template: "layouts/application"
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
= notification_icon(current_user.notification_level)
- else
= notification_icon(setting.level)
%span.str-truncated
= link_to group.name, group_path(group)
.pull-right
= form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if setting.global?
= notification_icon(current_user.notification_level)
- else
= notification_icon(setting.level)
%span.str-truncated
= link_to_project(project)
.pull-right
= form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
= f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- if notification.global?
= notification_icon(@notification)
- else
= notification_icon(notification)
%span.str-truncated
- if membership.kind_of? GroupMember
= link_to membership.group.name, membership.group
- else
= link_to_project(membership.project)
.pull-right
= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
= hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type')
= hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id')
= select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit'
- page_title "Notifications" - page_title "Notifications"
- header_title page_title, profile_notifications_path - header_title page_title, profile_notifications_path
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| %div
= form_errors(@user) - if @user.errors.any?
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
= hidden_field_tag :notification_type, 'global' = hidden_field_tag :notification_type, 'global'
.row .row
...@@ -16,56 +20,55 @@ ...@@ -16,56 +20,55 @@
.col-lg-9 .col-lg-9
%h5 %h5
Global notification settings Global notification settings
.form-group
= f.label :notification_email, class: "label-light"
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
.form-group
= f.label :notification_level, class: 'label-light'
.radio
= f.label :notification_level, value: Notification::N_DISABLED do
= f.radio_button :notification_level, Notification::N_DISABLED
.level-title
Disabled
%p You will not get any notifications via email
.radio = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
= f.label :notification_level, value: Notification::N_MENTION do .form-group
= f.radio_button :notification_level, Notification::N_MENTION = f.label :notification_email, class: "label-light"
.level-title = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
On Mention .form-group
%p You will receive notifications only for comments in which you were @mentioned = f.label :notification_level, class: 'label-light'
.radio
= f.label :notification_level, value: :disabled do
= f.radio_button :notification_level, :disabled
.level-title
Disabled
%p You will not get any notifications via email
.radio .radio
= f.label :notification_level, value: Notification::N_PARTICIPATING do = f.label :notification_level, value: :mention do
= f.radio_button :notification_level, Notification::N_PARTICIPATING = f.radio_button :notification_level, :mention
.level-title .level-title
Participating On Mention
%p You will only receive notifications from related resources (e.g. from your commits or assigned issues) %p You will receive notifications only for comments in which you were @mentioned
.radio .radio
= f.label :notification_level, value: Notification::N_WATCH do = f.label :notification_level, value: :participating do
= f.radio_button :notification_level, Notification::N_WATCH = f.radio_button :notification_level, :participating
.level-title .level-title
Watch Participating
%p You will receive notifications for any activity %p You will only receive notifications from related resources (e.g. from your commits or assigned issues)
.prepend-top-default .radio
= f.submit 'Update settings', class: "btn btn-create" = f.label :notification_level, value: :watch do
= f.radio_button :notification_level, :watch
.level-title
Watch
%p You will receive notifications for any activity
.prepend-top-default
= f.submit 'Update settings', class: "btn btn-create"
%hr %hr
.col-lg-9.col-lg-push-3 %h5
%h5 Groups (#{@group_notifications.count})
Groups (#{@group_members.count}) %div
%div %ul.bordered-list
%ul.bordered-list - @group_notifications.each do |setting|
- @group_members.each do |group_member| = render 'group_settings', setting: setting, group: setting.source
- notification = Notification.new(group_member) %h5
= render 'settings', type: 'group', membership: group_member, notification: notification Projects (#{@project_notifications.count})
%h5 %p.account-well
Projects (#{@project_members.count}) To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there.
%p.account-well .append-bottom-default
To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. %ul.bordered-list
.append-bottom-default - @project_notifications.each do |setting|
%ul.bordered-list = render 'project_settings', setting: setting, project: setting.source
- @project_members.each do |project_member|
- notification = Notification.new(project_member)
= render 'settings', type: 'project', membership: project_member, notification: notification
- if @saved
:plain
new Flash("Notification settings saved", "notice")
- else
:plain
new Flash("Failed to save new settings", "alert")
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
= f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." = f.text_area attr, class: classes, placeholder: placeholder
- else - else
= text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." = text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress') = icon('compress')
- case @membership - if @notification_setting
- when ProjectMember = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do = f.hidden_field :level
= hidden_field_tag :notification_type, 'project'
= hidden_field_tag :notification_id, @membership.id
= hidden_field_tag :notification_level
%span.dropdown %span.dropdown
%a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
= icon('bell') = icon('bell')
= notification_label(@membership) = notification_title(@notification_setting.level)
= icon('angle-down') = icon('angle-down')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- Notification.project_notification_levels.each do |level| - NotificationSetting.levels.each do |level|
= notification_list_item(level, @membership) = notification_list_item(level.first, @notification_setting)
- when GroupMember
.btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
...@@ -14,11 +14,11 @@ ...@@ -14,11 +14,11 @@
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.parallel.match= left[:text] %td.line_content.parallel.match= left[:text]
- else - else
%td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]}"} %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"}
= link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- if @comments_allowed && can?(current_user, :create_note, @project) - if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(left[:line_code], 'old') = link_to_new_diff_note(left[:line_code], 'old')
%td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text])
- if right[:type] == 'new' - if right[:type] == 'new'
- new_line_class = 'new' - new_line_class = 'new'
...@@ -27,11 +27,11 @@ ...@@ -27,11 +27,11 @@
- new_line_class = nil - new_line_class = nil
- new_line_code = left[:line_code] - new_line_code = left[:line_code]
%td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class}", data: { linenumber: right[:number] }} %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }}
= link_to raw(right[:number]), "##{new_line_code}", id: new_line_code = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- if @comments_allowed && can?(current_user, :create_note, @project) - if @comments_allowed && can?(current_user, :create_note, @project)
= link_to_new_diff_note(right[:line_code], 'new') = link_to_new_diff_note(right[:line_code], 'new')
%td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text])
- if @reply_allowed - if @reply_allowed
- comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code])
......
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue = render 'shared/issuable/form', f: f, issuable: @issue
:javascript :javascript
......
...@@ -63,9 +63,11 @@ ...@@ -63,9 +63,11 @@
= @issue.description = @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
.merge-requests #merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)}
= render 'merge_requests' // This element is filled in using JavaScript.
= render 'related_branches'
#related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)}
// This element is filled in using JavaScript.
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch' = render 'new_branch'
......
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request = render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript :javascript
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%span.pull-right %span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request) = link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr %hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request = render 'shared/issuable/form', f: f, issuable: @merge_request
= f.hidden_field :source_project_id = f.hidden_field :source_project_id
= f.hidden_field :source_branch = f.hidden_field :source_branch
......
...@@ -8,20 +8,22 @@ ...@@ -8,20 +8,22 @@
= render 'projects/merge_requests/widget/locked' = render 'projects/merge_requests/widget/locked'
:javascript :javascript
var merge_request_widget;
var opts = { var opts = {
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"}, check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "", ci_status: "",
ci_message: "Build {{status}} for \"{{title}}\"", ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
preparing: "{{status}} build for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"}, ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
preparing: "{{status}} build",
normal: "Build {{status}}"
},
builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
}; };
if(typeof merge_request_widget === 'undefined') { merge_request_widget = new MergeRequestWidget(opts);
merge_request_widget = new MergeRequestWidget(opts);
} else {
merge_request_widget.setOpts(opts);
}
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
= form_errors(@milestone) = form_errors(@milestone)
.row .row
.col-md-6 .col-md-6
.form-group .form-group
...@@ -11,7 +10,7 @@ ...@@ -11,7 +10,7 @@
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
- if current_user.can_select_namespace? - if current_user.can_select_namespace?
.input-group-addon .input-group-addon
= root_url = root_url
= f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1} = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
.input-group-addon .input-group-addon
\/ \/
- else - else
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f|
= note_target_fields(note) = note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.note-form-actions.clearfix .note-form-actions.clearfix
......
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view = hidden_field_tag :view, diff_view
= hidden_field_tag :line_type = hidden_field_tag :line_type
= note_target_fields(@note) = note_target_fields(@note)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
......
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
= "#{note.author.to_reference} commented" = "#{note.author.to_reference} commented"
%a{ href: "##{dom_id(note)}" } %a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- if note_editable?(note) .note-actions
.note-actions - access = note.project.team.human_max_access(note.author.id)
- access = note.project.team.human_max_access(note.author.id) - if access
- if access %span.note-role
%span.note-role = access
= access - if note_editable?(note)
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
......
...@@ -9,11 +9,11 @@ ...@@ -9,11 +9,11 @@
%strong #{@tag.name} %strong #{@tag.name}
.prepend-top-default .prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f| = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control' = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
.form-actions.prepend-top-default .form-actions.prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save' = f.submit 'Save changes', class: 'btn btn-save'
= link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
New Tag New Tag
%hr %hr
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do
.form-group .form-group
= label_tag :tag_name, nil, class: 'control-label' = label_tag :tag_name, nil, class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'description form-control' = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints' = render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions .form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel'
......
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page) = form_errors(@page)
= f.hidden_field :title, value: @page.title = f.hidden_field :title, value: @page.title
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= f.label :content, class: 'control-label' = f.label :content, class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control' = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
......
- project = note.project - project = note.project
- note_url = Gitlab::UrlBuilder.new(:note).build(note.id) - note_url = Gitlab::UrlBuilder.build(note)
- noteable_identifier = note.noteable.try(:iid) || note.noteable.id - noteable_identifier = note.noteable.try(:iid) || note.noteable.id
.search-result-row .search-result-row
%h5.note-search-caption.str-truncated %h5.note-search-caption.str-truncated
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.commit-message-container .commit-message-container
.max-width-marker .max-width-marker
= text_area_tag 'commit_message', = text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]), (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3), required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}" id: "commit_message-#{nonce}"
......
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