Commit 0115ad66 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/issue-move

* master: (121 commits)
  Dedupe labels in labels selector in Dashboard pages
  Refactor colors and lists
  Add a safeguard in MergeRequest#compute_diverged_commits_count
  Fix an issue when the target branch of a MR had been deleted
  Add avatar to issue and MR pages header
  Cleanup somce css colors
  Re-group scss variables
  Refactor `Todo#target`
  Fixes issue with filter label missing on labels & milestones
  Rename `Todo#to_reference` to `Todo#target_reference`
  Fixed failing tests
  Updated controller with before_action Fixed other issues based on feedback
  Fixes issue on dashboard issues
  Full labels data in JSON
  Fixed issue with labels dropdown getting wrong labels
  Update CHANGELOG
  Use `Note#for_project_snippet?` to skip notes on project snippet
  Use `Commit#short_id` instead of `Commit.truncate_sha`
  Reuse `for_commit?` on conditional validations
  Update schema info comment on todo related files
  ...

Conflicts:
	app/models/issue.rb
	db/schema.rb
	spec/models/issue_spec.rb
parents 9b13ce0b 4f0302f0
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.6.0 (unreleased) v 8.6.0 (unreleased)
- Add confidential issues
- Bump gitlab_git to 9.0.3 (Stan Hu) - Bump gitlab_git to 9.0.3 (Stan Hu)
- Support Golang subpackage fetching (Stan Hu) - Support Golang subpackage fetching (Stan Hu)
- Bump Capybara gem to 2.6.2 (Stan Hu) - Bump Capybara gem to 2.6.2 (Stan Hu)
- New branch button appears on issues where applicable
- Contributions to forked projects are included in calendar - Contributions to forked projects are included in calendar
- Improve the formatting for the user page bio (Connor Shea) - Improve the formatting for the user page bio (Connor Shea)
- Removed the default password from the initial admin account created during - Removed the default password from the initial admin account created during
...@@ -13,6 +15,7 @@ v 8.6.0 (unreleased) ...@@ -13,6 +15,7 @@ v 8.6.0 (unreleased)
- Add support for wiki with UTF-8 page names (Hiroyuki Sato) - Add support for wiki with UTF-8 page names (Hiroyuki Sato)
- Fix wiki search results point to raw source (Hiroyuki Sato) - Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room - Don't load all of GitLab in mail_room
- HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
- Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie)
...@@ -24,6 +27,7 @@ v 8.6.0 (unreleased) ...@@ -24,6 +27,7 @@ v 8.6.0 (unreleased)
- Rewrite logo to simplify SVG code (Sean Lang) - Rewrite logo to simplify SVG code (Sean Lang)
- Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach) - Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach)
- Ignore jobs that start with `.` (hidden jobs) - Ignore jobs that start with `.` (hidden jobs)
- Hide builds from project's settings when the feature is disabled
- Allow to pass name of created artifacts archive in `.gitlab-ci.yml` - Allow to pass name of created artifacts archive in `.gitlab-ci.yml`
- Refactor and greatly improve search performance - Refactor and greatly improve search performance
- Add support for cross-project label references - Add support for cross-project label references
...@@ -35,14 +39,23 @@ v 8.6.0 (unreleased) ...@@ -35,14 +39,23 @@ v 8.6.0 (unreleased)
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view - Don't show Issues/MRs from archived projects in Groups view
- Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs - Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
- Fix empty source_sha on Merge Request when there is no diff (Pierre de La Morinerie)
- Increase the notes polling timeout over time (Roberto Dip) - Increase the notes polling timeout over time (Roberto Dip)
- Add shortcut to toggle markdown preview (Florent Baldino) - Add shortcut to toggle markdown preview (Florent Baldino)
- Show labels in dashboard and group milestone views - Show labels in dashboard and group milestone views
- Fix an issue when the target branch of a MR had been deleted
- Add main language of a project in the list of projects (Tiago Botelho) - Add main language of a project in the list of projects (Tiago Botelho)
- Add #upcoming filter to Milestone filter (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages - Add ability to show archived projects on dashboard, explore and group pages
- Move group activity to separate page - Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted
- Continue parameters are checked to ensure redirection goes to the same instance - Continue parameters are checked to ensure redirection goes to the same instance
- User deletion is now done in the background so the request can not time out - User deletion is now done in the background so the request can not time out
- Canceled builds are now ignored in compound build status if marked as `allowed to fail`
- Trigger a todo for mentions on commits page
v 8.5.8
- Bump Git version requirement to 2.7.4
v 8.5.7 v 8.5.7
- Bump Git version requirement to 2.7.3 - Bump Git version requirement to 2.7.3
...@@ -55,7 +68,6 @@ v 8.5.5 ...@@ -55,7 +68,6 @@ v 8.5.5
- Prevent a 500 error in Todos when author was removed - Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages - Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior - Fix "Show all" link behavior
- Add #upcoming filter to Milestone filter (Tiago Botelho)
v 8.5.4 v 8.5.4
- Do not cache requests for badges (including builds badge) - Do not cache requests for badges (including builds badge)
......
...@@ -51,7 +51,7 @@ gem "browser", '~> 1.0.0' ...@@ -51,7 +51,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem "gitlab_git", '~> 9.0' gem "gitlab_git", '~> 10.0'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
......
...@@ -359,11 +359,11 @@ GEM ...@@ -359,11 +359,11 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.3) gitlab_git (10.0.0)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
rugged (~> 0.24.0b13) rugged (~> 0.24.0)
gitlab_meta (7.0) gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1) gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9) net-ldap (~> 0.9)
...@@ -942,7 +942,7 @@ DEPENDENCIES ...@@ -942,7 +942,7 @@ DEPENDENCIES
github-markup (~> 1.3.1) github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0) gitlab_emoji (~> 0.3.0)
gitlab_git (~> 9.0) gitlab_git (~> 10.0)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0) gollum-lib (~> 4.1.0)
......
...@@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software: ...@@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL - Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1 - Ruby (MRI) 2.1
- Git 2.7.3+ - Git 2.7.4+
- Redis 2.8+ - Redis 2.8+
- MySQL or PostgreSQL - MySQL or PostgreSQL
......
...@@ -14,7 +14,6 @@ class Dispatcher ...@@ -14,7 +14,6 @@ class Dispatcher
path = page.split(':') path = page.split(':')
shortcut_handler = null shortcut_handler = null
switch page switch page
when 'projects:issues:index' when 'projects:issues:index'
Issues.init() Issues.init()
...@@ -25,6 +24,8 @@ class Dispatcher ...@@ -25,6 +24,8 @@ class Dispatcher
new ZenMode() new ZenMode()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index'
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 DropzoneInput($('.milestone-form'))
......
...@@ -246,11 +246,15 @@ class GitLabDropdown ...@@ -246,11 +246,15 @@ class GitLabDropdown
if oldValue if oldValue
value = "#{oldValue},#{value}" value = "#{oldValue},#{value}"
else else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.toggleClass "is-active" el.toggleClass "is-active"
# Toggle the dropdown label
if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value? if value?
if !field.length if !field.length
# Create hidden input for form # Create hidden input for form
......
...@@ -7,6 +7,7 @@ class @Issue ...@@ -7,6 +7,7 @@ class @Issue
# Prevent duplicate event bindings # Prevent duplicate event bindings
@disableTaskList() @disableTaskList()
@fixAffixScroll() @fixAffixScroll()
@initParticipants()
if $('a.btn-close').length if $('a.btn-close').length
@initTaskList() @initTaskList()
@initIssueBtnEventListeners() @initIssueBtnEventListeners()
...@@ -84,3 +85,27 @@ class @Issue ...@@ -84,3 +85,27 @@ class @Issue
type: 'PATCH' type: 'PATCH'
url: $('form.js-issuable-update').attr('action') url: $('form.js-issuable-update').attr('action')
data: patchData data: patchData
initParticipants: ->
_this = @
$(document).on "click", ".js-participants-more", @toggleHiddenParticipants
$(".js-participants-author").each (i) ->
if i >= _this.PARTICIPANTS_ROW_COUNT
$(@)
.addClass "js-participants-hidden"
.hide()
toggleHiddenParticipants: (e) ->
e.preventDefault()
currentText = $(this).text().trim()
lessText = $(this).data("less-text")
originalText = $(this).data("original-text")
if currentText is originalText
$(this).text(lessText)
else
$(this).text(originalText)
$(".js-participants-hidden").toggle()
...@@ -41,24 +41,28 @@ ...@@ -41,24 +41,28 @@
@timer = null @timer = null
$("#issue_search").keyup -> $("#issue_search").keyup ->
clearTimeout(@timer) clearTimeout(@timer)
@timer = setTimeout(Issues.filterResults, 500) @timer = setTimeout( ->
Issues.filterResults $("#issue_search_form")
, 500)
filterResults: => filterResults: (form) =>
form = $("#issue_search_form") $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
search = $("#issue_search").val() formAction = form.attr('action')
$('.issues-holder').css("opacity", '0.5') formData = form.serialize()
issues_url = form.attr('action') + '?' + form.serialize() issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax $.ajax
type: "GET" type: "GET"
url: form.attr('action') url: formAction
data: form.serialize() data: formData
complete: -> complete: ->
$('.issues-holder').css("opacity", '1.0') $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) -> success: (data) ->
$('.issues-holder').html(data.html) $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
history.replaceState {page: issues_url}, document.title, issues_url history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload() Issues.reload()
dataType: "json" dataType: "json"
......
class @LabelsSelect class @LabelsSelect
constructor: -> constructor: ->
$('.js-label-select').each (i, dropdown) -> $('.js-label-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id') $dropdown = $(dropdown)
labelUrl = $(dropdown).data("labels") projectId = $dropdown.data('project-id')
selectedLabel = $(dropdown).data('selected') labelUrl = $dropdown.data('labels')
selectedLabel = $dropdown.data('selected')
if selectedLabel if selectedLabel
selectedLabel = selectedLabel.split(",") selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
showNo = $(dropdown).data('show-no') showNo = $dropdown.data('show-no')
showAny = $(dropdown).data('show-any') showAny = $dropdown.data('show-any')
defaultLabel = $dropdown.data('default-label')
if newLabelField.length if newLabelField.length
$('.suggest-colors-dropdown a').on "click", (e) -> $('.suggest-colors-dropdown a').on 'click', (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
newColorField.val $(this).data("color") newColorField.val $(this).data('color')
$('.js-dropdown-label-color-preview') $('.js-dropdown-label-color-preview')
.css 'background-color', $(this).data("color") .css 'background-color', $(this).data('color')
.addClass 'is-active' .addClass 'is-active'
$('.js-new-label-btn').on "click", (e) -> $('.js-new-label-btn').on 'click', (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
if newLabelField.val() isnt "" && newColorField.val() isnt "" if newLabelField.val() isnt '' and newColorField.val() isnt ''
$('.js-new-label-btn').disable() $('.js-new-label-btn').disable()
# Create new label with API # Create new label with API
...@@ -33,46 +35,38 @@ class @LabelsSelect ...@@ -33,46 +35,38 @@ class @LabelsSelect
color: newColorField.val() color: newColorField.val()
}, (label) -> }, (label) ->
$('.js-new-label-btn').enable() $('.js-new-label-btn').enable()
$('.dropdown-menu-back', $(dropdown).parent()).trigger "click" $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
$(dropdown).glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
# We have to fetch the JS version of the labels list because there is no
# public facing JSON url for labels
$.ajax( $.ajax(
url: labelUrl url: labelUrl
).done (data) -> ).done (data) ->
html = $(data)
data = []
html.find('.label-row a').each ->
data.push(
title: $(@).text().trim()
)
if showNo if showNo
data.unshift( data.unshift(
id: "0" id: 0
title: 'No label' title: 'No Label'
) )
if showAny if showAny
data.unshift( data.unshift(
title: 'Any label' isAny: true
title: 'Any Label'
) )
if data.length > 2 if data.length > 2
data.splice 2, 0, "divider" data.splice 2, 0, 'divider'
callback data callback data
renderRow: (label) -> renderRow: (label) ->
if $.isArray(selectedLabel) if $.isArray(selectedLabel)
selected = "" selected = ''
$.each selectedLabel, (i, selectedLbl) -> $.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim() selectedLbl = selectedLbl.trim()
if selected is "" && label.title is selectedLbl if selected is '' and label.title is selectedLbl
selected = "is-active" selected = 'is-active'
else else
selected = if label.title is selectedLabel then "is-active" else "" selected = if label.title is selectedLabel then 'is-active' else ''
"<li> "<li>
<a href='#' class='#{selected}'> <a href='#' class='#{selected}'>
...@@ -83,10 +77,24 @@ class @LabelsSelect ...@@ -83,10 +77,24 @@ class @LabelsSelect
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
fieldName: $(dropdown).data('field-name') toggleLabel: (selected) ->
if selected and selected.title isnt 'Any Label'
selected.title
else
defaultLabel
fieldName: $dropdown.data('field-name')
id: (label) -> id: (label) ->
if label.isAny?
''
else
label.title label.title
clicked: -> clicked: ->
if $(dropdown).hasClass "js-filter-submit" page = $('body').data 'page'
$(dropdown).parents('form').submit() isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
) )
class @MilestoneSelect class @MilestoneSelect
constructor: -> constructor: ->
$('.js-milestone-select').each (i, dropdown) -> $('.js-milestone-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id') $dropdown = $(dropdown)
milestonesUrl = $(dropdown).data('milestones') projectId = $dropdown.data('project-id')
selectedMilestone = $(dropdown).data('selected') milestonesUrl = $dropdown.data('milestones')
showNo = $(dropdown).data('show-no') selectedMilestone = $dropdown.data('selected')
showAny = $(dropdown).data('show-any') showNo = $dropdown.data('show-no')
useId = $(dropdown).data('use-id') showAny = $dropdown.data('show-any')
useId = $dropdown.data('use-id')
defaultLabel = $dropdown.data('default-label')
$(dropdown).glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
url: milestonesUrl url: milestonesUrl
).done (data) -> ).done (data) ->
html = $(data)
data = []
html.find('.milestone strong a').each ->
link = $(@).attr("href").split("/")
data.push(
id: link[link.length - 1]
title: $(@).text().trim()
)
if showNo if showNo
data.unshift( data.unshift(
id: "0" id: '0'
title: 'No Milestone' title: 'No Milestone'
) )
if showAny if showAny
data.unshift( data.unshift(
isAny: true
title: 'Any Milestone' title: 'Any Milestone'
) )
if data.length > 2 if data.length > 2
data.splice 2, 0, "divider" data.splice 2, 0, 'divider'
callback(data) callback(data)
filterable: true filterable: true
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
fieldName: $(dropdown).data('field-name') toggleLabel: (selected) ->
if selected && 'id' of selected
selected.title
else
defaultLabel
fieldName: $dropdown.data('field-name')
text: (milestone) -> text: (milestone) ->
milestone.title milestone.title
id: (milestone) -> id: (milestone) ->
if !useId if !useId
if milestone.title isnt "Any milestone" if !milestone.isAny?
milestone.title milestone.title
else else
"" ''
else else
milestone.id milestone.id
isSelected: (milestone) -> isSelected: (milestone) ->
milestone.title is selectedMilestone milestone.title is selectedMilestone
clicked: -> clicked: ->
if $(dropdown).hasClass "js-filter-submit" page = $('body').data 'page'
$(dropdown).parents('form').submit() isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
) )
...@@ -343,6 +343,7 @@ class @Notes ...@@ -343,6 +343,7 @@ 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()
$html.syntaxHighlight() $html.syntaxHighlight()
$html.find('.js-task-list-container').taskList('enable') $html.find('.js-task-list-container').taskList('enable')
...@@ -626,10 +627,10 @@ class @Notes ...@@ -626,10 +627,10 @@ class @Notes
if closebtn.text() isnt closetext if closebtn.text() isnt closetext
closebtn.text(closetext) closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)') if reopenbtn.is('.btn-comment-and-reopen')
reopenbtn.removeClass('btn-comment-and-reopen') reopenbtn.removeClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)') if closebtn.is('.btn-comment-and-close')
closebtn.removeClass('btn-comment-and-close') closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible') if discardbtn.is(':visible')
......
...@@ -3,3 +3,16 @@ class @ProjectNew ...@@ -3,3 +3,16 @@ class @ProjectNew
$('.project-edit-container').on 'ajax:before', => $('.project-edit-container').on 'ajax:before', =>
$('.project-edit-container').hide() $('.project-edit-container').hide()
$('.save-project-loader').show() $('.save-project-loader').show()
@toggleSettings()
@toggleSettingsOnclick()
toggleSettings: ->
checked = $("#project_builds_enabled").prop("checked")
if checked
$('.builds-feature').show()
else
$('.builds-feature').hide()
toggleSettingsOnclick: ->
$("#project_builds_enabled").on 'click', @toggleSettings
class @Todos
constructor: (@name) ->
@clearListeners()
@initBtnListeners()
clearListeners: ->
$('.done-todo').off('click')
$('.js-todos-mark-all').off('click')
initBtnListeners: ->
$('.done-todo').on('click', @doneClicked)
$('.js-todos-mark-all').on('click', @allDoneClicked)
doneClicked: (e) =>
e.preventDefault()
e.stopImmediatePropagation()
$this = $(e.currentTarget)
$this.disable()
$.ajax
type: 'POST'
url: $this.attr('href')
dataType: 'json'
data: '_method': 'delete'
success: (data) =>
@clearDone $this.closest('li')
@updateBadges data
allDoneClicked: (e) =>
e.preventDefault()
e.stopImmediatePropagation()
$this = $(e.currentTarget)
$this.disable()
$.ajax
type: 'POST'
url: $this.attr('href')
dataType: 'json'
data: '_method': 'delete'
success: (data) =>
$this.remove()
$('.js-todos-list').remove()
@updateBadges data
clearDone: ($row) ->
$ul = $row.closest('ul')
$row.remove()
if not $ul.find('li').length
$ul.parents('.panel').remove()
updateBadges: (data) ->
$('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count
...@@ -4,14 +4,16 @@ class @UsersSelect ...@@ -4,14 +4,16 @@ class @UsersSelect
@userPath = "/autocomplete/users/:id.json" @userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) => $('.js-user-search').each (i, dropdown) =>
@projectId = $(dropdown).data('project-id') $dropdown = $(dropdown)
@showCurrentUser = $(dropdown).data('current-user') @projectId = $dropdown.data('project-id')
showNullUser = $(dropdown).data('null-user') @showCurrentUser = $dropdown.data('current-user')
showAnyUser = $(dropdown).data('any-user') showNullUser = $dropdown.data('null-user')
firstUser = $(dropdown).data('first-user') showAnyUser = $dropdown.data('any-user')
selectedId = $(dropdown).data('selected') firstUser = $dropdown.data('first-user')
selectedId = $dropdown.data('selected')
$(dropdown).glDropdown( defaultLabel = $dropdown.data('default-label')
$dropdown.glDropdown(
data: (term, callback) => data: (term, callback) =>
@users term, (users) => @users term, (users) =>
if term.length is 0 if term.length is 0
...@@ -52,10 +54,21 @@ class @UsersSelect ...@@ -52,10 +54,21 @@ class @UsersSelect
search: search:
fields: ['name', 'username'] fields: ['name', 'username']
selectable: true selectable: true
fieldName: $(dropdown).data('field-name') fieldName: $dropdown.data('field-name')
toggleLabel: (selected) ->
if selected && 'id' of selected
selected.name
else
defaultLabel
clicked: -> clicked: ->
if $(dropdown).hasClass "js-filter-submit" page = $('body').data 'page'
$(dropdown).parents('form').submit() isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
renderRow: (user) -> renderRow: (user) ->
username = if user.username then "@#{user.username}" else "" username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false avatar = if user.avatar_url then user.avatar_url else false
......
...@@ -28,10 +28,6 @@ ...@@ -28,10 +28,6 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
color: $gl-gray; color: $gl-gray;
a {
color: $md-link-color;
}
&.oneline-block { &.oneline-block {
line-height: 42px; line-height: 42px;
} }
......
...@@ -208,3 +208,13 @@ ...@@ -208,3 +208,13 @@
background-color: #e4e7ed !important; background-color: #e4e7ed !important;
} }
} }
.btn-loading {
&:not(.disabled) .fa {
display: none;
}
.fa {
margin-right: 5px;
}
}
...@@ -8,20 +8,20 @@ ...@@ -8,20 +8,20 @@
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; } .prepend-top-5 { margin-top: 5px; }
.prepend-top-10 { margin-top:10px } .prepend-top-10 { margin-top: 10px }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px } .prepend-top-20 { margin-top: 20px }
.prepend-left-10 { margin-left:10px } .prepend-left-10 { margin-left: 10px }
.prepend-left-default { margin-left: $gl-padding; } .prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left:20px } .prepend-left-20 { margin-left: 20px }
.append-right-5 { margin-right: 5px } .append-right-5 { margin-right: 5px }
.append-right-10 { margin-right:10px } .append-right-10 { margin-right: 10px }
.append-right-default { margin-right: $gl-padding; } .append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right:20px } .append-right-20 { margin-right: 20px }
.append-bottom-0 { margin-bottom:0 } .append-bottom-0 { margin-bottom: 0 }
.append-bottom-10 { margin-bottom:10px } .append-bottom-10 { margin-bottom: 10px }
.append-bottom-15 { margin-bottom:15px } .append-bottom-15 { margin-bottom: 15px }
.append-bottom-20 { margin-bottom:20px } .append-bottom-20 { margin-bottom: 20px }
.append-bottom-default { margin-bottom: $gl-padding; } .append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block } .inline { display: inline-block }
.center { text-align: center } .center { text-align: center }
...@@ -134,10 +134,10 @@ p.time { ...@@ -134,10 +134,10 @@ p.time {
// Fix issue with notes & lists creating a bunch of bottom borders. // Fix issue with notes & lists creating a bunch of bottom borders.
li.note { li.note {
img { max-width:100% } img { max-width: 100% }
.note-title { .note-title {
li { li {
border-bottom:none !important; border-bottom: none !important;
} }
} }
} }
......
...@@ -9,6 +9,12 @@ ...@@ -9,6 +9,12 @@
border-left: $caret-width-base solid transparent; border-left: $caret-width-base solid transparent;
} }
.btn-group {
.caret {
margin-left: 0;
}
}
.dropdown { .dropdown {
position: relative; position: relative;
} }
......
...@@ -3,22 +3,11 @@ ...@@ -3,22 +3,11 @@
vertical-align: top; vertical-align: top;
} }
@media (min-width: 800px) { @media (min-width: $screen-sm-min) {
.issues-filters, .issues-filters,
.issues_bulk_update { .issues_bulk_update {
select, .select2-container { .dropdown-menu-toggle {
width: 120px !important; width: 132px;
display: inline-block;
}
}
}
@media (min-width: 1200px) {
.issues-filters,
.issues_bulk_update {
select, .select2-container {
width: 150px !important;
display: inline-block;
} }
} }
} }
...@@ -111,14 +111,17 @@ ul.content-list { ...@@ -111,14 +111,17 @@ ul.content-list {
> li { > li {
border-color: $table-border-color; border-color: $table-border-color;
color: $list-text-color;
font-size: $list-font-size; font-size: $list-font-size;
color: $list-text-color;
.title { .title {
color: $list-title-color;
font-weight: 600; font-weight: 600;
} }
a {
color: $gl-dark-link-color;
}
.description { .description {
p { p {
@include str-truncated; @include str-truncated;
...@@ -141,6 +144,10 @@ ul.content-list { ...@@ -141,6 +144,10 @@ ul.content-list {
} }
} }
.panel > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
ul.controls { ul.controls {
padding-top: 1px; padding-top: 1px;
float: right; float: right;
......
$row-hover: #f4f8fe; /*
$gl-text-color: #54565b; * Layout
$gl-text-green: #4a2; */
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-header-color: #323232;
$gl-link-color: #333c48;
$md-text-color: #444;
$md-link-color: #3084bb;
$progress-color: #c0392b;
$gl-font-size: 15px;
$list-font-size: 15px;
$sidebar_collapsed_width: 62px; $sidebar_collapsed_width: 62px;
$sidebar_width: 230px; $sidebar_width: 230px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 258px; $gutter_inner_width: 258px;
$avatar_radius: 50%;
$code_font_size: 13px; /*
$code_line_height: 1.5; * UI elements
*/
$border-color: #efeff1; $border-color: #efeff1;
$table-border-color: #eef0f2; $table-border-color: #eef0f2;
$background-color: #faf9f9; $background-color: #faf9f9;
$header-height: 58px;
$fixed-layout-width: 1280px; /*
$gl-gray: #5a5a5a; * Text
*/
$gl-font-size: 15px;
$gl-title-color: #333;
$gl-text-color: #555;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3084bb;
$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
$gl-gray: $gl-text-color;
$gl-header-color: $gl-title-color;
/*
* Lists
*/
$list-font-size: $gl-font-size;
$list-title-color: $gl-title-color;
$list-text-color: $gl-text-color;
/*
* Markdown
*/
$md-text-color: $gl-text-color;
$md-link-color: $gl-link-color;
/*
* Code
*/
$code_font_size: 13px;
$code_line_height: 1.5;
/*
* Padding
*/
$gl-padding: 16px; $gl-padding: 16px;
$gl-btn-padding: 10px; $gl-btn-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top:10px; $gl-padding-top: 10px;
/*
* Misc
*/
$row-hover: #f4f8fe;
$progress-color: #c0392b;
$avatar_radius: 50%;
$header-height: 58px;
$fixed-layout-width: 1280px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$secondary-text: #7f8fa4;
$error-exclamation-point: #e62958; $error-exclamation-point: #e62958;
$border-radius-default: 3px; $border-radius-default: 3px;
$list-title-color: #333;
$list-text-color: #555;
$btn-transparent-color: #8f8f8f; $btn-transparent-color: #8f8f8f;
$ssh-key-icon-color: #8f8f8f; $ssh-key-icon-color: #8f8f8f;
$ssh-key-icon-size: 18px; $ssh-key-icon-size: 18px;
$provider-btn-group-border: #e5e5e5; $provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1; $provider-btn-not-active-color: #4688f1;
......
...@@ -55,7 +55,7 @@ li.commit { ...@@ -55,7 +55,7 @@ li.commit {
} }
.commit-row-message { .commit-row-message {
color: $gl-link-color; color: $gl-dark-link-color;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
......
...@@ -11,15 +11,15 @@ ...@@ -11,15 +11,15 @@
} }
.dashboard-search-filter { .dashboard-search-filter {
padding:5px; padding: 5px;
.search-text-input { .search-text-input {
float:left; float: left;
@extend .col-md-2; @extend .col-md-2;
} }
.btn { .btn {
margin-left: 5px; margin-left: 5px;
float:left; float: left;
} }
} }
......
// Common // Common
.diff-file { .diff-file {
border: 1px solid $border-color; border: 1px solid $border-color;
border-top: none; margin-bottom: $gl-padding;
.diff-header { .diff-header {
position: relative; position: relative;
...@@ -361,3 +361,11 @@ ...@@ -361,3 +361,11 @@
border-color: $border; border-color: $border;
} }
} }
.files {
margin-top: -1px;
.diff-file:last-child {
margin-bottom: 0;
}
}
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
border-bottom: 1px solid $table-border-color; border-bottom: 1px solid $table-border-color;
color: #7f8fa4; color: $list-text-color;
&.event-inline { &.event-inline {
.avatar { .avatar {
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
} }
a { a {
color: #4c4e54; color: $gl-dark-link-color;
} }
.avatar { .avatar {
...@@ -31,10 +31,7 @@ ...@@ -31,10 +31,7 @@
.event-title { .event-title {
@include str-truncated(calc(100% - 174px)); @include str-truncated(calc(100% - 174px));
font-weight: 600; font-weight: 600;
color: $list-text-color;
.author_name {
color: #333;
}
} }
.event-body { .event-body {
...@@ -94,7 +91,7 @@ ...@@ -94,7 +91,7 @@
} }
} }
&:last-child { border:none } &:last-child { border: none }
.event_commits { .event_commits {
li { li {
...@@ -138,7 +135,7 @@ ...@@ -138,7 +135,7 @@
@include str-truncated(100%); @include str-truncated(100%);
padding: 5px 0; padding: 5px 0;
font-size: 13px; font-size: 13px;
float:left; float: left;
margin-right: -150px; margin-right: -150px;
padding-right: 150px; padding-right: 150px;
line-height: 20px; line-height: 20px;
......
@media (max-width: $screen-sm-max) {
.issuable-affix {
margin-top: 20px;
}
}
@media (max-width: $screen-md-max) {
.issuable-affix {
position: static;
}
}
@media (min-width: $screen-md-max) {
.issuable-affix {
&.affix-top {
position: static;
}
&.affix {
position: fixed;
top: 70px;
margin-right: 35px;
&.no-affix {
position: relative;
top: 0;
}
}
}
}
.issuable-details { .issuable-details {
section { section {
.issuable-discussion { .issuable-discussion {
...@@ -54,6 +23,10 @@ ...@@ -54,6 +23,10 @@
padding: 6px 10px; padding: 6px 10px;
} }
} }
&.has-labels {
margin-bottom: -5px;
}
} }
.issuable-sidebar { .issuable-sidebar {
...@@ -66,8 +39,9 @@ ...@@ -66,8 +39,9 @@
width: $gutter_inner_width; width: $gutter_inner_width;
// -- // --
&:first-child { &.issuable-sidebar-header {
padding-top: 5px; padding-top: 0;
padding-bottom: 10px;
} }
&:last-child { &:last-child {
...@@ -75,7 +49,6 @@ ...@@ -75,7 +49,6 @@
} }
span { span {
margin-top: 7px;
display: inline-block; display: inline-block;
} }
...@@ -84,7 +57,7 @@ ...@@ -84,7 +57,7 @@
} }
.issuable-count { .issuable-count {
margin-top: 7px;
} }
.gutter-toggle { .gutter-toggle {
...@@ -99,19 +72,19 @@ ...@@ -99,19 +72,19 @@
.title { .title {
color: $gl-text-color; color: $gl-text-color;
margin-bottom: 8px; margin-bottom: 10px;
line-height: 1;
.avatar { .avatar {
margin-left: 0; margin-left: 0;
} }
label {
font-weight: normal;
margin-right: 4px;
}
.edit-link { .edit-link {
color: $gl-gray; color: $gl-gray;
&:hover {
color: $md-link-color;
}
} }
} }
...@@ -144,11 +117,6 @@ ...@@ -144,11 +117,6 @@
.btn-clipboard { .btn-clipboard {
color: $gl-gray; color: $gl-gray;
} }
.participants .avatar {
margin-top: 6px;
margin-right: 2px;
}
} }
.right-sidebar { .right-sidebar {
...@@ -163,8 +131,12 @@ ...@@ -163,8 +131,12 @@
&.right-sidebar-expanded { &.right-sidebar-expanded {
width: $gutter_width; width: $gutter_width;
hr { .value {
display: none; line-height: 1;
}
.bold {
font-weight: 600;
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -172,8 +144,23 @@ ...@@ -172,8 +144,23 @@
} }
.gutter-toggle { .gutter-toggle {
margin-top: 7px;
border-left: 1px solid $border-gray-light; border-left: 1px solid $border-gray-light;
} }
.assignee .avatar {
float: left;
margin-right: 10px;
margin-bottom: 0;
margin-left: 0;
}
.username {
display: block;
margin-top: 4px;
font-size: 13px;
font-weight: normal;
}
} }
.subscribe-button { .subscribe-button {
...@@ -193,14 +180,6 @@ ...@@ -193,14 +180,6 @@
width: $sidebar_collapsed_width; width: $sidebar_collapsed_width;
padding-top: 0; padding-top: 0;
hr {
margin: 0;
color: $gray-normal;
border-color: $gray-normal;
width: 62px;
margin-left: -20px
}
.block { .block {
width: $sidebar_collapsed_width - 1px; width: $sidebar_collapsed_width - 1px;
margin-left: -19px; margin-left: -19px;
...@@ -209,12 +188,18 @@ ...@@ -209,12 +188,18 @@
overflow: hidden; overflow: hidden;
} }
.participants {
border-bottom: 1px solid $border-gray-light;
}
.hide-collapsed { .hide-collapsed {
display: none; display: none;
} }
.gutter-toggle { .gutter-toggle {
margin-left: -36px; width: 100%;
margin-left: 0;
padding-left: 25px;
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -229,6 +214,10 @@ ...@@ -229,6 +214,10 @@
margin-top: 0; margin-top: 0;
} }
.author {
display: none;
}
.btn-clipboard { .btn-clipboard {
border: none; border: none;
...@@ -241,6 +230,11 @@ ...@@ -241,6 +230,11 @@
} }
} }
} }
.sidebar-collapsed-user {
padding-bottom: 0;
margin-bottom: 10px;
}
} }
.btn { .btn {
...@@ -251,6 +245,13 @@ ...@@ -251,6 +245,13 @@
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
} }
} }
a:not(.btn) {
&:hover {
color: $md-link-color;
text-decoration: none;
}
}
} }
.btn-default.gutter-toggle { .btn-default.gutter-toggle {
...@@ -262,3 +263,37 @@ ...@@ -262,3 +263,37 @@
color: $gray-darkest; color: $gray-darkest;
} }
} }
.edited-text {
color: $gray-darkest;
.author_link {
color: $gray-darkest;
}
}
.participants-list {
margin: -5px -5px;
}
.participants-author {
display: inline-block;
padding: 5px 5px;
.author_link {
display: block;
}
.avatar.avatar-inline {
margin: 0;
}
}
.participants-more {
margin-top: 5px;
margin-left: 5px;
a {
color: #8c8c8c;
}
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
padding: 10px $gl-padding; padding: 10px $gl-padding;
position: relative; position: relative;
.issue-title { .title {
margin-bottom: 2px; margin-bottom: 2px;
} }
...@@ -49,7 +49,7 @@ form.edit-issue { ...@@ -49,7 +49,7 @@ form.edit-issue {
margin: 0; margin: 0;
} }
.merge-requests-title { .merge-requests-title, .related-branches-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
...@@ -130,7 +130,7 @@ form.edit-issue { ...@@ -130,7 +130,7 @@ form.edit-issue {
} }
.issue-closed-by-widget { .issue-closed-by-widget {
color: $secondary-text; color: $gl-text-color;
margin-left: 52px; margin-left: 52px;
} }
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
&.middle { &.middle {
border-top: 0; border-top: 0;
margin-bottom:0; margin-bottom: 0;
@include border-radius(0); @include border-radius(0);
} }
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
*/ */
@-webkit-keyframes targe3-note { @-webkit-keyframes targe3-note {
from { background:#fffff0; } from { background: #fffff0; }
50% { background:#ffffd3; } 50% { background: #ffffd3; }
to { background:#fffff0; } to { background: #fffff0; }
} }
ul.notes { ul.notes {
...@@ -93,12 +93,12 @@ ul.notes { ...@@ -93,12 +93,12 @@ ul.notes {
.discussion { .discussion {
overflow: hidden; overflow: hidden;
display: block; display: block;
position:relative; position: relative;
} }
.note { .note {
display: block; display: block;
position:relative; position: relative;
.note-body { .note-body {
overflow: auto; overflow: auto;
...@@ -108,6 +108,13 @@ ul.notes { ...@@ -108,6 +108,13 @@ ul.notes {
word-wrap: break-word; word-wrap: break-word;
@include md-typography; @include md-typography;
// On diffs code should wrap nicely and not overflow
pre {
code {
white-space: pre-wrap;
}
}
// Reset ul style types since we're nested inside a ul already // Reset ul style types since we're nested inside a ul already
& > ul { & > ul {
list-style-type: disc; list-style-type: disc;
......
...@@ -33,6 +33,13 @@ ...@@ -33,6 +33,13 @@
.project-settings-dropdown { .project-settings-dropdown {
margin-left: 10px; margin-left: 10px;
display: inline-block; display: inline-block;
.dropdown-menu {
left: auto;
width: auto;
right: 0px;
max-width: 240px;
}
} }
} }
...@@ -286,11 +293,11 @@ table.table.protected-branches-list tr.no-border { ...@@ -286,11 +293,11 @@ table.table.protected-branches-list tr.no-border {
padding-bottom: 4px; padding-bottom: 4px;
ul.nav { ul.nav {
display:inline-block; display: inline-block;
} }
.nav li { .nav li {
display:inline; display: inline;
} }
.nav > li > a { .nav > li > a {
...@@ -303,11 +310,11 @@ table.table.protected-branches-list tr.no-border { ...@@ -303,11 +310,11 @@ table.table.protected-branches-list tr.no-border {
} }
li { li {
display:inline; display: inline;
} }
a { a {
float:left; float: left;
font-size: 17px; font-size: 17px;
} }
......
...@@ -14,25 +14,8 @@ ...@@ -14,25 +14,8 @@
} }
.todo-item { .todo-item {
font-size: $gl-font-size;
padding-left: $gl-avatar-size + $gl-padding-top;
color: $secondary-text;
a {
color: #4c4e54;
}
.avatar {
margin-left: -($gl-avatar-size + $gl-padding-top);
}
.todo-title { .todo-title {
@include str-truncated(calc(100% - 174px)); @include str-truncated(calc(100% - 174px));
font-weight: 600;
.author-name {
color: #333;
}
} }
.todo-body { .todo-body {
......
...@@ -41,12 +41,12 @@ ...@@ -41,12 +41,12 @@
vertical-align: middle; vertical-align: middle;
i, a { i, a {
color: $gl-link-color; color: $gl-dark-link-color;
} }
img { img {
position: relative; position: relative;
top:-1px; top: -1px;
} }
} }
......
...@@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController
:email, :remember_me, :bio, :name, :username, :email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
:projects_limit, :can_create_group, :admin, :key_id :projects_limit, :can_create_group, :admin, :key_id, :external
) )
end end
......
class Dashboard::TodosController < Dashboard::ApplicationController class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all] before_action :find_todos, only: [:index, :destroy, :destroy_all]
def index def index
@todos = @todos.page(params[:page]).per(PER_PAGE) @todos = @todos.page(params[:page]).per(PER_PAGE)
end end
def destroy def destroy
todo.done! todo.done
todo_notice = 'Todo was successfully marked as done.'
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { render nothing: true } format.js { render nothing: true }
format.json do
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
end end
end end
def destroy_all def destroy_all
@todos.each(&:done!) @todos.each(&:done)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { render nothing: true } format.js { render nothing: true }
format.json do
find_todos
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
end end
end end
......
...@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController ...@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
include MergeRequestsAction include MergeRequestsAction
before_action :event_filter, only: :activity before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests] before_action :projects, only: [:issues, :merge_requests, :labels, :milestones]
respond_to :html respond_to :html
...@@ -20,6 +20,29 @@ class DashboardController < Dashboard::ApplicationController ...@@ -20,6 +20,29 @@ class DashboardController < Dashboard::ApplicationController
end end
end end
def labels
labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title)
respond_to do |format|
format.json do
render json: labels
end
end
end
def milestones
milestones = Milestone.where(project_id: @projects).active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
respond_to do |format|
format.json do
render json: grouped_milestones
end
end
end
protected protected
def load_events def load_events
......
class Projects::BadgesController < Projects::ApplicationController class Projects::BadgesController < Projects::ApplicationController
before_action :set_no_cache before_action :no_cache_headers
def build def build
respond_to do |format| respond_to do |format|
...@@ -10,15 +10,4 @@ class Projects::BadgesController < Projects::ApplicationController ...@@ -10,15 +10,4 @@ class Projects::BadgesController < Projects::ApplicationController
end end
end end
end end
private
def set_no_cache
expires_now
# Add some deprecated headers for older agents
#
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
end
end end
...@@ -23,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -23,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController
def create def create
branch_name = sanitize(strip_tags(params[:branch_name])) branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name) branch_name = Addressable::URI.unescape(branch_name)
ref = sanitize(strip_tags(params[:ref]))
ref = Addressable::URI.unescape(ref)
result = CreateBranchService.new(project, current_user). result = CreateBranchService.new(project, current_user).
execute(branch_name, ref) execute(branch_name, ref)
if params[:issue_iid]
issue = @project.issues.find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
if result[:status] == :success if result[:status] == :success
@branch = result[:branch] @branch = result[:branch]
redirect_to namespace_project_tree_path(@project.namespace, @project, redirect_to namespace_project_tree_path(@project.namespace, @project,
...@@ -49,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -49,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController
format.js { render status: status[:return_code] } format.js { render status: status[:return_code] }
end end
end end
private
def ref
if params[:ref]
ref_escaped = sanitize(strip_tags(params[:ref]))
Addressable::URI.unescape(ref_escaped)
else
@project.default_branch
end
end
end end
...@@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :issue, only: [:edit, :update, :show] before_action :issue, only: [:edit, :update, :show]
# Allow read any issue # Allow read any issue
before_action :authorize_read_issue! before_action :authorize_read_issue!, only: [:show]
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -65,6 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -65,6 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController
@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) @merge_requests = @issue.referenced_merge_requests(current_user)
@related_branches = @issue.related_branches - @merge_requests.map(&:source_branch)
respond_with(@issue) respond_with(@issue)
end end
...@@ -133,6 +134,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -133,6 +134,10 @@ class Projects::IssuesController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
end
def authorize_update_issue! def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue) return render_404 unless can?(current_user, :update_issue, @issue)
end end
...@@ -163,7 +168,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -163,7 +168,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(
:title, :assignee_id, :position, :description, :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: [] :milestone_id, :state_event, :task_num, label_ids: []
) )
end end
......
...@@ -12,6 +12,13 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -12,6 +12,13 @@ class Projects::LabelsController < Projects::ApplicationController
def index def index
@labels = @project.labels.page(params[:page]).per(PER_PAGE) @labels = @project.labels.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
format.json do
render json: @project.labels
end
end
end end
def new def new
......
...@@ -19,8 +19,16 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -19,8 +19,16 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
@milestones = @milestones.includes(:project) @milestones = @milestones.includes(:project)
respond_to do |format|
format.html do
@milestones = @milestones.page(params[:page]).per(PER_PAGE) @milestones = @milestones.page(params[:page]).per(PER_PAGE)
end end
format.json do
render json: @milestones
end
end
end
def new def new
@milestone = @project.milestones.new @milestone = @project.milestones.new
......
...@@ -134,7 +134,7 @@ class ProjectsController < ApplicationController ...@@ -134,7 +134,7 @@ class ProjectsController < ApplicationController
def autocomplete_sources def autocomplete_sources
note_type = params['type'] note_type = params['type']
note_id = params['type_id'] note_id = params['type_id']
autocomplete = ::Projects::AutocompleteService.new(@project) autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
......
...@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder ...@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder
def klass def klass
Issue Issue
end end
private
def init_collection
Issue.visible_to_user(current_user)
end
end end
...@@ -40,25 +40,26 @@ class ProjectsFinder ...@@ -40,25 +40,26 @@ class ProjectsFinder
private private
def group_projects(current_user, group) def group_projects(current_user, group)
if current_user return [group.projects.public_only] unless current_user
[
user_group_projects = [
group_projects_for_user(current_user, group), group_projects_for_user(current_user, group),
group.projects.public_and_internal_only,
group.shared_projects.visible_to_user(current_user) group.shared_projects.visible_to_user(current_user)
] ]
if current_user.external?
user_group_projects << group.projects.public_only
else else
[group.projects.public_only] user_group_projects << group.projects.public_and_internal_only
end end
end end
def all_projects(current_user) def all_projects(current_user)
if current_user return [public_projects] unless current_user
[
current_user.authorized_projects, if current_user.external?
public_and_internal_projects [current_user.authorized_projects, public_projects]
]
else else
[Project.public_only] [current_user.authorized_projects, public_and_internal_projects]
end end
end end
......
...@@ -182,7 +182,7 @@ module ApplicationHelper ...@@ -182,7 +182,7 @@ module ApplicationHelper
# Returns an HTML-safe String # Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s, element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago js-timeago-pending", 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.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' } data: { toggle: 'tooltip', placement: placement, container: 'body' }
...@@ -196,6 +196,22 @@ module ApplicationHelper ...@@ -196,6 +196,22 @@ module ApplicationHelper
element element
end end
def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false)
return if object.updated_at == object.created_at
content_tag :small, class: "edited-text" do
output = content_tag(:span, "Edited ")
output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class)
if include_author && object.updated_by && object.updated_by != object.author
output << content_tag(:span, " by ")
output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil)
end
output
end
end
def render_markup(file_name, file_content) def render_markup(file_name, file_content)
if gitlab_markdown?(file_name) if gitlab_markdown?(file_name)
Haml::Helpers.preserve(markdown(file_content)) Haml::Helpers.preserve(markdown(file_content))
...@@ -285,7 +301,7 @@ module ApplicationHelper ...@@ -285,7 +301,7 @@ module ApplicationHelper
if project.nil? if project.nil?
nil nil
elsif current_controller?(:issues) elsif current_controller?(:issues)
project.issues.send(entity).count project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests) elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count project.merge_requests.send(entity).count
end end
......
...@@ -24,7 +24,7 @@ module DropdownsHelper ...@@ -24,7 +24,7 @@ module DropdownsHelper
capture(&block) if block && !options.has_key?(:footer_content) capture(&block) if block && !options.has_key?(:footer_content)
end end
if block && options.has_key?(:footer_content) if block && options[:footer_content]
output << content_tag(:div, class: "dropdown-footer") do output << content_tag(:div, class: "dropdown-footer") do
capture(&block) capture(&block)
end end
......
...@@ -194,7 +194,7 @@ module EventsHelper ...@@ -194,7 +194,7 @@ module EventsHelper
end end
def event_to_atom(xml, event) def event_to_atom(xml, event)
if event.proper? if event.proper?(current_user)
xml.entry do xml.entry do
event_link = event_feed_url(event) event_link = event_feed_url(event)
event_title = event_feed_title(event) event_title = event_feed_title(event)
......
...@@ -20,6 +20,23 @@ module IssuablesHelper ...@@ -20,6 +20,23 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end end
def user_dropdown_label(user_id, default_label)
return "Unassigned" if user_id == "0"
if @project
member = @project.team.find_member(user_id)
user = member.user if member
else
user = User.find_by(id: user_id)
end
if user
user.name
else
default_label
end
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -111,6 +111,10 @@ module IssuesHelper ...@@ -111,6 +111,10 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ') end.sort.to_sentence(last_word_connector: ', or ')
end end
def confidential_icon(issue)
icon('eye-slash') if issue.confidential?
end
def emoji_icon(name, unicode = nil, aliases = []) def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name) rescue "" unicode ||= Emoji.emoji_filename(name) rescue ""
......
...@@ -109,19 +109,12 @@ module LabelsHelper ...@@ -109,19 +109,12 @@ module LabelsHelper
end end
end end
def projects_labels_options def labels_filter_path
labels =
if @project if @project
@project.labels namespace_project_labels_path(@project.namespace, @project, :json)
else else
Label.where(project_id: @projects) labels_dashboard_path(:json)
end end
grouped_labels = GlobalLabel.build_collection(labels)
grouped_labels.unshift(Label::None)
grouped_labels.unshift(Label::Any)
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end end
def label_subscription_status(label) def label_subscription_status(label)
......
...@@ -38,7 +38,7 @@ module MilestonesHelper ...@@ -38,7 +38,7 @@ module MilestonesHelper
def milestone_progress_bar(milestone) def milestone_progress_bar(milestone)
options = { options = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
style: "width: #{milestone.percent_complete}%;" style: "width: #{milestone.percent_complete(current_user)}%;"
} }
content_tag :div, class: 'progress' do content_tag :div, class: 'progress' do
...@@ -46,22 +46,12 @@ module MilestonesHelper ...@@ -46,22 +46,12 @@ module MilestonesHelper
end end
end end
def projects_milestones_options def milestones_filter_dropdown_path
milestones =
if @project if @project
@project.milestones namespace_project_milestones_path(@project.namespace, @project, :json)
else else
Milestone.where(project_id: @projects) milestones_dashboard_path(:json)
end.active end
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end end
def milestone_remaining_days(milestone) def milestone_remaining_days(milestone)
......
...@@ -26,7 +26,7 @@ module ProjectsHelper ...@@ -26,7 +26,7 @@ module ProjectsHelper
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
end end
def link_to_member(project, author, opts = {}) def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts) opts = default_opts.merge(opts)
...@@ -44,6 +44,8 @@ module ProjectsHelper ...@@ -44,6 +44,8 @@ module ProjectsHelper
author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
end end
author_html << capture(&block) if block
author_html = author_html.html_safe author_html = author_html.html_safe
if opts[:name] if opts[:name]
......
...@@ -16,15 +16,20 @@ module TodosHelper ...@@ -16,15 +16,20 @@ module TodosHelper
def todo_target_link(todo) def todo_target_link(todo)
target = todo.target_type.titleize.downcase target = todo.target_type.titleize.downcase
link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) } link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
end end
def todo_target_path(todo) def todo_target_path(todo)
anchor = dom_id(todo.note) if todo.note.present? anchor = dom_id(todo.note) if todo.note.present?
if todo.for_commit?
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
polymorphic_path([todo.project.namespace.becomes(Namespace), polymorphic_path([todo.project.namespace.becomes(Namespace),
todo.project, todo.target], anchor: anchor) todo.project, todo.target], anchor: anchor)
end end
end
def todos_filter_params def todos_filter_params
{ {
......
...@@ -49,7 +49,6 @@ class Ability ...@@ -49,7 +49,6 @@ class Ability
rules = [ rules = [
:read_project, :read_project,
:read_wiki, :read_wiki,
:read_issue,
:read_label, :read_label,
:read_milestone, :read_milestone,
:read_project_snippet, :read_project_snippet,
...@@ -63,6 +62,9 @@ class Ability ...@@ -63,6 +62,9 @@ class Ability
# Allow to read builds by anonymous user if guests are allowed # Allow to read builds by anonymous user if guests are allowed
rules << :read_build if project.public_builds? rules << :read_build if project.public_builds?
# Allow to read issues by anonymous user if issue is not confidential
rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
rules - project_disabled_features_rules(project) rules - project_disabled_features_rules(project)
else else
[] []
...@@ -109,23 +111,10 @@ class Ability ...@@ -109,23 +111,10 @@ class Ability
key = "/user/#{user.id}/project/#{project.id}" key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin RequestStore.store[key] ||= begin
team = project.team # Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
# Rules based on role in project
if team.master?(user)
rules.push(*project_master_rules)
elsif team.developer?(user)
rules.push(*project_dev_rules)
elsif team.reporter?(user) if project.public? || (project.internal? && !user.external?)
rules.push(*project_report_rules)
elsif team.guest?(user)
rules.push(*project_guest_rules)
end
if project.public? || project.internal?
rules.push(*public_project_rules) rules.push(*public_project_rules)
# Allow to read builds for internal projects # Allow to read builds for internal projects
...@@ -148,6 +137,19 @@ class Ability ...@@ -148,6 +137,19 @@ class Ability
end end
end end
def project_team_rules(team, user)
# Rules based on role in project
if team.master?(user)
project_master_rules
elsif team.developer?(user)
project_dev_rules
elsif team.reporter?(user)
project_report_rules
elsif team.guest?(user)
project_guest_rules
end
end
def public_project_rules def public_project_rules
@public_project_rules ||= project_guest_rules + [ @public_project_rules ||= project_guest_rules + [
:download_code, :download_code,
...@@ -321,6 +323,7 @@ class Ability ...@@ -321,6 +323,7 @@ class Ability
end end
rules += project_abilities(user, subject.project) rules += project_abilities(user, subject.project)
rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules rules
end end
end end
...@@ -356,7 +359,7 @@ class Ability ...@@ -356,7 +359,7 @@ class Ability
] ]
end end
if snippet.public? || snippet.internal? if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet rules << :read_personal_snippet
end end
...@@ -439,5 +442,17 @@ class Ability ...@@ -439,5 +442,17 @@ class Ability
:"admin_#{name}" :"admin_#{name}"
] ]
end end
def filter_confidential_issues_abilities(user, issue, rules)
return rules if user.admin? || !issue.confidential?
unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
rules.delete(:admin_issue)
rules.delete(:read_issue)
rules.delete(:update_issue)
end
rules
end
end end
end end
...@@ -114,7 +114,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -114,7 +114,7 @@ class CommitStatus < ActiveRecord::Base
end end
def ignored? def ignored?
failed? && allow_failure? allow_failure? && (failed? || canceled?)
end end
def duration def duration
......
module Milestoneish module Milestoneish
def closed_items_count def closed_items_count(user = nil)
issues.closed.size + merge_requests.closed_and_merged.size issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end end
def total_items_count def total_items_count(user = nil)
issues.size + merge_requests.size issues_visible_to_user(user).size + merge_requests.size
end end
def complete? def complete?(user = nil)
total_items_count == closed_items_count total_items_count(user) == closed_items_count(user)
end end
def percent_complete def percent_complete(user = nil)
((closed_items_count * 100) / total_items_count).abs ((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError rescue ZeroDivisionError
0 0
end end
...@@ -22,4 +22,8 @@ module Milestoneish ...@@ -22,4 +22,8 @@ module Milestoneish
(due_date - Date.today).to_i (due_date - Date.today).to_i
end end
def issues_visible_to_user(user = nil)
issues.visible_to_user(user)
end
end end
...@@ -73,15 +73,17 @@ class Event < ActiveRecord::Base ...@@ -73,15 +73,17 @@ class Event < ActiveRecord::Base
end end
end end
def proper? def proper?(user = nil)
if push? if push?
true true
elsif membership_changed? elsif membership_changed?
true true
elsif created_project? elsif created_project?
true true
elsif issue?
Ability.abilities.allowed?(user, :read_issue, issue)
else else
((issue? || merge_request? || note?) && target) || milestone? ((merge_request? || note?) && target) || milestone?
end end
end end
......
...@@ -61,6 +61,13 @@ class Issue < ActiveRecord::Base ...@@ -61,6 +61,13 @@ class Issue < ActiveRecord::Base
attributes attributes
end end
def self.visible_to_user(user)
return where(confidential: false) if user.blank?
return all if user.admin?
where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
end
def self.reference_prefix def self.reference_prefix
'#' '#'
end end
...@@ -90,11 +97,21 @@ class Issue < ActiveRecord::Base ...@@ -90,11 +97,21 @@ class Issue < ActiveRecord::Base
end end
def referenced_merge_requests(current_user = nil) def referenced_merge_requests(current_user = nil)
@referenced_merge_requests ||= {}
@referenced_merge_requests[current_user] ||= begin
Gitlab::ReferenceExtractor.lazily do Gitlab::ReferenceExtractor.lazily do
[self, *notes].flat_map do |note| [self, *notes].flat_map do |note|
note.all_references(current_user).merge_requests note.all_references(current_user).merge_requests
end end
end.sort_by(&:iid) end.sort_by(&:iid).uniq
end
end
def related_branches
return [] if self.project.empty_repo?
self.project.repository.branch_names.select do |branch|
branch =~ /\A#{iid}-(?!\d+-stable)/i
end
end end
# Reset issue events cache # Reset issue events cache
...@@ -135,4 +152,15 @@ class Issue < ActiveRecord::Base ...@@ -135,4 +152,15 @@ class Issue < ActiveRecord::Base
!moved? && user.can?(:admin_issue, self.project) !moved? && user.can?(:admin_issue, self.project)
end end
def to_branch_name
"#{iid}-#{title.parameterize}"
end
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
self.related_branches.empty? &&
self.closed_by_merge_requests(current_user).empty?
end
end end
...@@ -516,11 +516,15 @@ class MergeRequest < ActiveRecord::Base ...@@ -516,11 +516,15 @@ class MergeRequest < ActiveRecord::Base
end end
def target_sha def target_sha
@target_sha ||= target_project.repository.commit(target_branch).sha @target_sha ||= target_project.repository.commit(target_branch).try(:sha)
end end
def source_sha def source_sha
last_commit.try(:sha) last_commit.try(:sha) || source_tip.try(:sha)
end
def source_tip
source_branch && source_project.repository.commit(source_branch)
end end
def fetch_ref def fetch_ref
...@@ -568,8 +572,11 @@ class MergeRequest < ActiveRecord::Base ...@@ -568,8 +572,11 @@ class MergeRequest < ActiveRecord::Base
end end
def compute_diverged_commits_count def compute_diverged_commits_count
return 0 unless source_sha && target_sha
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size
end end
private :compute_diverged_commits_count
def diverged_from_target_branch? def diverged_from_target_branch?
diverged_commits_count > 0 diverged_commits_count > 0
......
...@@ -121,8 +121,8 @@ class Milestone < ActiveRecord::Base ...@@ -121,8 +121,8 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero? active? && issues.opened.count.zero?
end end
def is_empty? def is_empty?(user = nil)
total_items_count.zero? total_items_count(user).zero?
end end
def author_id def author_id
......
...@@ -254,12 +254,6 @@ class Project < ActiveRecord::Base ...@@ -254,12 +254,6 @@ class Project < ActiveRecord::Base
where('projects.last_activity_at < ?', 6.months.ago) where('projects.last_activity_at < ?', 6.months.ago)
end end
def publicish(user)
visibility_levels = [Project::PUBLIC]
visibility_levels << Project::INTERNAL if user
where(visibility_level: visibility_levels)
end
def with_push def with_push
joins(:events).where('events.action = ?', Event::PUSHED) joins(:events).where('events.action = ?', Event::PUSHED)
end end
...@@ -577,10 +571,7 @@ class Project < ActiveRecord::Base ...@@ -577,10 +571,7 @@ class Project < ActiveRecord::Base
end end
def avatar_in_git def avatar_in_git
@avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') repository.avatar
@avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg')
@avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif')
@avatar_file
end end
def avatar_url def avatar_url
......
...@@ -3,6 +3,10 @@ require 'securerandom' ...@@ -3,6 +3,10 @@ require 'securerandom'
class Repository class Repository
class CommitError < StandardError; end class CommitError < StandardError; end
# Files to use as a project avatar in case no avatar was uploaded via the web
# UI.
AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
attr_accessor :path_with_namespace, :project attr_accessor :path_with_namespace, :project
...@@ -223,12 +227,6 @@ class Repository ...@@ -223,12 +227,6 @@ class Repository
send(key) send(key)
end end
end end
branches.each do |branch|
unless cache.exist?(:"diverging_commit_counts_#{branch.name}")
send(:diverging_commit_counts, branch)
end
end
end end
def expire_tags_cache def expire_tags_cache
...@@ -241,12 +239,13 @@ class Repository ...@@ -241,12 +239,13 @@ class Repository
@branches = nil @branches = nil
end end
def expire_cache(branch_name = nil) def expire_cache(branch_name = nil, revision = nil)
cache_keys.each do |key| cache_keys.each do |key|
cache.expire(key) cache.expire(key)
end end
expire_branch_cache(branch_name) expire_branch_cache(branch_name)
expire_avatar_cache(branch_name, revision)
# 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.
...@@ -296,18 +295,6 @@ class Repository ...@@ -296,18 +295,6 @@ class Repository
@tag_count = nil @tag_count = nil
end end
def rebuild_cache
cache_keys.each do |key|
cache.expire(key)
send(key)
end
branches.each do |branch|
cache.expire(:"diverging_commit_counts_#{branch.name}")
diverging_commit_counts(branch)
end
end
def lookup_cache def lookup_cache
@lookup_cache ||= {} @lookup_cache ||= {}
end end
...@@ -316,6 +303,23 @@ class Repository ...@@ -316,6 +303,23 @@ class Repository
cache.expire(:branch_names) cache.expire(:branch_names)
end end
def expire_avatar_cache(branch_name = nil, revision = nil)
# Avatars are pulled from the default branch, thus if somebody pushes to a
# different branch there's no need to expire anything.
return if branch_name && branch_name != root_ref
# We don't want to flush the cache if the commit didn't actually make any
# changes to any of the possible avatar files.
if revision && commit = self.commit(revision)
return unless commit.diffs.
any? { |diff| AVATAR_FILES.include?(diff.new_path) }
end
cache.expire(:avatar)
@avatar = nil
end
# Runs code just before a repository is deleted. # Runs code just before a repository is deleted.
def before_delete def before_delete
expire_cache if exists? expire_cache if exists?
...@@ -350,8 +354,8 @@ class Repository ...@@ -350,8 +354,8 @@ class Repository
end end
# Runs code after a new commit has been pushed. # Runs code after a new commit has been pushed.
def after_push_commit(branch_name) def after_push_commit(branch_name, revision)
expire_cache(branch_name) expire_cache(branch_name, revision)
end end
# Runs code after a new branch has been created. # Runs code after a new branch has been created.
...@@ -857,6 +861,14 @@ class Repository ...@@ -857,6 +861,14 @@ class Repository
end end
end end
def avatar
@avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file|
blob_at_branch('master', file)
end
end
end
private private
def cache def cache
......
...@@ -5,14 +5,15 @@ ...@@ -5,14 +5,15 @@
# id :integer not null, primary key # id :integer not null, primary key
# user_id :integer not null # user_id :integer not null
# project_id :integer not null # project_id :integer not null
# target_id :integer not null # target_id :integer
# target_type :string not null # target_type :string not null
# author_id :integer # author_id :integer
# note_id :integer
# action :integer not null # action :integer not null
# state :string not null # state :string not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# note_id :integer
# commit_id :string
# #
class Todo < ActiveRecord::Base class Todo < ActiveRecord::Base
...@@ -27,7 +28,9 @@ class Todo < ActiveRecord::Base ...@@ -27,7 +28,9 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target, :user, presence: true validates :action, :project, :target_type, :user, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
default_scope { reorder(id: :desc) } default_scope { reorder(id: :desc) }
...@@ -36,7 +39,7 @@ class Todo < ActiveRecord::Base ...@@ -36,7 +39,7 @@ class Todo < ActiveRecord::Base
state_machine :state, initial: :pending do state_machine :state, initial: :pending do
event :done do event :done do
transition [:pending, :done] => :done transition [:pending] => :done
end end
state :pending state :pending
...@@ -50,4 +53,25 @@ class Todo < ActiveRecord::Base ...@@ -50,4 +53,25 @@ class Todo < ActiveRecord::Base
target.title target.title
end end
end end
def for_commit?
target_type == "Commit"
end
# override to return commits, which are not active record
def target
if for_commit?
project.commit(commit_id) rescue nil
else
super
end
end
def target_reference
if for_commit?
target.short_id
else
target.to_reference
end
end
end end
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
# hide_project_limit :boolean default(FALSE) # hide_project_limit :boolean default(FALSE)
# unlock_token :string # unlock_token :string
# otp_grace_period_started_at :datetime # otp_grace_period_started_at :datetime
# external :boolean default(FALSE)
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -77,6 +78,7 @@ class User < ActiveRecord::Base ...@@ -77,6 +78,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token add_authentication_token_field :authentication_token
default_value_for :admin, false default_value_for :admin, false
default_value_for :external, false
default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false default_value_for :hide_no_ssh_key, false
...@@ -171,6 +173,7 @@ class User < ActiveRecord::Base ...@@ -171,6 +173,7 @@ class User < ActiveRecord::Base
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token before_save :ensure_authentication_token
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct after_save :ensure_namespace_correct
after_initialize :set_projects_limit after_initialize :set_projects_limit
after_create :post_create_hook after_create :post_create_hook
...@@ -218,6 +221,7 @@ class User < ActiveRecord::Base ...@@ -218,6 +221,7 @@ class User < ActiveRecord::Base
# Scopes # Scopes
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
...@@ -273,6 +277,8 @@ class User < ActiveRecord::Base ...@@ -273,6 +277,8 @@ class User < ActiveRecord::Base
self.with_two_factor self.with_two_factor
when 'wop' when 'wop'
self.without_projects self.without_projects
when 'external'
self.external
else else
self.active self.active
end end
...@@ -841,4 +847,11 @@ class User < ActiveRecord::Base ...@@ -841,4 +847,11 @@ class User < ActiveRecord::Base
def send_devise_notification(notification, *args) def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later devise_mailer.send(notification, self, *args).deliver_later
end end
def ensure_external_user_rights
return unless self.external?
self.can_create_group = false
self.projects_limit = 0
end
end end
...@@ -9,7 +9,8 @@ module Commits ...@@ -9,7 +9,8 @@ module Commits
@commit = params[:commit] @commit = params[:commit]
@create_merge_request = params[:create_merge_request].present? @create_merge_request = params[:create_merge_request].present?
validate and commit check_push_permissions unless @create_merge_request
commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ReversionError => ex ValidationError, ReversionError => ex
error(ex.message) error(ex.message)
...@@ -45,11 +46,11 @@ module Commits ...@@ -45,11 +46,11 @@ module Commits
end end
end end
def validate def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch) allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed unless allowed
raise_error('You are not allowed to push into this branch') raise ValidationError.new('You are not allowed to push into this branch')
end end
true true
......
...@@ -17,7 +17,7 @@ class GitPushService < BaseService ...@@ -17,7 +17,7 @@ class GitPushService < BaseService
# 6. Checks if the project's main language has changed # 6. Checks if the project's main language has changed
# #
def execute def execute
@project.repository.after_push_commit(branch_name) @project.repository.after_push_commit(branch_name, params[:newrev])
if push_remove_branch? if push_remove_branch?
@project.repository.after_remove_branch @project.repository.after_remove_branch
......
...@@ -47,6 +47,21 @@ module MergeRequests ...@@ -47,6 +47,21 @@ module MergeRequests
merge_request.title = merge_request.source_branch.titleize.humanize merge_request.title = merge_request.source_branch.titleize.humanize
end end
# When your branch name starts with an iid followed by a dash this pattern will
# be interpreted as the use wants to close that issue on this project
# Pattern example: 112-fix-mep-mep
# Will lead to appending `Closes #112` to the description
if match = merge_request.source_branch.match(/\A(\d+)-/)
iid = match[1]
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
merge_request.description << closes_issue.prepend("\n")
else
merge_request.description = closes_issue
end
end
merge_request merge_request
end end
......
module Projects module Projects
class AutocompleteService < BaseService class AutocompleteService < BaseService
def initialize(project)
@project = project
end
def issues def issues
@project.issues.opened.select([:iid, :title]) @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end end
def merge_requests def merge_requests
......
...@@ -24,7 +24,7 @@ module Projects ...@@ -24,7 +24,7 @@ module Projects
def execute def execute
raise LeaseTaken if !try_obtain_lease raise LeaseTaken if !try_obtain_lease
GitlabShellWorker.perform_async(:gc, @project.path_with_namespace) GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace)
ensure ensure
@project.update_column(:pushes_since_gc, 0) @project.update_column(:pushes_since_gc, 0)
end end
......
...@@ -11,7 +11,7 @@ module Search ...@@ -11,7 +11,7 @@ module Search
projects = ProjectsFinder.new.execute(current_user) projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group projects = projects.in_namespace(group.id) if group
Gitlab::SearchResults.new(projects, params[:search]) Gitlab::SearchResults.new(current_user, projects, params[:search])
end end
end end
end end
...@@ -7,7 +7,8 @@ module Search ...@@ -7,7 +7,8 @@ module Search
end end
def execute def execute
Gitlab::ProjectSearchResults.new(project, Gitlab::ProjectSearchResults.new(current_user,
project,
params[:search], params[:search],
params[:repository_ref]) params[:repository_ref])
end end
......
...@@ -207,6 +207,18 @@ class SystemNoteService ...@@ -207,6 +207,18 @@ class SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
# Called when a branch is created from the 'new branch' button on a issue
# Example note text:
#
# "Started branch `201-issue-branch-button`"
def self.new_issue_branch(issue, project, author, branch)
h = Gitlab::Application.routes.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})"
create_note(noteable: issue, project: project, author: author, note: body)
end
# Called when a Mentionable references a Noteable # Called when a Mentionable references a Noteable
# #
# noteable - Noteable object being referenced # noteable - Noteable object being referenced
......
...@@ -103,24 +103,16 @@ class TodoService ...@@ -103,24 +103,16 @@ class TodoService
# * mark all pending todos related to the target for the current user as done # * mark all pending todos related to the target for the current user as done
# #
def mark_pending_todos_as_done(target, user) def mark_pending_todos_as_done(target, user)
pending_todos(user, target.project, target).update_all(state: :done) attributes = attributes_for_target(target)
pending_todos(user, attributes).update_all(state: :done)
end end
private private
def create_todos(project, target, author, users, action, note = nil) def create_todos(users, attributes)
Array(users).each do |user| Array(users).each do |user|
next if pending_todos(user, project, target).exists? next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id))
Todo.create(
project: project,
user_id: user.id,
author_id: author.id,
target_id: target.id,
target_type: target.class.name,
action: action,
note: note
)
end end
end end
...@@ -130,8 +122,8 @@ class TodoService ...@@ -130,8 +122,8 @@ class TodoService
end end
def handle_note(note, author) def handle_note(note, author)
# Skip system notes, notes on commit, and notes on project snippet # Skip system notes, and notes on project snippet
return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type) return if note.system? || note.for_project_snippet?
project = note.project project = note.project
target = note.noteable target = note.noteable
...@@ -142,13 +134,39 @@ class TodoService ...@@ -142,13 +134,39 @@ class TodoService
def create_assignment_todo(issuable, author) def create_assignment_todo(issuable, author)
if issuable.assignee && issuable.assignee != author if issuable.assignee && issuable.assignee != author
create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED) attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
create_todos(issuable.assignee, attributes)
end
end
def create_mention_todos(project, target, author, note = nil)
mentioned_users = filter_mentioned_users(project, note || target, author)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
target_id: target.id,
target_type: target.class.name,
commit_id: nil
}
if target.is_a?(Commit)
attributes.merge!(target_id: nil, commit_id: target.id)
end end
attributes
end end
def create_mention_todos(project, issuable, author, note = nil) def attributes_for_todo(project, target, author, action, note = nil)
mentioned_users = filter_mentioned_users(project, note || issuable, author) attributes_for_target(target).merge!(
create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note) project_id: project.id,
author_id: author.id,
action: action,
note: note
)
end end
def filter_mentioned_users(project, target, author) def filter_mentioned_users(project, target, author)
...@@ -160,11 +178,8 @@ class TodoService ...@@ -160,11 +178,8 @@ class TodoService
mentioned_users.uniq mentioned_users.uniq
end end
def pending_todos(user, project, target) def pending_todos(user, criteria = {})
user.todos.pending.where( valid_keys = [:project_id, :target_id, :target_type, :commit_id]
project_id: project.id, user.todos.pending.where(criteria.slice(*valid_keys))
target_id: target.id,
target_type: target.class.name
)
end end
end end
...@@ -58,9 +58,15 @@ ...@@ -58,9 +58,15 @@
= f.label :admin, class: 'control-label' = f.label :admin, class: 'control-label'
- if current_user == @user - if current_user == @user
.col-sm-10= f.check_box :admin, disabled: true .col-sm-10= f.check_box :admin, disabled: true
.col-sm-10 You cannot remove your own admin rights .col-sm-10 You cannot remove your own admin rights.
- else - else
.col-sm-10= f.check_box :admin .col-sm-10= f.check_box :admin
.form-group
= f.label :external, class: 'control-label'
.col-sm-10= f.check_box :external
.col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
%fieldset %fieldset
%legend Profile %legend Profile
.form-group .form-group
......
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
= link_to admin_users_path(filter: 'two_factor_disabled') do = link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled 2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count) %small.badge= number_with_delimiter(User.without_two_factor.count)
%li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
= link_to admin_users_path(filter: 'external') do
External
%small.badge= number_with_delimiter(User.external.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"} %li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do = link_to admin_users_path(filter: "blocked") do
Blocked Blocked
...@@ -70,12 +74,14 @@ ...@@ -70,12 +74,14 @@
%li %li
.list-item-name .list-item-name
- if user.blocked? - if user.blocked?
%i.fa.fa-lock.cred = icon("lock", class: "cred")
- else - else
%i.fa.fa-user.cgreen = icon("user", class: "cgreen")
= link_to user.name, [:admin, user] = link_to user.name, [:admin, user]
- if user.admin? - if user.admin?
%strong.cred (Admin) %strong.cred (Admin)
- if user.external?
%strong.cred (External)
- if user == current_user - if user == current_user
%span.cred It's you! %span.cred It's you!
.pull-right .pull-right
......
...@@ -47,6 +47,10 @@ ...@@ -47,6 +47,10 @@
- else - else
Disabled Disabled
%li
%span.light External User:
%strong
= @user.external? ? "Yes" : "No"
%li %li
%span.light Can create groups: %span.light Can create groups:
%strong %strong
......
- publicish_project_count = Project.publicish(current_user).count - publicish_project_count = ProjectsFinder.new.execute(current_user).count
%h3.page-title Welcome to GitLab! %h3.page-title Welcome to GitLab!
%p.light Self hosted Git management application. %p.light Self hosted Git management application.
%hr %hr
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
- if current_user.can_create_project? - if current_user.can_create_project?
.link_holder .link_holder
= link_to new_project_path, class: "btn btn-new" do = link_to new_project_path, class: "btn btn-new" do
%i.fa.fa-plus = icon('plus')
New Project New Project
- if current_user.can_create_group? - if current_user.can_create_group?
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.todo-item.todo-block .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title .todo-title.title
%span.author-name %span.author-name
- if todo.author - if todo.author
= link_to_author(todo) = link_to_author(todo)
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
- if todo.pending? - if todo.pending?
.todo-actions.pull-right .todo-actions.pull-right
= link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn' = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
Done
= icon('spinner spin')
.todo-body .todo-body
.todo-note .todo-note
......
...@@ -3,13 +3,15 @@ ...@@ -3,13 +3,15 @@
.top-area .top-area
%ul.nav-links %ul.nav-links
%li{class: ('active' if params[:state].blank? || params[:state] == 'pending')} - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
%li{class: "todos-pending #{todo_pending_active}"}
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
To do To do
%span{class: 'badge'} %span{class: 'badge'}
= todos_pending_count = todos_pending_count
%li{class: ('active' if params[:state] == 'done')} - todo_done_active = ('active' if params[:state] == 'done')
%li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do = link_to todos_filter_path(state: 'done') do
%span %span
Done Done
...@@ -18,7 +20,9 @@ ...@@ -18,7 +20,9 @@
.nav-controls .nav-controls
- if @todos.any?(&:pending?) - if @todos.any?(&:pending?)
= link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
Mark all as done
= icon('spinner spin')
.todos-filters .todos-filters
.gray-content-block.second-block .gray-content-block.second-block
...@@ -42,12 +46,12 @@ ...@@ -42,12 +46,12 @@
.prepend-top-default .prepend-top-default
- if @todos.any? - if @todos.any?
- @todos.group_by(&:project).each do |group| - @todos.group_by(&:project).each do |group|
.panel.panel-default.panel-small .panel.panel-default.panel-small.js-todos-list
- project = group[0] - project = group[0]
.panel-heading .panel-heading
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project) = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
%ul.well-list.todos-list %ul.content-list.todos-list
= render group[1] = render group[1]
= paginate @todos, theme: "gitlab" = paginate @todos, theme: "gitlab"
- else - else
......
- if event.proper? - if event.proper?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp .event-item-timestamp
#{time_ago_with_tooltip(event.created_at)} #{time_ago_with_tooltip(event.created_at)}
......
...@@ -46,6 +46,8 @@ ...@@ -46,6 +46,8 @@
%h1.title= title %h1.title= title
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
- if @project && !@project.empty_repo? - if @project && !@project.empty_repo?
- if ref = @ref || @project.repository.root_ref
:javascript :javascript
var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}"; var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}";
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
%span %span
Issues Issues
- if @project.default_issues_tracker? - if @project.default_issues_tracker?
%span.count.issue_counter= number_with_delimiter(@project.issues.opened.count) %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do = nav_link(controller: :merge_requests) do
......
%fieldset.builds-feature
%legend
Builds:
.form-group
.col-sm-offset-2.col-sm-10
%p Get recent application code using the following command:
.radio
= f.label :build_allow_git_fetch_false do
= f.radio_button :build_allow_git_fetch, 'false'
%strong git clone
%br
%span.descr Slower but makes sure you have a clean dir before every build
.radio
= f.label :build_allow_git_fetch_true do
= f.radio_button :build_allow_git_fetch, 'true'
%strong git fetch
%br
%span.descr Faster
.form-group
= f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
.col-sm-10
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block per build in minutes
.form-group
= f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
.col-sm-10
.input-group
%span.input-group-addon /
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
%span.input-group-addon /
%p.help-block
We will use this regular expression to find test coverage output in build trace.
Leave blank if you want to disable this feature
.bs-callout.bs-callout-info
%p Below are examples of regex for existing tools:
%ul
%li
Simplecov (Ruby) -
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
%code \d+\%\s*$
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :public_builds do
= f.check_box :public_builds
%strong Public builds
.help-block Allow everyone to access builds for Public and Internal projects
.form-group
= f.label :runners_token, "Runners token", class: 'control-label'
.col-sm-10
= f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
%p.help-block The secure token used to checkout project.
...@@ -42,6 +42,10 @@ ...@@ -42,6 +42,10 @@
.diff-content.diff-wrap-lines .diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs -# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?') - return unless blob.respond_to?('text?')
- if diff_file.too_large?
.nothing-here-block
This diff could not be displayed because it is too large.
- else
- if blob_text_viewable?(blob) - if blob_text_viewable?(blob)
- if diff_view == 'parallel' - if diff_view == 'parallel'
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
......
...@@ -84,6 +84,8 @@ ...@@ -84,6 +84,8 @@
%br %br
%span.descr Share code pastes with others out of git repository %span.descr Share code pastes with others out of git repository
= render 'builds_settings', f: f
%fieldset.features %fieldset.features
%legend %legend
Project avatar: Project avatar:
...@@ -110,69 +112,6 @@ ...@@ -110,69 +112,6 @@
%hr %hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
%fieldset.features
%legend
Continuous Integration
.form-group
.col-sm-offset-2.col-sm-10
%p Get recent application code using the following command:
.radio
= f.label :build_allow_git_fetch_false do
= f.radio_button :build_allow_git_fetch, 'false'
%strong git clone
%br
%span.descr Slower but makes sure you have a clean dir before every build
.radio
= f.label :build_allow_git_fetch_true do
= f.radio_button :build_allow_git_fetch, 'true'
%strong git fetch
%br
%span.descr Faster
.form-group
= f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
.col-sm-10
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block per build in minutes
.form-group
= f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
.col-sm-10
.input-group
%span.input-group-addon /
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
%span.input-group-addon /
%p.help-block
We will use this regular expression to find test coverage output in build trace.
Leave blank if you want to disable this feature
.bs-callout.bs-callout-info
%p Below are examples of regex for existing tools:
%ul
%li
Simplecov (Ruby) -
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
%code \d+\%\s*$
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :public_builds do
= f.check_box :public_builds
%strong Public builds
.help-block Allow everyone to access builds for Public and Internal projects
%fieldset.features
%legend
Advanced settings
.form-group
= f.label :runners_token, "CI token", class: 'control-label'
.col-sm-10
= f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
%p.help-block The secure token used to checkout project.
.form-actions .form-actions
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save"
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
.issue-check .issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-title .issue-title.title
%span.issue-title-text %span.issue-title-text
= link_to_gfm issue.title, issue_path(issue), class: "title" = confidential_icon(issue)
%ul.controls.light = link_to_gfm issue.title, issue_path(issue)
%ul.controls
- if issue.closed? - if issue.closed?
%li %li
CLOSED CLOSED
......
-if @merge_requests.any? - if @merge_requests.any?
%h2.merge-requests-title %h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request') = pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list %ul.unstyled-list
......
- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
.pull-right
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do
= icon('code-fork')
New Branch
- if @related_branches.any?
%h2.related-branches-title
= pluralize(@related_branches.count, 'Related Branch')
%ul.unstyled-list
- @related_branches.each do |branch|
%li
- sha = @project.repository.find_branch(branch).target
- ci_commit = @project.ci_commit(sha) if sha
- if ci_commit
%span.related-branch-ci-status
= render_ci_status(ci_commit)
%span.related-branch-info
%strong
= link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
= branch
...@@ -22,20 +22,20 @@ ...@@ -22,20 +22,20 @@
= icon('angle-double-left') = icon('angle-double-left')
.issue-meta .issue-meta
= confidential_icon(@issue)
%strong.identifier %strong.identifier
Issue ##{@issue.iid} Issue ##{@issue.iid}
%span.creator %span.creator
by opened
.editor-details .editor-details
.editor-details .editor-details
= time_ago_with_tooltip(@issue.created_at)
by
%strong %strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs") = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
%span.hidden-xs
= '@' + @issue.author.username
%strong %strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false) by_username: true, avatar: false)
= time_ago_with_tooltip(@issue.created_at)
.pull-right.issue-btn-group .pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project) - if can?(current_user, :create_issue, @project)
...@@ -63,15 +63,14 @@ ...@@ -63,15 +63,14 @@
= markdown(@issue.description, cache_key: [@issue, "description"]) = markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @issue.description = @issue.description
- if @issue.updated_at != @issue.created_at = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
%small
Edited
= time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
.merge-requests .merge-requests
= render 'merge_requests' = render 'merge_requests'
= render 'related_branches'
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch'
= render 'votes/votes_block', votable: @issue = render 'votes/votes_block', votable: @issue
.row .row
......
%li{ class: mr_css_classes(merge_request) } %li{ class: mr_css_classes(merge_request) }
.merge-request-title .merge-request-title.title
%span.merge-request-title-text %span.merge-request-title-text
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title" = link_to_gfm merge_request.title, merge_request_path(merge_request)
%ul.controls.light %ul.controls
- if merge_request.merged? - if merge_request.merged?
%li %li
MERGED MERGED
......
...@@ -11,7 +11,4 @@ ...@@ -11,7 +11,4 @@
%textarea.hidden.js-task-list-field %textarea.hidden.js-task-list-field
= @merge_request.description = @merge_request.description
- if @merge_request.updated_at != @merge_request.created_at = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
%small
Edited
= time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom')
...@@ -8,18 +8,21 @@ ...@@ -8,18 +8,21 @@
= icon('angle-double-left') = icon('angle-double-left')
.issue-meta .issue-meta
%strong.identifier %strong.identifier
Merge Request ##{@merge_request.iid} %span.hidden-sm.hidden-md.hidden-lg
MR
%span.hidden-xs
Merge Request
!#{@merge_request.iid}
%span.creator %span.creator
by opened
.editor-details .editor-details
= time_ago_with_tooltip(@merge_request.created_at)
by
%strong %strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs") = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
%span.hidden-xs
= '@' + @merge_request.author.username
%strong %strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false) by_username: true, avatar: false)
= time_ago_with_tooltip(@merge_request.created_at)
.issue-btn-group.pull-right .issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request) - if can?(current_user, :update_merge_request, @merge_request)
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
= preserve do = preserve do
= markdown @milestone.description = markdown @milestone.description
- if @milestone.complete? && @milestone.active? - if @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default .alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now. %span All issues for this milestone are closed. You may close milestone now.
......
...@@ -27,20 +27,13 @@ ...@@ -27,20 +27,13 @@
%span.note-last-update %span.note-last-update
%a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'} %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'}
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
- if note.updated_at != note.created_at
%span.note-updated-at
&middot;
= icon('edit', title: 'edited')
= time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
- if note.updated_by && note.updated_by != note.author
by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"]) = markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note) - if note_editable?(note)
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note.attachment.url - if note.attachment.url
.note-attachment .note-attachment
...@@ -54,4 +47,3 @@ ...@@ -54,4 +47,3 @@
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred') = icon('trash-o', class: 'cred')
.clear
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%span.caret %span.caret
%span.sr-only %span.sr-only
Select Archive Format Select Archive Format
%ul.col-xs-10.dropdown-menu{ role: 'menu' } %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li %li
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download %i.fa.fa-download
......
.search-result-row .search-result-row
%h4 %h4
= confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title %span.term.str-truncated= issue.title
.pull-right ##{issue.iid} .pull-right ##{issue.iid}
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%i.fa.fa-cogs %i.fa.fa-cogs
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
%i.fa.fa-sign-out = icon('sign-out')
.stats .stats
%span %span
...@@ -22,7 +22,8 @@ ...@@ -22,7 +22,8 @@
= number_with_delimiter(group.users.count) = number_with_delimiter(group.users.count)
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
= link_to group, class: 'group-name title' do .title
= link_to group, class: 'group-name' do
= group.name = group.name
- if group_member - if group_member
......
...@@ -9,75 +9,20 @@ ...@@ -9,75 +9,20 @@
.filter-item.inline .filter-item.inline
- if params[:author_id] - if params[:author_id]
= hidden_field_tag(:author_id, params[:author_id]) = hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } }) placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline .filter-item.inline
- if params[:assignee_id] - if params[:assignee_id]
= hidden_field_tag(:assignee_id, params[:assignee_id]) = hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee",
placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } }) placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter .filter-item.inline.milestone-filter
- if params[:milestone_title] = render "shared/issuable/milestone_dropdown"
= hidden_field_tag(:milestone_title, params[:milestone_title])
= dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
- if @project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, @project
%li
= link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
Create new
%li
= link_to namespace_project_milestones_path(@project.namespace, @project) do
- if can? current_user, :admin_milestone, @project
Manage milestones
- else
View milestones
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
- if params[:label_name] = render "shared/issuable/label_dropdown"
= hidden_field_tag(:label_name, params[:label_name])
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
%span.dropdown-toggle-text
Label
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one
= dropdown_title("Filter by label")
= dropdown_filter("Search labels")
= dropdown_content
- if @project
= dropdown_footer do
%ul.dropdown-footer-list
- if can? current_user, :admin_label, @project
%li
%a.dropdown-toggle-page{href: "#"}
Create new
%li
= link_to namespace_project_labels_path(@project.namespace, @project) do
- if can? current_user, :admin_label, @project
Manage labels
- else
View labels
- if can? current_user, :admin_label, @project
.dropdown-page-two
= dropdown_title("Create new label", back: true)
= dropdown_content do
%input#new_label_color{type: "hidden"}
%input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
.dropdown-label-color-preview.js-dropdown-label-color-preview
.suggest-colors.suggest-colors-dropdown
- suggested_colors.each do |color|
= link_to '#', style: "background-color: #{color}", data: { color: color } do
&nbsp
%button.btn.btn-primary.js-new-label-btn{type: "button"}
Create
= dropdown_loading
.dropdown-loading
= icon('spinner spin')
.pull-right .pull-right
= render 'shared/sort_dropdown' = render 'shared/sort_dropdown'
......
...@@ -29,6 +29,15 @@ ...@@ -29,6 +29,15 @@
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
.error-alert .error-alert
- if issuable.is_a?(Issue) && !issuable.project.private?
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :confidential do
= f.check_box :confidential
This issue is confidential and should only be visible to team members
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
%hr %hr
.form-group .form-group
......
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