mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-14 15:16:04 +10:00
Merge branch 'master' into issue_3400_port
# Conflicts: # app/assets/javascripts/gl_dropdown.js.coffee
This commit is contained in:
+18
-14
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"always-semicolon": true,
|
||||
"color-case": "lower",
|
||||
"block-indent": " ",
|
||||
"color-shorthand": true,
|
||||
"element-case": "lower",
|
||||
"space-before-colon": "",
|
||||
"space-after-colon": " ",
|
||||
"space-before-combinator": " ",
|
||||
"space-after-combinator": " ",
|
||||
"space-between-declarations": "\n",
|
||||
"space-before-opening-brace": " ",
|
||||
"space-after-opening-brace": "\n",
|
||||
"space-before-closing-brace": "\n",
|
||||
"unitless-zero": true
|
||||
"exclude": [
|
||||
"app/assets/stylesheets/framework/tw_bootstrap_variables.scss",
|
||||
"app/assets/stylesheets/framework/fonts.scss"
|
||||
],
|
||||
"always-semicolon": true,
|
||||
"color-case": "lower",
|
||||
"block-indent": " ",
|
||||
"color-shorthand": true,
|
||||
"element-case": "lower",
|
||||
"space-before-colon": "",
|
||||
"space-after-colon": " ",
|
||||
"space-before-combinator": " ",
|
||||
"space-after-combinator": " ",
|
||||
"space-between-declarations": "\n",
|
||||
"space-before-opening-brace": " ",
|
||||
"space-after-opening-brace": "\n",
|
||||
"space-before-closing-brace": "\n",
|
||||
"unitless-zero": true
|
||||
}
|
||||
|
||||
@@ -128,7 +128,6 @@ scss-lint:
|
||||
- bundle exec rake scss_lint
|
||||
tags:
|
||||
- ruby
|
||||
allow_failure: true
|
||||
|
||||
brakeman:
|
||||
stage: test
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ linters:
|
||||
# Selectors should always use hyphenated-lowercase, rather than camelCase or
|
||||
# snake_case.
|
||||
SelectorFormat:
|
||||
enabled: true
|
||||
enabled: false
|
||||
convention: hyphenated_lowercase
|
||||
|
||||
# Prefer the shortest shorthand form possible for properties that support it.
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.6.0 (unreleased)
|
||||
- Add ability to move issue to another project
|
||||
- Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
|
||||
v 8.7.0 (unreleased)
|
||||
- Preserve time notes/comments have been updated at when moving issue
|
||||
- Make HTTP(s) label consistent on clone bar (Stan Hu)
|
||||
- Fix avatar stretching by providing a cropping feature
|
||||
|
||||
v 8.6.1
|
||||
- Add option to reload the schema before restoring a database backup. !2807
|
||||
- Display navigation controls on mobile. !3214
|
||||
- Fixed bug where participants would not work correctly on merge requests. !3329
|
||||
- Fix sorting issues by votes on the groups issues page results in SQL errors. !3333
|
||||
- Restrict notifications for confidential issues. !3334
|
||||
- Do not allow to move issue if it has not been persisted. !3340
|
||||
- Add a confirmation step before deleting an issuable. !3341
|
||||
- Fixes issue with signin button overflowing on mobile. !3342
|
||||
- Auto collapses the navigation sidebar when resizing. !3343
|
||||
- Fix build dependencies, when the dependency is a string. !3344
|
||||
- Shows error messages when trying to create label in dropdown menu. !3345
|
||||
- Fixes issue with assign milestone not loading milestone list. !3346
|
||||
- Fix an issue causing the Dashboard/Milestones page to be blank. !3348
|
||||
|
||||
v 8.6.0
|
||||
- Add ability to move issue to another project
|
||||
- Prevent tokens in the import URL to be showed by the UI
|
||||
- Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
|
||||
- Add confidential issues
|
||||
- Bump gitlab_git to 9.0.3 (Stan Hu)
|
||||
- Fix diff image view modes (2-up, swipe, onion skin) not working (Stan Hu)
|
||||
@@ -18,9 +38,11 @@ v 8.6.0 (unreleased)
|
||||
setup. A password can be provided during setup (see installation docs), or
|
||||
GitLab will ask the user to create a new one upon first visit.
|
||||
- Fix issue when pushing to projects ending in .wiki
|
||||
- Properly display YAML front matter in Markdown
|
||||
- Add support for wiki with UTF-8 page names (Hiroyuki Sato)
|
||||
- Fix wiki search results point to raw source (Hiroyuki Sato)
|
||||
- Don't load all of GitLab in mail_room
|
||||
- Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
|
||||
- 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
|
||||
- Memoize @group in Admin::GroupsController (Yatish Mehta)
|
||||
@@ -60,6 +82,7 @@ v 8.6.0 (unreleased)
|
||||
- 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
|
||||
- Let project owners and admins soft delete issues and merge requests
|
||||
|
||||
v 8.5.8
|
||||
- Bump Git version requirement to 2.7.4
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- [Issue tracker guidelines](#issue-tracker-guidelines)
|
||||
- [Issue weight](#issue-weight)
|
||||
- [Regression issues](#regression-issues)
|
||||
- [Technical debt](#technical-debt)
|
||||
- [Merge requests](#merge-requests)
|
||||
- [Merge request guidelines](#merge-request-guidelines)
|
||||
- [Merge request description format](#merge-request-description-format)
|
||||
@@ -242,6 +243,28 @@ addressed.
|
||||
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
|
||||
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
|
||||
|
||||
### Technical debt
|
||||
|
||||
In order to track things that can be improved in GitLab's codebase, we created
|
||||
the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
|
||||
|
||||
This label should be added to issues that describe things that can be improved,
|
||||
shortcuts that have been taken, code that needs refactoring, features that need
|
||||
additional attention, and all other things that have been left behind due to
|
||||
high velocity of development.
|
||||
|
||||
Everyone can create an issue, though you may need to ask for adding a specific
|
||||
label, if you do not have permissions to do it by yourself. Additional labels
|
||||
can be combined with the `technical debt` label, to make it easier to schedule
|
||||
the improvements for a release.
|
||||
|
||||
Issues tagged with the `technical debt` label have the same priority like issues
|
||||
that describe a new feature to be introduced in GitLab, and should be scheduled
|
||||
for a release by the appropriate person.
|
||||
|
||||
Make sure to mention the merge request that the `technical debt` issue is
|
||||
associated with in the description of the issue.
|
||||
|
||||
## Merge requests
|
||||
|
||||
We welcome merge requests with fixes and improvements to GitLab code, tests,
|
||||
|
||||
@@ -1064,6 +1064,3 @@ DEPENDENCIES
|
||||
web-console (~> 2.0)
|
||||
webmock (~> 1.21.0)
|
||||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.11.2
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
dataType: "json"
|
||||
).done (label) ->
|
||||
callback(label)
|
||||
.error (message) ->
|
||||
callback(message.responseJSON)
|
||||
|
||||
# Return group projects list. Filtered by query
|
||||
groupProjects: (group_id, query, callback) ->
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#= require jquery.nicescroll
|
||||
#= require_tree .
|
||||
#= require fuzzaldrin-plus
|
||||
#= require cropper
|
||||
|
||||
window.slugify = (text) ->
|
||||
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
||||
@@ -139,7 +140,7 @@ $ ->
|
||||
|
||||
# Initialize tooltips
|
||||
$('body').tooltip(
|
||||
selector: '.has_tooltip, [data-toggle="tooltip"]'
|
||||
selector: '.has-tooltip, [data-toggle="tooltip"]'
|
||||
placement: (_, el) ->
|
||||
$el = $(el)
|
||||
$el.data('placement') || 'bottom'
|
||||
@@ -218,13 +219,20 @@ $ ->
|
||||
$this = $(this)
|
||||
$this.attr 'value', $this.val()
|
||||
|
||||
$sidebarGutterToggle = $('.js-sidebar-toggle')
|
||||
$navIconToggle = $('.toggle-nav-collapse')
|
||||
|
||||
$(document)
|
||||
.off 'breakpoint:change'
|
||||
.on 'breakpoint:change', (e, breakpoint) ->
|
||||
if breakpoint is 'sm' or breakpoint is 'xs'
|
||||
$gutterIcon = $('.js-sidebar-toggle').find('i')
|
||||
$gutterIcon = $sidebarGutterToggle.find('i')
|
||||
if $gutterIcon.hasClass('fa-angle-double-right')
|
||||
$gutterIcon.closest('a').trigger('click')
|
||||
$sidebarGutterToggle.trigger('click')
|
||||
|
||||
$navIcon = $navIconToggle.find('.fa')
|
||||
if $navIcon.hasClass('fa-angle-left')
|
||||
$navIconToggle.trigger('click')
|
||||
|
||||
$(document)
|
||||
.off 'click', '.js-sidebar-toggle'
|
||||
|
||||
@@ -122,7 +122,7 @@ class @AwardsHandler
|
||||
|
||||
nodes = []
|
||||
nodes.push(
|
||||
"<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
|
||||
"<button class='btn award-control js-emoji-btn has-tooltip active' title='me'>",
|
||||
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
|
||||
"<span class='award-control-text js-counter'>1</span>",
|
||||
"</button>"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
class GitLabCrop
|
||||
# Matches everything but the file name
|
||||
FILENAMEREGEX = /^.*[\\\/]/
|
||||
|
||||
constructor: (input, opts = {}) ->
|
||||
@fileInput = $(input)
|
||||
|
||||
# We should rename to avoid spec to fail
|
||||
# Form will submit the proper input filed with a file using FormData
|
||||
@fileInput
|
||||
.attr('name', "#{@fileInput.attr('name')}-trigger")
|
||||
.attr('id', "#{@fileInput.attr('id')}-trigger")
|
||||
|
||||
# Set defaults
|
||||
{
|
||||
@exportWidth = 200
|
||||
@exportHeight = 200
|
||||
@cropBoxWidth = 200
|
||||
@cropBoxHeight = 200
|
||||
@form = @fileInput.parents('form')
|
||||
|
||||
# Required params
|
||||
@filename
|
||||
@previewImage
|
||||
@modalCrop
|
||||
@pickImageEl
|
||||
@uploadImageBtn
|
||||
@modalCropImg
|
||||
} = opts
|
||||
|
||||
# Ensure needed elements are jquery objects
|
||||
# If selector is provided we will convert them to a jQuery Object
|
||||
@filename = @getElement(@filename)
|
||||
@previewImage = @getElement(@previewImage)
|
||||
@pickImageEl = @getElement(@pickImageEl)
|
||||
|
||||
# Modal elements usually are outside the @form element
|
||||
@modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
|
||||
@uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
|
||||
@modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
|
||||
|
||||
@cropActionsBtn = @modalCrop.find('[data-method]')
|
||||
|
||||
@bindEvents()
|
||||
|
||||
getElement: (selector) ->
|
||||
$(selector, @form)
|
||||
|
||||
bindEvents: ->
|
||||
_this = @
|
||||
@fileInput.on 'change', (e) ->
|
||||
_this.onFileInputChange(e, @)
|
||||
|
||||
@pickImageEl.on 'click', @onPickImageClick
|
||||
@modalCrop.on 'shown.bs.modal', @onModalShow
|
||||
@modalCrop.on 'hidden.bs.modal', @onModalHide
|
||||
@uploadImageBtn.on 'click', @onUploadImageBtnClick
|
||||
@cropActionsBtn.on 'click', (e) ->
|
||||
btn = @
|
||||
_this.onActionBtnClick(btn)
|
||||
@croppedImageBlob = null
|
||||
|
||||
onPickImageClick: =>
|
||||
@fileInput.trigger('click')
|
||||
|
||||
onModalShow: =>
|
||||
_this = @
|
||||
@modalCropImg.cropper(
|
||||
viewMode: 1
|
||||
center: false
|
||||
aspectRatio: 1
|
||||
modal: true
|
||||
scalable: false
|
||||
rotatable: false
|
||||
zoomable: true
|
||||
dragMode: 'move'
|
||||
guides: false
|
||||
zoomOnTouch: false
|
||||
zoomOnWheel: false
|
||||
cropBoxMovable: false
|
||||
cropBoxResizable: false
|
||||
toggleDragModeOnDblclick: false
|
||||
built: ->
|
||||
$image = $(@)
|
||||
container = $image.cropper 'getContainerData'
|
||||
cropBoxWidth = _this.cropBoxWidth;
|
||||
cropBoxHeight = _this.cropBoxHeight;
|
||||
|
||||
$image.cropper('setCropBoxData',
|
||||
width: cropBoxWidth,
|
||||
height: cropBoxHeight,
|
||||
left: (container.width - cropBoxWidth) / 2,
|
||||
top: (container.height - cropBoxHeight) / 2
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
onModalHide: =>
|
||||
@modalCropImg
|
||||
.attr('src', '') # Remove attached image
|
||||
.cropper('destroy') # Destroy cropper instance
|
||||
|
||||
onUploadImageBtnClick: (e) =>
|
||||
e.preventDefault()
|
||||
@setBlob()
|
||||
@setPreview()
|
||||
@modalCrop.modal('hide')
|
||||
@fileInput.val('')
|
||||
|
||||
onActionBtnClick: (btn) ->
|
||||
data = $(btn).data()
|
||||
|
||||
if @modalCropImg.data('cropper') && data.method
|
||||
result = @modalCropImg.cropper data.method, data.option
|
||||
|
||||
onFileInputChange: (e, input) ->
|
||||
@readFile(input)
|
||||
|
||||
readFile: (input) ->
|
||||
_this = @
|
||||
reader = new FileReader
|
||||
reader.onload = ->
|
||||
_this.modalCropImg.attr('src', reader.result)
|
||||
_this.modalCrop.modal('show')
|
||||
|
||||
reader.readAsDataURL(input.files[0])
|
||||
|
||||
dataURLtoBlob: (dataURL) ->
|
||||
binary = atob(dataURL.split(',')[1])
|
||||
array = []
|
||||
for v, k in binary
|
||||
array.push(binary.charCodeAt(k))
|
||||
new Blob([new Uint8Array(array)], type: 'image/png')
|
||||
|
||||
setPreview: ->
|
||||
@previewImage.attr('src', @dataURL)
|
||||
filename = @fileInput.val().replace(FILENAMEREGEX, '')
|
||||
@filename.text(filename)
|
||||
|
||||
setBlob: ->
|
||||
@dataURL = @modalCropImg.cropper('getCroppedCanvas',
|
||||
width: 200
|
||||
height: 200
|
||||
).toDataURL('image/png')
|
||||
@croppedImageBlob = @dataURLtoBlob(@dataURL)
|
||||
|
||||
getBlob: ->
|
||||
@croppedImageBlob
|
||||
|
||||
$.fn.glCrop = (opts) ->
|
||||
return @.each ->
|
||||
$(@).data('glcrop', new GitLabCrop(@, opts))
|
||||
@@ -1,16 +1,33 @@
|
||||
class GitLabDropdownFilter
|
||||
BLUR_KEYCODES = [27, 40]
|
||||
HAS_VALUE_CLASS = "has-value"
|
||||
|
||||
constructor: (@dropdown, @options) ->
|
||||
constructor: (@input, @options) ->
|
||||
{
|
||||
@input
|
||||
@filterInputBlur = true
|
||||
} = @options
|
||||
|
||||
$inputContainer = @input.parent()
|
||||
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
|
||||
|
||||
# Clear click
|
||||
$clearButton.on 'click', (e) =>
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@input
|
||||
.val('')
|
||||
.trigger('keyup')
|
||||
.focus()
|
||||
|
||||
# Key events
|
||||
timeout = ""
|
||||
@input.on "keyup", (e) =>
|
||||
if e.keyCode is 13 && @input.val() isnt ""
|
||||
if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
|
||||
$inputContainer.addClass HAS_VALUE_CLASS
|
||||
else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
|
||||
$inputContainer.removeClass HAS_VALUE_CLASS
|
||||
|
||||
if e.keyCode is 13 and @input.val() isnt ""
|
||||
if @options.enterCallback
|
||||
@options.enterCallback()
|
||||
return
|
||||
@@ -119,7 +136,9 @@ class GitLabDropdown
|
||||
|
||||
# Init filterable
|
||||
if @options.filterable
|
||||
@filter = new GitLabDropdownFilter @dropdown,
|
||||
@input = @dropdown.find('.dropdown-input .dropdown-input-field')
|
||||
|
||||
@filter = new GitLabDropdownFilter @input,
|
||||
filterInputBlur: @filterInputBlur
|
||||
input: @filterInput
|
||||
remote: @options.filterRemote
|
||||
@@ -129,6 +148,7 @@ class GitLabDropdown
|
||||
return @fullData
|
||||
callback: (data) =>
|
||||
@parseData data
|
||||
@highlightRow 1
|
||||
enterCallback: =>
|
||||
@selectFirstRow()
|
||||
|
||||
@@ -278,11 +298,19 @@ class GitLabDropdown
|
||||
|
||||
noResults: ->
|
||||
html = "<li>"
|
||||
html += "<a href='#' class='is-focused'>"
|
||||
html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
|
||||
html += "No matching results."
|
||||
html += "</a>"
|
||||
html += "</li>"
|
||||
|
||||
highlightRow: (index) ->
|
||||
if @input.val() isnt ""
|
||||
selector = '.dropdown-content li:first-child a'
|
||||
if @dropdown.find(".dropdown-toggle-page").length
|
||||
selector = ".dropdown-page-one .dropdown-content li:first-child a"
|
||||
|
||||
$(selector).addClass 'is-focused'
|
||||
|
||||
rowClicked: (el) ->
|
||||
fieldName = @options.fieldName
|
||||
field = @dropdown.parent().find("input[name='#{fieldName}']")
|
||||
@@ -326,7 +354,7 @@ class GitLabDropdown
|
||||
if @dropdown.find(".dropdown-toggle-page").length
|
||||
selector = ".dropdown-page-one .dropdown-content li:first-child a"
|
||||
|
||||
# similute a click on the first link
|
||||
# simulate a click on the first link
|
||||
$(selector).trigger "click"
|
||||
|
||||
$.fn.glDropdown = (opts) ->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#= require jquery.waitforimages
|
||||
|
||||
class @IssuableContext
|
||||
constructor: ->
|
||||
@initParticipants()
|
||||
|
||||
new UsersSelect()
|
||||
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
|
||||
|
||||
@@ -17,3 +17,27 @@ class @IssuableContext
|
||||
block.find('.js-select2').select2("open")
|
||||
|
||||
$(".right-sidebar").niceScroll()
|
||||
|
||||
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()
|
||||
|
||||
@@ -7,7 +7,6 @@ class @Issue
|
||||
# Prevent duplicate event bindings
|
||||
@disableTaskList()
|
||||
@fixAffixScroll()
|
||||
@initParticipants()
|
||||
if $('a.btn-close').length
|
||||
@initTaskList()
|
||||
@initIssueBtnEventListeners()
|
||||
@@ -85,27 +84,3 @@ class @Issue
|
||||
type: 'PATCH'
|
||||
url: $('form.js-issuable-update').attr('action')
|
||||
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()
|
||||
|
||||
@@ -6,7 +6,7 @@ class @LabelsSelect
|
||||
labelUrl = $dropdown.data('labels')
|
||||
selectedLabel = $dropdown.data('selected')
|
||||
if selectedLabel
|
||||
selectedLabel = selectedLabel.split(',')
|
||||
selectedLabel = selectedLabel.toString().split(',')
|
||||
newLabelField = $('#new_label_name')
|
||||
newColorField = $('#new_label_color')
|
||||
showNo = $dropdown.data('show-no')
|
||||
@@ -14,28 +14,81 @@ class @LabelsSelect
|
||||
defaultLabel = $dropdown.data('default-label')
|
||||
|
||||
if newLabelField.length
|
||||
$newLabelCreateButton = $('.js-new-label-btn')
|
||||
$colorPreview = $('.js-dropdown-label-color-preview')
|
||||
$newLabelError = $dropdown.parent().find('.js-label-error')
|
||||
$newLabelError.hide()
|
||||
|
||||
# Suggested colors in the dropdown to chose from pre-chosen colors
|
||||
$('.suggest-colors-dropdown a').on 'click', (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
newColorField.val $(this).data('color')
|
||||
$('.js-dropdown-label-color-preview')
|
||||
newColorField
|
||||
.val($(this).data('color'))
|
||||
.trigger('change')
|
||||
$colorPreview
|
||||
.css 'background-color', $(this).data('color')
|
||||
.parent()
|
||||
.addClass 'is-active'
|
||||
|
||||
$('.js-new-label-btn').on 'click', (e) ->
|
||||
# Cancel button takes back to first page
|
||||
resetForm = ->
|
||||
newLabelField
|
||||
.val ''
|
||||
.trigger 'change'
|
||||
newColorField
|
||||
.val ''
|
||||
.trigger 'change'
|
||||
$colorPreview
|
||||
.css 'background-color', ''
|
||||
.parent()
|
||||
.removeClass 'is-active'
|
||||
|
||||
$('.dropdown-menu-back').on 'click', ->
|
||||
resetForm()
|
||||
|
||||
$('.js-cancel-label-btn').on 'click', (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
resetForm()
|
||||
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
|
||||
|
||||
# Listen for change and keyup events on label and color field
|
||||
# This allows us to enable the button when ready
|
||||
enableLabelCreateButton = ->
|
||||
if newLabelField.val() isnt '' and newColorField.val() isnt ''
|
||||
$('.js-new-label-btn').disable()
|
||||
$newLabelCreateButton.enable()
|
||||
else
|
||||
$newLabelCreateButton.disable()
|
||||
|
||||
# Create new label with API
|
||||
Api.newLabel projectId, {
|
||||
name: newLabelField.val()
|
||||
color: newColorField.val()
|
||||
}, (label) ->
|
||||
$('.js-new-label-btn').enable()
|
||||
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
|
||||
newLabelField.on 'keyup change', enableLabelCreateButton
|
||||
|
||||
newColorField.on 'keyup change', enableLabelCreateButton
|
||||
|
||||
# Send the API call to create the label
|
||||
$newLabelCreateButton
|
||||
.disable()
|
||||
.on 'click', (e) ->
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if newLabelField.val() isnt '' and newColorField.val() isnt ''
|
||||
$newLabelError.hide()
|
||||
$('.js-new-label-btn').disable()
|
||||
|
||||
# Create new label with API
|
||||
Api.newLabel projectId, {
|
||||
name: newLabelField.val()
|
||||
color: newColorField.val()
|
||||
}, (label) ->
|
||||
$('.js-new-label-btn').enable()
|
||||
|
||||
if label.message?
|
||||
$newLabelError
|
||||
.text label.message
|
||||
.show()
|
||||
else
|
||||
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
|
||||
|
||||
$dropdown.glDropdown(
|
||||
data: (term, callback) ->
|
||||
@@ -68,8 +121,11 @@ class @LabelsSelect
|
||||
else
|
||||
selected = if label.title is selectedLabel then 'is-active' else ''
|
||||
|
||||
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
|
||||
|
||||
"<li>
|
||||
<a href='#' class='#{selected}'>
|
||||
#{color}
|
||||
#{label.title}
|
||||
</a>
|
||||
</li>"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
class @Profile
|
||||
constructor: ->
|
||||
constructor: (opts = {}) ->
|
||||
{
|
||||
@form = $('.edit-user')
|
||||
} = opts
|
||||
|
||||
# Automatically submit the Preferences form when any of its radio buttons change
|
||||
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
|
||||
$(this).parents('form').submit()
|
||||
@@ -17,14 +21,46 @@ class @Profile
|
||||
$('.update-notifications').on 'ajax:complete', ->
|
||||
$(this).find('.btn-save').enable()
|
||||
|
||||
$('.js-choose-user-avatar-button').bind "click", ->
|
||||
form = $(this).closest("form")
|
||||
form.find(".js-user-avatar-input").click()
|
||||
@bindEvents()
|
||||
|
||||
$('.js-user-avatar-input').bind "change", ->
|
||||
form = $(this).closest("form")
|
||||
filename = $(this).val().replace(/^.*[\\\/]/, '')
|
||||
form.find(".js-avatar-filename").text(filename)
|
||||
cropOpts =
|
||||
filename: '.js-avatar-filename'
|
||||
previewImage: '.avatar-image .avatar'
|
||||
modalCrop: '.modal-profile-crop'
|
||||
pickImageEl: '.js-choose-user-avatar-button'
|
||||
uploadImageBtn: '.js-upload-user-avatar'
|
||||
modalCropImg: '.modal-profile-crop-image'
|
||||
|
||||
@avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
|
||||
|
||||
bindEvents: ->
|
||||
@form.on 'submit', @onSubmitForm
|
||||
|
||||
onSubmitForm: (e) =>
|
||||
e.preventDefault()
|
||||
@saveForm()
|
||||
|
||||
saveForm: ->
|
||||
self = @
|
||||
|
||||
formData = new FormData(@form[0])
|
||||
formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
|
||||
|
||||
$.ajax
|
||||
url: @form.attr('action')
|
||||
type: @form.attr('method')
|
||||
data: formData
|
||||
dataType: "json"
|
||||
processData: false
|
||||
contentType: false
|
||||
success: (response) ->
|
||||
new Flash(response.message, 'notice')
|
||||
error: (jqXHR) ->
|
||||
new Flash(jqXHR.responseJSON.message, 'alert')
|
||||
complete: ->
|
||||
window.scrollTo 0, 0
|
||||
# Enable submit button after requests ends
|
||||
self.form.find(':input[disabled]').enable()
|
||||
|
||||
$ ->
|
||||
# Extract the SSH Key title from its comment
|
||||
|
||||
@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
|
||||
toggleSidebar = ->
|
||||
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
|
||||
$('header').toggleClass("header-collapsed header-expanded")
|
||||
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
|
||||
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
|
||||
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
|
||||
|
||||
|
||||
@@ -95,4 +95,4 @@ window.ContributorsStatGraphUtil =
|
||||
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
|
||||
true
|
||||
else
|
||||
false
|
||||
false
|
||||
|
||||
@@ -30,6 +30,7 @@ class @UsersSelect
|
||||
if showNullUser
|
||||
showDivider += 1
|
||||
users.unshift(
|
||||
beforeDivider: true
|
||||
name: 'Unassigned',
|
||||
id: 0
|
||||
)
|
||||
@@ -39,6 +40,7 @@ class @UsersSelect
|
||||
name = showAnyUser
|
||||
name = 'Any User' if name == true
|
||||
anyUser = {
|
||||
beforeDivider: true
|
||||
name: name,
|
||||
id: null
|
||||
}
|
||||
@@ -75,20 +77,27 @@ class @UsersSelect
|
||||
selected = if user.id is selectedId then "is-active" else ""
|
||||
img = ""
|
||||
|
||||
if avatar
|
||||
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
|
||||
|
||||
"<li>
|
||||
<a href='#' class='dropdown-menu-user-link #{selected}'>
|
||||
#{img}
|
||||
<strong class='dropdown-menu-user-full-name'>
|
||||
if user.beforeDivider?
|
||||
"<li>
|
||||
<a href='#' class='#{selected}'>
|
||||
#{user.name}
|
||||
</strong>
|
||||
<span class='dropdown-menu-user-username'>
|
||||
#{username}
|
||||
</span>
|
||||
</a>
|
||||
</li>"
|
||||
</a>
|
||||
</li>"
|
||||
else
|
||||
if avatar
|
||||
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
|
||||
|
||||
"<li>
|
||||
<a href='#' class='dropdown-menu-user-link #{selected}'>
|
||||
#{img}
|
||||
<strong class='dropdown-menu-user-full-name'>
|
||||
#{user.name}
|
||||
</strong>
|
||||
<span class='dropdown-menu-user-username'>
|
||||
#{username}
|
||||
</span>
|
||||
</a>
|
||||
</li>"
|
||||
)
|
||||
|
||||
$('.ajax-users-select').each (i, select) =>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*= require_self
|
||||
*= require dropzone/basic
|
||||
*= require cal-heatmap
|
||||
*= require cropper.css
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -107,10 +107,28 @@
|
||||
margin: 0;
|
||||
font-size: 23px;
|
||||
font-weight: normal;
|
||||
margin: 16px 0 5px 0;
|
||||
margin: 16px 0 5px;
|
||||
color: #4c4e54;
|
||||
font-size: 23px;
|
||||
line-height: 1.1;
|
||||
|
||||
h1 {
|
||||
color: #313236;
|
||||
margin-bottom: 6px;
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.visibility-icon {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-size: 18px;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0 $gl-padding;
|
||||
color: #5c5d5e;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-desc {
|
||||
|
||||
@@ -292,8 +292,11 @@ table {
|
||||
}
|
||||
|
||||
.btn-sign-in {
|
||||
margin-top: 10px;
|
||||
text-shadow: none;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
margin-top: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-filters {
|
||||
@@ -375,7 +378,6 @@ table {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 250px !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
width: 240px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 0;
|
||||
padding: 10px 10px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
background-color: $dropdown-bg;
|
||||
@@ -130,6 +130,12 @@
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.dropdown-menu-empty-link {
|
||||
&.is-focused {
|
||||
background-color: $dropdown-empty-row-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +189,7 @@
|
||||
}
|
||||
|
||||
.dropdown-select {
|
||||
width: 280px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.dropdown-menu-align-right {
|
||||
@@ -237,7 +243,7 @@
|
||||
|
||||
.dropdown-title-button {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
color: $dropdown-title-btn-color;
|
||||
font-size: 14px;
|
||||
@@ -270,6 +276,22 @@
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-input-clear {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&.has-value {
|
||||
.dropdown-input-clear {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-input-search {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-input-field {
|
||||
@@ -286,13 +308,13 @@
|
||||
border-color: $dropdown-input-focus-border;
|
||||
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
|
||||
|
||||
+ .fa {
|
||||
~ .fa {
|
||||
color: $dropdown-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ .fa {
|
||||
~ .fa {
|
||||
color: $dropdown-link-color;
|
||||
}
|
||||
}
|
||||
@@ -338,11 +360,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-labels {
|
||||
.label {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
margin-right: 5px;
|
||||
text-indent: -99999px;
|
||||
}
|
||||
.dropdown-label-box {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
@@ -11,3 +11,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.filter-item {
|
||||
display: block;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
|
||||
// the way the `src` property is formatted in this file.
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
|
||||
@@ -117,4 +117,4 @@ body {
|
||||
&.ui_violet {
|
||||
@include gitlab-theme(#98c, $theme-violet, #436, #325);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ header {
|
||||
|
||||
.header-content {
|
||||
height: $header-height;
|
||||
padding-right: 20px;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
|
||||
@@ -16,7 +16,7 @@ body {
|
||||
}
|
||||
|
||||
.container .content {
|
||||
margin: 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navless-container {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Generic mixins
|
||||
*/
|
||||
@mixin box-shadow($shadow) {
|
||||
@mixin box-shadow($shadow) {
|
||||
-webkit-box-shadow: $shadow;
|
||||
-moz-box-shadow: $shadow;
|
||||
-ms-box-shadow: $shadow;
|
||||
|
||||
@@ -102,6 +102,10 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 34px;
|
||||
display: inline-block;
|
||||
@@ -124,9 +128,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide on extra small devices (phones) */
|
||||
@media (max-width: $screen-xs-max) {
|
||||
display: none;
|
||||
padding-bottom: 0;
|
||||
|
||||
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
|
||||
margin: 0 0 10px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
display: block;
|
||||
height: auto;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-short {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// Applies on /dashboard/issues
|
||||
.project-item-select-holder {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small devices (tablets, 768px and lower) */
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
#logo {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: 58px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-with-sidebar {
|
||||
padding-top: $header-height;
|
||||
transition-duration: .3s;
|
||||
@@ -18,28 +25,10 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#logo {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: 58px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.right-sidebar-expanded {
|
||||
/* Extra small devices (phones, less than 768px) */
|
||||
/* No media query since this is the default in Bootstrap */
|
||||
padding-right: 0;
|
||||
/* Small devices (tablets, 768px and up) */
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: $gutter_width;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
z-index: 999;
|
||||
z-index: 1000;
|
||||
background: $background-color;
|
||||
}
|
||||
|
||||
@@ -202,53 +191,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin expanded-sidebar {
|
||||
padding-left: $sidebar_collapsed_width;
|
||||
.collapse-nav a {
|
||||
width: $sidebar_width;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 13px;
|
||||
background: transparent;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
transition-duration: .3s;
|
||||
outline: none;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
padding-left: $sidebar_width;
|
||||
}
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
/* Extra small devices (phones, less than 768px) */
|
||||
padding-right: 0;
|
||||
/* Small devices (tablets, 768px and up) */
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar_width;
|
||||
|
||||
.nav-sidebar {
|
||||
width: $sidebar_width;
|
||||
}
|
||||
|
||||
.nav-sidebar li a{
|
||||
width: 230px;
|
||||
|
||||
&.back-link {
|
||||
i {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin collapsed-sidebar {
|
||||
.page-sidebar-collapsed {
|
||||
padding-left: $sidebar_collapsed_width;
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
/* Extra small devices (phones, less than 768px) */
|
||||
padding-right: 0;
|
||||
/* Small devices (tablets, 768px and up) */
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@@ -293,35 +256,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-nav a {
|
||||
width: $sidebar_width;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 13px;
|
||||
background: transparent;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
transition-duration: .3s;
|
||||
outline: none;
|
||||
}
|
||||
.page-sidebar-expanded {
|
||||
padding-left: $sidebar_collapsed_width;
|
||||
|
||||
.collapse-nav a:hover {
|
||||
text-decoration: none;
|
||||
background: #f2f6f7;
|
||||
}
|
||||
@media (min-width: $screen-md-min) {
|
||||
padding-left: $sidebar_width;
|
||||
}
|
||||
|
||||
.page-sidebar-collapsed {
|
||||
/* Extra small devices (phones, less than 768px) */
|
||||
@include collapsed-sidebar;
|
||||
padding-right: 0;
|
||||
/* Small devices (tablets, 768px and up) */
|
||||
@media (min-width: $screen-sm-min) {
|
||||
@include collapsed-sidebar;
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar_width;
|
||||
|
||||
.nav-sidebar {
|
||||
width: $sidebar_width;
|
||||
}
|
||||
|
||||
.nav-sidebar li a {
|
||||
width: 230px;
|
||||
|
||||
&.back-link {
|
||||
i {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-sidebar-expanded {
|
||||
@include expanded-sidebar;
|
||||
.right-sidebar-collapsed {
|
||||
padding-right: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar-expanded {
|
||||
padding-right: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
padding-right: $gutter_width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
h1 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
margin: 24px 0 12px 0;
|
||||
padding: 0 0 10px 0;
|
||||
margin: 24px 0 12px;
|
||||
padding: 0 0 10px;
|
||||
border-bottom: 1px solid #e7e9ed;
|
||||
color: #313236;
|
||||
}
|
||||
@@ -48,27 +48,27 @@
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
margin: 24px 0 12px 0;
|
||||
margin: 24px 0 12px;
|
||||
color: #313236;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 24px 0 12px 0;
|
||||
margin: 24px 0 12px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 24px 0 12px 0;
|
||||
margin: 24px 0 12px;
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 24px 0 12px 0;
|
||||
margin: 24px 0 12px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 24px 0 12px 0;
|
||||
margin: 24px 0 12px;
|
||||
font-size: 0.90em;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
color: #7f8fa4;
|
||||
font-size: inherit;
|
||||
padding: 8px 21px;
|
||||
margin: 12px 0 12px;
|
||||
margin: 12px 0;
|
||||
border-left: 3px solid #e7e9ed;
|
||||
}
|
||||
|
||||
@@ -88,13 +88,13 @@
|
||||
|
||||
p {
|
||||
color: #5c5d5e;
|
||||
margin: 6px 0 0 0;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
table {
|
||||
@extend .table;
|
||||
@extend .table-bordered;
|
||||
margin: 12px 0 12px 0;
|
||||
margin: 12px 0;
|
||||
color: #5c5d5e;
|
||||
th {
|
||||
background: #f8fafc;
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 12px 0 12px 0;
|
||||
margin: 12px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6em;
|
||||
overflow-x: auto;
|
||||
@@ -191,7 +191,7 @@ body {
|
||||
line-height: 1.3;
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin: 12px 7px 12px 7px;
|
||||
margin: 12px 7px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
@@ -168,13 +168,14 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||
*/
|
||||
$dropdown-bg: #fff;
|
||||
$dropdown-link-color: #555;
|
||||
$dropdown-link-hover-bg: rgba(#000, .04);
|
||||
$dropdown-link-hover-bg: $row-hover;
|
||||
$dropdown-empty-row-bg: rgba(#000, .04);
|
||||
$dropdown-border-color: rgba(#000, .1);
|
||||
$dropdown-shadow-color: rgba(#000, .1);
|
||||
$dropdown-divider-color: rgba(#000, .1);
|
||||
$dropdown-header-color: #959494;
|
||||
$dropdown-title-btn-color: #bfbfbf;
|
||||
$dropdown-input-color: #c7c7c7;
|
||||
$dropdown-input-color: #555;
|
||||
$dropdown-input-focus-border: rgb(58, 171, 240);
|
||||
$dropdown-input-focus-shadow: rgba(#000, .2);
|
||||
$dropdown-loading-bg: rgba(#fff, .6);
|
||||
|
||||
@@ -3,12 +3,12 @@ img {
|
||||
height: auto;
|
||||
}
|
||||
p.details {
|
||||
font-style:italic;
|
||||
color:#777
|
||||
font-style: italic;
|
||||
color: #777
|
||||
}
|
||||
.footer p {
|
||||
font-size:small;
|
||||
color:#777
|
||||
font-size: small;
|
||||
color: #777
|
||||
}
|
||||
pre.commit-message {
|
||||
white-space: pre-wrap;
|
||||
@@ -20,5 +20,5 @@ pre.commit-message {
|
||||
color: #090;
|
||||
}
|
||||
.file-stats .deleted-file {
|
||||
color: #B00;
|
||||
color: #b00;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
background: #fff
|
||||
}
|
||||
|
||||
.visibility-levels {
|
||||
.controls {
|
||||
margin-bottom: 9px;
|
||||
.visibility-levels {
|
||||
.controls {
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
i {
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
}
|
||||
.image-info {
|
||||
font-size: 12px;
|
||||
margin: 5px 0 0 0;
|
||||
margin: 5px 0 0;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
.block {
|
||||
width: $sidebar_collapsed_width - 1px;
|
||||
margin-left: -19px;
|
||||
padding: 15px 0 0 0;
|
||||
padding: 15px 0 0;
|
||||
border-bottom: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -273,12 +273,12 @@
|
||||
}
|
||||
|
||||
.participants-list {
|
||||
margin: -5px -5px;
|
||||
margin: -5px;
|
||||
}
|
||||
|
||||
.participants-author {
|
||||
display: inline-block;
|
||||
padding: 5px 5px;
|
||||
padding: 5px;
|
||||
|
||||
.author_link {
|
||||
display: block;
|
||||
|
||||
@@ -9,28 +9,45 @@
|
||||
}
|
||||
|
||||
&.suggest-colors-dropdown {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
@include border-radius(0);
|
||||
width: 36.7px;
|
||||
width: (100% / 7);
|
||||
margin-right: 0;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label-color-preview {
|
||||
display: none;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
.dropdown-new-label {
|
||||
.dropdown-content {
|
||||
max-height: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label-color-input {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.is-active {
|
||||
display: block;
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label-color-preview {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-top-left-radius: $border-radius-base;
|
||||
border-bottom-left-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.label-row {
|
||||
.label {
|
||||
padding: 9px;
|
||||
@@ -45,3 +62,10 @@
|
||||
.label-subscription {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-labels-error {
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: $gl-danger;
|
||||
color: $white-light;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
.login-heading h3 {
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 10px 0;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
.account-well {
|
||||
padding: 10px 10px;
|
||||
padding: 10px;
|
||||
background-color: $help-well-bg;
|
||||
border: 1px solid $help-well-border;
|
||||
border-radius: $border-radius-base;
|
||||
@@ -197,3 +197,24 @@
|
||||
width: 105px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-profile-crop {
|
||||
.modal-dialog {
|
||||
width: 380px;
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.profile-crop-image-container {
|
||||
height: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.crop-controls {
|
||||
padding: 10px 0 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,28 +68,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.project-home-desc {
|
||||
h1 {
|
||||
color: #313236;
|
||||
margin: 0;
|
||||
margin-bottom: 6px;
|
||||
font-size: 23px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.visibility-icon {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-size: 18px;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0 $gl-padding;
|
||||
color: #5c5d5e;
|
||||
}
|
||||
}
|
||||
|
||||
.project-repo-buttons {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 0;
|
||||
@@ -333,7 +311,7 @@ pre.light-well {
|
||||
}
|
||||
|
||||
.git-empty {
|
||||
margin: 0 7px 0 7px;
|
||||
margin: 0 7px;
|
||||
|
||||
h5 {
|
||||
color: #5c5d5e;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#contributors {
|
||||
.contributors-list {
|
||||
margin: 0 0 10px 0;
|
||||
margin: 0 0 10px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -21,19 +21,19 @@
|
||||
$l-white: #fff;
|
||||
|
||||
.term-bold {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
.term-italic {
|
||||
font-style: italic;
|
||||
font-style: italic;
|
||||
}
|
||||
.term-conceal {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
.term-underline {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.term-cross {
|
||||
text-decoration: line-through;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.term-fg-black {
|
||||
|
||||
@@ -61,6 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||
:session_expire_delay,
|
||||
:default_project_visibility,
|
||||
:default_snippet_visibility,
|
||||
:default_group_visibility,
|
||||
:restricted_signup_domains_raw,
|
||||
:version_check_enabled,
|
||||
:admin_notification_email,
|
||||
|
||||
@@ -59,6 +59,6 @@ class Admin::GroupsController < Admin::ApplicationController
|
||||
end
|
||||
|
||||
def group_params
|
||||
params.require(:group).permit(:name, :description, :path, :avatar)
|
||||
params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Admin::ProjectsController < Admin::ApplicationController
|
||||
before_action :project, only: [:show, :transfer]
|
||||
before_action :group, only: [:show, :transfer]
|
||||
before_action :repository, only: [:show, :transfer]
|
||||
|
||||
def index
|
||||
@projects = Project.all
|
||||
|
||||
@@ -23,7 +23,6 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
helper_method :abilities, :can?, :current_application_settings
|
||||
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
|
||||
helper_method :repository, :can_collaborate_with_project?
|
||||
|
||||
rescue_from Encoding::CompatibilityError do |exception|
|
||||
log_exception(exception)
|
||||
@@ -116,47 +115,6 @@ class ApplicationController < ActionController::Base
|
||||
abilities.allowed?(object, action, subject)
|
||||
end
|
||||
|
||||
def project
|
||||
unless @project
|
||||
namespace = params[:namespace_id]
|
||||
id = params[:project_id] || params[:id]
|
||||
|
||||
# Redirect from
|
||||
# localhost/group/project.git
|
||||
# to
|
||||
# localhost/group/project
|
||||
#
|
||||
if id =~ /\.git\Z/
|
||||
redirect_to request.original_url.gsub(/\.git\/?\Z/, '') and return
|
||||
end
|
||||
|
||||
project_path = "#{namespace}/#{id}"
|
||||
@project = Project.find_with_namespace(project_path)
|
||||
|
||||
if @project and can?(current_user, :read_project, @project)
|
||||
if @project.path_with_namespace != project_path
|
||||
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
|
||||
end
|
||||
@project
|
||||
elsif current_user.nil?
|
||||
@project = nil
|
||||
authenticate_user!
|
||||
else
|
||||
@project = nil
|
||||
render_404 and return
|
||||
end
|
||||
end
|
||||
@project
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository ||= project.repository
|
||||
end
|
||||
|
||||
def authorize_project!(action)
|
||||
return access_denied! unless can?(current_user, action, project)
|
||||
end
|
||||
|
||||
def access_denied!
|
||||
render "errors/access_denied", layout: "errors", status: 404
|
||||
end
|
||||
@@ -165,14 +123,6 @@ class ApplicationController < ActionController::Base
|
||||
render "errors/git_not_found.html", layout: "errors", status: 404
|
||||
end
|
||||
|
||||
def method_missing(method_sym, *arguments, &block)
|
||||
if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
|
||||
authorize_project!($1.to_sym)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def render_403
|
||||
head :forbidden
|
||||
end
|
||||
@@ -181,10 +131,6 @@ class ApplicationController < ActionController::Base
|
||||
render file: Rails.root.join("public", "404"), layout: false, status: "404"
|
||||
end
|
||||
|
||||
def require_non_empty_project
|
||||
redirect_to @project if @project.empty_repo?
|
||||
end
|
||||
|
||||
def no_cache_headers
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
@@ -410,13 +356,6 @@ class ApplicationController < ActionController::Base
|
||||
current_user.nil? && root_path == request.path
|
||||
end
|
||||
|
||||
def can_collaborate_with_project?(project = nil)
|
||||
project ||= @project
|
||||
|
||||
can?(current_user, :push_code, project) ||
|
||||
(current_user && current_user.already_forked?(project))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_default_sort
|
||||
|
||||
@@ -6,7 +6,6 @@ module GlobalMilestones
|
||||
@milestones = MilestonesFinder.new.execute(@projects, params)
|
||||
@milestones = GlobalMilestone.build_collection(@milestones)
|
||||
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
|
||||
@milestones = Kaminari.paginate_array(@milestones).page(params[:page])
|
||||
end
|
||||
|
||||
def milestone
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
module IssuableActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authorize_destroy_issuable!, only: :destroy
|
||||
end
|
||||
|
||||
def destroy
|
||||
issuable.destroy
|
||||
|
||||
name = issuable.class.name.titleize.downcase
|
||||
flash[:notice] = "The #{name} was successfully deleted."
|
||||
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_destroy_issuable!
|
||||
unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
|
||||
return access_denied!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,9 @@
|
||||
class Dashboard::ApplicationController < ApplicationController
|
||||
layout 'dashboard'
|
||||
|
||||
private
|
||||
|
||||
def projects
|
||||
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class Dashboard::LabelsController < Dashboard::ApplicationController
|
||||
def index
|
||||
labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: labels }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
|
||||
include GlobalMilestones
|
||||
|
||||
before_action :projects
|
||||
before_action :milestones, only: [:index]
|
||||
before_action :milestone, only: [:show]
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
|
||||
end
|
||||
format.json do
|
||||
render json: milestones
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects
|
||||
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
|
||||
include MergeRequestsAction
|
||||
|
||||
before_action :event_filter, only: :activity
|
||||
before_action :projects, only: [:issues, :merge_requests, :labels, :milestones]
|
||||
before_action :projects, only: [:issues, :merge_requests]
|
||||
|
||||
respond_to :html
|
||||
|
||||
@@ -20,29 +20,6 @@ class DashboardController < Dashboard::ApplicationController
|
||||
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
|
||||
|
||||
def load_events
|
||||
@@ -57,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController
|
||||
@events = @event_filter.apply_filter(@events).with_associations
|
||||
@events = @events.limit(20).offset(params[:offset] || 0)
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Explore::GroupsController < Explore::ApplicationController
|
||||
def index
|
||||
@groups = Group.order_id_desc
|
||||
@groups = GroupsFinder.new.execute(current_user)
|
||||
@groups = @groups.search(params[:search]) if params[:search].present?
|
||||
@groups = @groups.sort(@sort = params[:sort])
|
||||
@groups = @groups.page(params[:page])
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
class Groups::ApplicationController < ApplicationController
|
||||
layout 'group'
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
before_action :group
|
||||
|
||||
private
|
||||
|
||||
def group
|
||||
@group ||= Group.find_by(path: params[:group_id])
|
||||
end
|
||||
unless @group
|
||||
id = params[:group_id] || params[:id]
|
||||
@group = Group.find_by(path: id)
|
||||
|
||||
def authorize_read_group!
|
||||
unless @group and can?(current_user, :read_group, @group)
|
||||
if current_user.nil?
|
||||
return authenticate_user!
|
||||
else
|
||||
return render_404
|
||||
unless @group && can?(current_user, :read_group, @group)
|
||||
@group = nil
|
||||
|
||||
if current_user.nil?
|
||||
authenticate_user!
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@group
|
||||
end
|
||||
|
||||
def group_projects
|
||||
@projects ||= GroupProjectsFinder.new(group).execute(current_user)
|
||||
end
|
||||
|
||||
def authorize_admin_group!
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Groups::AvatarsController < Groups::ApplicationController
|
||||
before_action :authorize_admin_group!
|
||||
|
||||
def destroy
|
||||
@group.remove_avatar!
|
||||
@group.save
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
class Groups::GroupMembersController < Groups::ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_read_group!
|
||||
before_action :authorize_admin_group_member!, except: [:index, :leave]
|
||||
|
||||
def index
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
class Groups::MilestonesController < Groups::ApplicationController
|
||||
include GlobalMilestones
|
||||
|
||||
before_action :projects
|
||||
before_action :milestones, only: [:index]
|
||||
before_action :group_projects
|
||||
before_action :milestone, only: [:show, :update]
|
||||
before_action :authorize_group_milestone!, only: [:create, :update]
|
||||
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@@ -17,7 +21,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||
project_ids = params[:milestone][:project_ids]
|
||||
title = milestone_params[:title]
|
||||
|
||||
@group.projects.where(id: project_ids).each do |project|
|
||||
@projects.where(id: project_ids).each do |project|
|
||||
Milestones::CreateService.new(project, current_user, milestone_params).execute
|
||||
end
|
||||
|
||||
@@ -37,7 +41,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def authorize_group_milestone!
|
||||
def authorize_admin_milestones!
|
||||
return render_404 unless can?(current_user, :admin_milestones, group)
|
||||
end
|
||||
|
||||
@@ -48,8 +52,4 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||
def milestone_path(title)
|
||||
group_milestone_path(@group, title.to_slug.to_s, title: title)
|
||||
end
|
||||
|
||||
def projects
|
||||
@projects ||= @group.projects
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,16 +5,15 @@ class GroupsController < Groups::ApplicationController
|
||||
|
||||
respond_to :html
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:index, :show, :issues, :merge_requests]
|
||||
before_action :authenticate_user!, only: [:new, :create]
|
||||
before_action :group, except: [:index, :new, :create]
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_read_group!, except: [:index, :show, :new, :create, :autocomplete]
|
||||
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
|
||||
before_action :authorize_create_group!, only: [:new, :create]
|
||||
|
||||
# Load group projects
|
||||
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
|
||||
before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
|
||||
before_action :event_filter, only: [:activity]
|
||||
|
||||
layout :determine_layout
|
||||
@@ -28,11 +27,9 @@ class GroupsController < Groups::ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@group = Group.new(group_params)
|
||||
@group.name = @group.path.dup unless @group.name
|
||||
@group = Groups::CreateService.new(current_user, group_params).execute
|
||||
|
||||
if @group.save
|
||||
@group.add_owner(current_user)
|
||||
if @group.persisted?
|
||||
redirect_to @group, notice: "Group '#{@group.name}' was successfully created."
|
||||
else
|
||||
render action: "new"
|
||||
@@ -41,12 +38,13 @@ class GroupsController < Groups::ApplicationController
|
||||
|
||||
def show
|
||||
@last_push = current_user.recent_push if current_user
|
||||
|
||||
@projects = @projects.includes(:namespace)
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = @projects.sort(@sort = params[:sort])
|
||||
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
|
||||
|
||||
@shared_projects = @group.shared_projects
|
||||
@shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
@@ -83,7 +81,7 @@ class GroupsController < Groups::ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if @group.update_attributes(group_params)
|
||||
if Groups::UpdateService.new(@group, current_user, group_params).execute
|
||||
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
|
||||
else
|
||||
render action: "edit"
|
||||
@@ -98,26 +96,6 @@ class GroupsController < Groups::ApplicationController
|
||||
|
||||
protected
|
||||
|
||||
def group
|
||||
@group ||= Group.find_by(path: params[:id])
|
||||
@group || render_404
|
||||
end
|
||||
|
||||
def load_projects
|
||||
@projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity
|
||||
end
|
||||
|
||||
# Dont allow unauthorized access to group
|
||||
def authorize_read_group!
|
||||
unless @group and (@projects.present? or can?(current_user, :read_group, @group))
|
||||
if current_user.nil?
|
||||
return authenticate_user!
|
||||
else
|
||||
return render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_create_group!
|
||||
unless can?(current_user, :create_group, nil)
|
||||
return render_404
|
||||
@@ -135,7 +113,7 @@ class GroupsController < Groups::ApplicationController
|
||||
end
|
||||
|
||||
def group_params
|
||||
params.require(:group).permit(:name, :description, :path, :avatar, :public, :share_with_group_lock)
|
||||
params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock)
|
||||
end
|
||||
|
||||
def load_events
|
||||
|
||||
@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
|
||||
|
||||
if user
|
||||
redirect_to user_path(user)
|
||||
elsif group
|
||||
elsif group && can?(current_user, :read_group, namespace)
|
||||
redirect_to group_path(group)
|
||||
elsif current_user.nil?
|
||||
authenticate_user!
|
||||
|
||||
@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController
|
||||
def update
|
||||
user_params.except!(:email) if @user.ldap_user?
|
||||
|
||||
if @user.update_attributes(user_params)
|
||||
flash[:notice] = "Profile was successfully updated"
|
||||
else
|
||||
messages = @user.errors.full_messages.uniq.join('. ')
|
||||
flash[:alert] = "Failed to update profile. #{messages}"
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back_or_default(default: { action: 'show' }) }
|
||||
if @user.update_attributes(user_params)
|
||||
message = "Profile was successfully updated"
|
||||
format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
|
||||
format.json { render json: { message: message } }
|
||||
else
|
||||
message = @user.errors.full_messages.uniq.join('. ')
|
||||
format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
|
||||
format.json { render json: { message: message }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,20 +1,74 @@
|
||||
class Projects::ApplicationController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
before_action :project
|
||||
before_action :repository
|
||||
layout 'project'
|
||||
|
||||
def authenticate_user!
|
||||
# Restrict access to Projects area only
|
||||
# for non-signed users
|
||||
if !current_user
|
||||
id = params[:project_id] || params[:id]
|
||||
project_with_namespace = "#{params[:namespace_id]}/#{id}"
|
||||
@project = Project.find_with_namespace(project_with_namespace)
|
||||
helper_method :repository, :can_collaborate_with_project?
|
||||
|
||||
return if @project && @project.public?
|
||||
private
|
||||
|
||||
def project
|
||||
unless @project
|
||||
namespace = params[:namespace_id]
|
||||
id = params[:project_id] || params[:id]
|
||||
|
||||
# Redirect from
|
||||
# localhost/group/project.git
|
||||
# to
|
||||
# localhost/group/project
|
||||
#
|
||||
if id =~ /\.git\Z/
|
||||
redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
|
||||
return
|
||||
end
|
||||
|
||||
project_path = "#{namespace}/#{id}"
|
||||
@project = Project.find_with_namespace(project_path)
|
||||
|
||||
if @project && can?(current_user, :read_project, @project)
|
||||
if @project.path_with_namespace != project_path
|
||||
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace)
|
||||
end
|
||||
else
|
||||
@project = nil
|
||||
|
||||
if current_user.nil?
|
||||
authenticate_user!
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
super
|
||||
@project
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository ||= project.repository
|
||||
end
|
||||
|
||||
def can_collaborate_with_project?(project = nil)
|
||||
project ||= @project
|
||||
|
||||
can?(current_user, :push_code, project) ||
|
||||
(current_user && current_user.already_forked?(project))
|
||||
end
|
||||
|
||||
def authorize_project!(action)
|
||||
return access_denied! unless can?(current_user, action, project)
|
||||
end
|
||||
|
||||
def method_missing(method_sym, *arguments, &block)
|
||||
if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
|
||||
authorize_project!($1.to_sym)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def require_non_empty_project
|
||||
redirect_to namespace_project_path(@project.namespace, @project) if @project.empty_repo?
|
||||
end
|
||||
|
||||
def require_branch_head
|
||||
@@ -26,8 +80,6 @@ class Projects::ApplicationController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_diff_view_cookie!
|
||||
view = params[:view] || cookies[:diff_view]
|
||||
cookies.permanent[:diff_view] = params[:view] = view if view
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Projects::AvatarsController < Projects::ApplicationController
|
||||
include BlobHelper
|
||||
|
||||
before_action :project
|
||||
before_action :authorize_admin_project!, only: [:destroy]
|
||||
|
||||
def show
|
||||
@blob = @repository.blob_at_branch('master', @project.avatar_in_git)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Projects::IssuesController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
include IssuableActions
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :issue, only: [:edit, :update, :show]
|
||||
@@ -133,6 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||
end
|
||||
end
|
||||
alias_method :subscribable_resource, :issue
|
||||
alias_method :issuable, :issue
|
||||
|
||||
def authorize_read_issue!
|
||||
return render_404 unless can?(current_user, :read_issue, @issue)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
include DiffHelper
|
||||
include IssuableActions
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :merge_request, only: [
|
||||
@@ -255,6 +256,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
|
||||
end
|
||||
alias_method :subscribable_resource, :merge_request
|
||||
alias_method :issuable, :merge_request
|
||||
|
||||
def closes_issues
|
||||
@closes_issues ||= @merge_request.closes_issues
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
class Projects::UploadsController < Projects::ApplicationController
|
||||
skip_before_action :authenticate_user!, :reject_blocked!, :project,
|
||||
skip_before_action :reject_blocked!, :project,
|
||||
:repository, if: -> { action_name == 'show' && image? }
|
||||
|
||||
before_action :authorize_upload_file!, only: [:create]
|
||||
|
||||
def create
|
||||
link_to_file = ::Projects::UploadService.new(project, params[:file]).
|
||||
execute
|
||||
@@ -26,6 +28,8 @@ class Projects::UploadsController < Projects::ApplicationController
|
||||
send_file uploader.file.path, disposition: disposition
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def uploader
|
||||
return @uploader if defined?(@uploader)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class ProjectsController < ApplicationController
|
||||
class ProjectsController < Projects::ApplicationController
|
||||
include ExtractsPath
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:show, :activity]
|
||||
before_action :authenticate_user!, except: [:show, :activity]
|
||||
before_action :project, except: [:new, :create]
|
||||
before_action :repository, except: [:new, :create]
|
||||
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
|
||||
|
||||
@@ -108,7 +108,7 @@ class UsersController < ApplicationController
|
||||
end
|
||||
|
||||
def load_groups
|
||||
@groups = @user.groups.order_id_desc
|
||||
@groups = JoinedGroupsFinder.new(@user).execute(current_user)
|
||||
end
|
||||
|
||||
def projects_for_current_user
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class ContributedProjectsFinder
|
||||
class ContributedProjectsFinder < UnionFinder
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
@@ -11,27 +11,19 @@ class ContributedProjectsFinder
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def execute(current_user = nil)
|
||||
if current_user
|
||||
relation = projects_visible_to_user(current_user)
|
||||
else
|
||||
relation = public_projects
|
||||
end
|
||||
segments = all_projects(current_user)
|
||||
|
||||
relation.includes(:namespace).order_id_desc
|
||||
find_union(segments, Project).includes(:namespace).order_id_desc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_visible_to_user(current_user)
|
||||
authorized = @user.contributed_projects.visible_to_user(current_user)
|
||||
def all_projects(current_user)
|
||||
projects = []
|
||||
|
||||
union = Gitlab::SQL::Union.
|
||||
new([authorized.select(:id), public_projects.select(:id)])
|
||||
projects << @user.contributed_projects.visible_to_user(current_user) if current_user
|
||||
projects << @user.contributed_projects.public_to_user(current_user)
|
||||
|
||||
Project.where("projects.id IN (#{union.to_sql})")
|
||||
end
|
||||
|
||||
def public_projects
|
||||
@user.contributed_projects.public_only
|
||||
projects
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
class GroupProjectsFinder < UnionFinder
|
||||
def initialize(group, options = {})
|
||||
@group = group
|
||||
@options = options
|
||||
end
|
||||
|
||||
def execute(current_user = nil)
|
||||
segments = group_projects(current_user)
|
||||
find_union(segments, Project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group_projects(current_user)
|
||||
only_owned = @options.fetch(:only_owned, false)
|
||||
only_shared = @options.fetch(:only_shared, false)
|
||||
|
||||
projects = []
|
||||
|
||||
if current_user
|
||||
if @group.users.include?(current_user)
|
||||
projects << @group.projects unless only_shared
|
||||
projects << @group.shared_projects unless only_owned
|
||||
else
|
||||
unless only_shared
|
||||
projects << @group.projects.visible_to_user(current_user)
|
||||
projects << @group.projects.public_to_user(current_user)
|
||||
end
|
||||
|
||||
unless only_owned
|
||||
projects << @group.shared_projects.visible_to_user(current_user)
|
||||
projects << @group.shared_projects.public_to_user(current_user)
|
||||
end
|
||||
end
|
||||
else
|
||||
projects << @group.projects.public_only unless only_shared
|
||||
projects << @group.shared_projects.public_only unless only_owned
|
||||
end
|
||||
|
||||
projects
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
class GroupsFinder < UnionFinder
|
||||
def execute(current_user = nil)
|
||||
segments = all_groups(current_user)
|
||||
|
||||
find_union(segments, Group).order_id_desc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_groups(current_user)
|
||||
groups = []
|
||||
|
||||
groups << current_user.authorized_groups if current_user
|
||||
groups << Group.unscoped.public_to_user(current_user)
|
||||
|
||||
groups
|
||||
end
|
||||
end
|
||||
@@ -80,9 +80,10 @@ class IssuableFinder
|
||||
@projects = project
|
||||
elsif current_user && params[:authorized_only].presence && !current_user_related?
|
||||
@projects = current_user.authorized_projects.reorder(nil)
|
||||
elsif group
|
||||
@projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
|
||||
else
|
||||
@projects = ProjectsFinder.new.execute(current_user, group: group).
|
||||
reorder(nil)
|
||||
@projects = ProjectsFinder.new.execute(current_user).reorder(nil)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -171,14 +172,12 @@ class IssuableFinder
|
||||
|
||||
def by_scope(items)
|
||||
case params[:scope]
|
||||
when 'created-by-me', 'authored' then
|
||||
when 'created-by-me', 'authored'
|
||||
items.where(author_id: current_user.id)
|
||||
when 'all' then
|
||||
items
|
||||
when 'assigned-to-me' then
|
||||
when 'assigned-to-me'
|
||||
items.where(assignee_id: current_user.id)
|
||||
else
|
||||
raise 'You must specify default scope'
|
||||
items
|
||||
end
|
||||
end
|
||||
|
||||
@@ -198,8 +197,7 @@ class IssuableFinder
|
||||
end
|
||||
|
||||
def by_group(items)
|
||||
items = items.of_group(group) if group
|
||||
|
||||
# Selection by group is already covered by `by_project` and `projects`
|
||||
items
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
class JoinedGroupsFinder < UnionFinder
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
# Finds the groups of the source user, optionally limited to those visible to
|
||||
# the current user.
|
||||
def execute(current_user = nil)
|
||||
segments = all_groups(current_user)
|
||||
|
||||
find_union(segments, Group).order_id_desc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_groups(current_user)
|
||||
groups = []
|
||||
|
||||
groups << @user.authorized_groups.visible_to_user(current_user) if current_user
|
||||
groups << @user.authorized_groups.public_to_user(current_user)
|
||||
|
||||
groups
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class PersonalProjectsFinder
|
||||
class PersonalProjectsFinder < UnionFinder
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
@@ -11,31 +11,19 @@ class PersonalProjectsFinder
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def execute(current_user = nil)
|
||||
if current_user
|
||||
relation = projects_visible_to_user(current_user)
|
||||
else
|
||||
relation = public_projects
|
||||
end
|
||||
segments = all_projects(current_user)
|
||||
|
||||
relation.includes(:namespace).order_id_desc
|
||||
find_union(segments, Project).includes(:namespace).order_id_desc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_visible_to_user(current_user)
|
||||
authorized = @user.personal_projects.visible_to_user(current_user)
|
||||
def all_projects(current_user)
|
||||
projects = []
|
||||
|
||||
union = Gitlab::SQL::Union.
|
||||
new([authorized.select(:id), public_and_internal_projects.select(:id)])
|
||||
projects << @user.personal_projects.visible_to_user(current_user) if current_user
|
||||
projects << @user.personal_projects.public_to_user(current_user)
|
||||
|
||||
Project.where("projects.id IN (#{union.to_sql})")
|
||||
end
|
||||
|
||||
def public_projects
|
||||
@user.personal_projects.public_only
|
||||
end
|
||||
|
||||
def public_and_internal_projects
|
||||
@user.personal_projects.public_and_internal_only
|
||||
projects
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,81 +1,18 @@
|
||||
class ProjectsFinder
|
||||
# Returns all projects, optionally including group projects a user has access
|
||||
# to.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# Retrieving all public projects:
|
||||
#
|
||||
# ProjectsFinder.new.execute
|
||||
#
|
||||
# Retrieving all public/internal projects and those the given user has access
|
||||
# to:
|
||||
#
|
||||
# ProjectsFinder.new.execute(some_user)
|
||||
#
|
||||
# Retrieving all public/internal projects as well as the group's projects the
|
||||
# user has access to:
|
||||
#
|
||||
# ProjectsFinder.new.execute(some_user, group: some_group)
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
class ProjectsFinder < UnionFinder
|
||||
def execute(current_user = nil, options = {})
|
||||
group = options[:group]
|
||||
segments = all_projects(current_user)
|
||||
|
||||
if group
|
||||
segments = group_projects(current_user, group)
|
||||
else
|
||||
segments = all_projects(current_user)
|
||||
end
|
||||
|
||||
if segments.length > 1
|
||||
union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
|
||||
|
||||
Project.where("projects.id IN (#{union.to_sql})")
|
||||
else
|
||||
segments.first
|
||||
end
|
||||
find_union(segments, Project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def group_projects(current_user, group)
|
||||
return [group.projects.public_only] unless current_user
|
||||
|
||||
user_group_projects = [
|
||||
group_projects_for_user(current_user, group),
|
||||
group.shared_projects.visible_to_user(current_user)
|
||||
]
|
||||
if current_user.external?
|
||||
user_group_projects << group.projects.public_only
|
||||
else
|
||||
user_group_projects << group.projects.public_and_internal_only
|
||||
end
|
||||
end
|
||||
|
||||
def all_projects(current_user)
|
||||
return [public_projects] unless current_user
|
||||
projects = []
|
||||
|
||||
if current_user.external?
|
||||
[current_user.authorized_projects, public_projects]
|
||||
else
|
||||
[current_user.authorized_projects, public_and_internal_projects]
|
||||
end
|
||||
end
|
||||
projects << current_user.authorized_projects if current_user
|
||||
projects << Project.unscoped.public_to_user(current_user)
|
||||
|
||||
def group_projects_for_user(current_user, group)
|
||||
if group.users.include?(current_user)
|
||||
group.projects
|
||||
else
|
||||
group.projects.visible_to_user(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def public_projects
|
||||
Project.unscoped.public_only
|
||||
end
|
||||
|
||||
def public_and_internal_projects
|
||||
Project.unscoped.public_and_internal_only
|
||||
projects
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class UnionFinder
|
||||
def find_union(segments, klass)
|
||||
if segments.length > 1
|
||||
union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
|
||||
|
||||
klass.where("#{klass.table_name}.id IN (#{union.to_sql})")
|
||||
else
|
||||
segments.first
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -27,7 +27,7 @@ module BlobHelper
|
||||
link_opts)
|
||||
|
||||
if !on_top_of_branch?(project, ref)
|
||||
button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
|
||||
button_tag "Edit", class: "btn btn-default disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
|
||||
elsif can_edit_blob?(blob, project, ref)
|
||||
link_to "Edit", edit_path, class: 'btn'
|
||||
elsif can?(current_user, :fork_project, project)
|
||||
@@ -50,9 +50,9 @@ module BlobHelper
|
||||
return unless blob
|
||||
|
||||
if !on_top_of_branch?(project, ref)
|
||||
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
|
||||
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
|
||||
elsif blob.lfs_pointer?
|
||||
button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
|
||||
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
|
||||
elsif can_edit_blob?(blob, project, ref)
|
||||
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
|
||||
elsif can?(current_user, :fork_project, project)
|
||||
|
||||
@@ -24,7 +24,7 @@ module ButtonHelper
|
||||
|
||||
def http_clone_button(project)
|
||||
klass = 'http-selector'
|
||||
klass << ' has_tooltip' if current_user.try(:require_password?)
|
||||
klass << ' has-tooltip' if current_user.try(:require_password?)
|
||||
|
||||
protocol = gitlab_config.protocol.upcase
|
||||
|
||||
@@ -41,7 +41,7 @@ module ButtonHelper
|
||||
|
||||
def ssh_clone_button(project)
|
||||
klass = 'ssh-selector'
|
||||
klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
|
||||
klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
|
||||
|
||||
content_tag :a, 'SSH',
|
||||
class: klass,
|
||||
|
||||
@@ -182,7 +182,7 @@ module CommitsHelper
|
||||
end
|
||||
|
||||
options = {
|
||||
class: "commit-#{options[:source]}-link has_tooltip",
|
||||
class: "commit-#{options[:source]}-link has-tooltip",
|
||||
data: { 'original-title'.to_sym => sanitize(source_email) }
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,8 @@ module DropdownsHelper
|
||||
def dropdown_filter(placeholder)
|
||||
content_tag :div, class: "dropdown-input" do
|
||||
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
|
||||
filter_output << icon('search')
|
||||
filter_output << icon('search', class: "dropdown-input-search")
|
||||
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
|
||||
|
||||
filter_output.html_safe
|
||||
end
|
||||
|
||||
@@ -19,6 +19,10 @@ module GroupsHelper
|
||||
end
|
||||
end
|
||||
|
||||
def can_change_group_visibility_level?(group)
|
||||
can?(current_user, :change_visibility_level, group)
|
||||
end
|
||||
|
||||
def group_icon(group)
|
||||
if group.is_a?(String)
|
||||
group = Group.find_by(path: group)
|
||||
|
||||
@@ -56,7 +56,7 @@ module LabelsHelper
|
||||
|
||||
# Intentionally not using content_tag here so that this method can be called
|
||||
# by LabelReferenceFilter
|
||||
span = %(<span class="label color-label #{"has_tooltip" if tooltip}" ) +
|
||||
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
|
||||
%(style="background-color: #{label_color}; color: #{text_color}" ) +
|
||||
%(title="#{escape_once(label.description)}" data-container="body">) +
|
||||
%(#{escape_once(label.name)}#{label_suffix}</span>)
|
||||
@@ -114,7 +114,7 @@ module LabelsHelper
|
||||
if @project
|
||||
namespace_project_labels_path(@project.namespace, @project, :json)
|
||||
else
|
||||
labels_dashboard_path(:json)
|
||||
dashboard_labels_path(:json)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ module MilestonesHelper
|
||||
if @project
|
||||
namespace_project_milestones_path(@project.namespace, @project, :json)
|
||||
else
|
||||
milestones_dashboard_path(:json)
|
||||
dashboard_milestones_path(:json)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ module ProjectsHelper
|
||||
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
|
||||
else
|
||||
title = opts[:title].sub(":name", sanitize(author.name))
|
||||
link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
|
||||
link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ module VisibilityLevelHelper
|
||||
case form_model
|
||||
when Project
|
||||
project_visibility_level_description(level)
|
||||
when Group
|
||||
group_visibility_level_description(level)
|
||||
when Snippet
|
||||
snippet_visibility_level_description(level, form_model)
|
||||
end
|
||||
@@ -35,6 +37,17 @@ module VisibilityLevelHelper
|
||||
end
|
||||
end
|
||||
|
||||
def group_visibility_level_description(level)
|
||||
case level
|
||||
when Gitlab::VisibilityLevel::PRIVATE
|
||||
"The group and its projects can only be viewed by members."
|
||||
when Gitlab::VisibilityLevel::INTERNAL
|
||||
"The group and any internal projects can be viewed by any logged in user."
|
||||
when Gitlab::VisibilityLevel::PUBLIC
|
||||
"The group and any public projects can be viewed without any authentication."
|
||||
end
|
||||
end
|
||||
|
||||
def snippet_visibility_level_description(level, snippet = nil)
|
||||
case level
|
||||
when Gitlab::VisibilityLevel::PRIVATE
|
||||
@@ -50,6 +63,23 @@ module VisibilityLevelHelper
|
||||
end
|
||||
end
|
||||
|
||||
def visibility_icon_description(form_model)
|
||||
case form_model
|
||||
when Project
|
||||
project_visibility_icon_description(form_model.visibility_level)
|
||||
when Group
|
||||
group_visibility_icon_description(form_model.visibility_level)
|
||||
end
|
||||
end
|
||||
|
||||
def group_visibility_icon_description(level)
|
||||
"#{visibility_level_label(level)} - #{group_visibility_level_description(level)}"
|
||||
end
|
||||
|
||||
def project_visibility_icon_description(level)
|
||||
"#{visibility_level_label(level)} - #{project_visibility_level_description(level)}"
|
||||
end
|
||||
|
||||
def visibility_level_label(level)
|
||||
Project.visibility_levels.key(level)
|
||||
end
|
||||
@@ -67,8 +97,11 @@ module VisibilityLevelHelper
|
||||
current_application_settings.default_snippet_visibility
|
||||
end
|
||||
|
||||
def default_group_visibility
|
||||
current_application_settings.default_group_visibility
|
||||
end
|
||||
|
||||
def skip_level?(form_model, level)
|
||||
form_model.is_a?(Project) &&
|
||||
!form_model.visibility_level_allowed?(level)
|
||||
form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
|
||||
end
|
||||
end
|
||||
|
||||
+28
-18
@@ -85,7 +85,7 @@ class Ability
|
||||
subject.group
|
||||
end
|
||||
|
||||
if group && group.projects.public_only.any?
|
||||
if group && group.public?
|
||||
[:read_group]
|
||||
else
|
||||
[]
|
||||
@@ -114,6 +114,13 @@ class Ability
|
||||
# Push abilities on the users team role
|
||||
rules.push(*project_team_rules(project.team, user))
|
||||
|
||||
if project.owner == user ||
|
||||
(project.group && project.group.has_owner?(user)) ||
|
||||
user.admin?
|
||||
|
||||
rules.push(*project_owner_rules)
|
||||
end
|
||||
|
||||
if project.public? || (project.internal? && !user.external?)
|
||||
rules.push(*public_project_rules)
|
||||
|
||||
@@ -121,14 +128,6 @@ class Ability
|
||||
rules << :read_build if project.public_builds?
|
||||
end
|
||||
|
||||
if project.owner == user || user.admin?
|
||||
rules.push(*project_admin_rules)
|
||||
end
|
||||
|
||||
if project.group && project.group.has_owner?(user)
|
||||
rules.push(*project_admin_rules)
|
||||
end
|
||||
|
||||
if project.archived?
|
||||
rules -= project_archived_rules
|
||||
end
|
||||
@@ -171,7 +170,8 @@ class Ability
|
||||
:read_note,
|
||||
:create_project,
|
||||
:create_issue,
|
||||
:create_note
|
||||
:create_note,
|
||||
:upload_file
|
||||
]
|
||||
end
|
||||
|
||||
@@ -228,14 +228,16 @@ class Ability
|
||||
]
|
||||
end
|
||||
|
||||
def project_admin_rules
|
||||
@project_admin_rules ||= project_master_rules + [
|
||||
def project_owner_rules
|
||||
@project_owner_rules ||= project_master_rules + [
|
||||
:change_namespace,
|
||||
:change_visibility_level,
|
||||
:rename_project,
|
||||
:remove_project,
|
||||
:archive_project,
|
||||
:remove_fork_project
|
||||
:remove_fork_project,
|
||||
:destroy_merge_request,
|
||||
:destroy_issue
|
||||
]
|
||||
end
|
||||
|
||||
@@ -273,11 +275,9 @@ class Ability
|
||||
def group_abilities(user, group)
|
||||
rules = []
|
||||
|
||||
if user.admin? || group.users.include?(user) || ProjectsFinder.new.execute(user, group: group).any?
|
||||
rules << :read_group
|
||||
end
|
||||
rules << :read_group if can_read_group?(user, group)
|
||||
|
||||
# Only group masters and group owners can create new projects in group
|
||||
# Only group masters and group owners can create new projects
|
||||
if group.has_master?(user) || group.has_owner?(user) || user.admin?
|
||||
rules += [
|
||||
:create_projects,
|
||||
@@ -290,13 +290,23 @@ class Ability
|
||||
rules += [
|
||||
:admin_group,
|
||||
:admin_namespace,
|
||||
:admin_group_member
|
||||
:admin_group_member,
|
||||
:change_visibility_level
|
||||
]
|
||||
end
|
||||
|
||||
rules.flatten
|
||||
end
|
||||
|
||||
def can_read_group?(user, group)
|
||||
return true if user.admin?
|
||||
return true if group.public?
|
||||
return true if group.internal? && !user.external?
|
||||
return true if group.users.include?(user)
|
||||
|
||||
GroupProjectsFinder.new(group).execute(user).any?
|
||||
end
|
||||
|
||||
def namespace_abilities(user, namespace)
|
||||
rules = []
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# max_attachment_size :integer default(10), not null
|
||||
# default_project_visibility :integer
|
||||
# default_snippet_visibility :integer
|
||||
# default_group_visibility :integer
|
||||
# restricted_signup_domains :text
|
||||
# user_oauth_applications :boolean default(TRUE)
|
||||
# after_sign_out_path :string(255)
|
||||
|
||||
@@ -230,7 +230,7 @@ class Commit
|
||||
end
|
||||
|
||||
def revert_message
|
||||
%Q{Revert "#{title}"\n\n#{revert_description}}
|
||||
%Q{Revert "#{title.strip}"\n\n#{revert_description}}
|
||||
end
|
||||
|
||||
def reverts_commit?(commit)
|
||||
|
||||
@@ -7,7 +7,10 @@ module InternalId
|
||||
end
|
||||
|
||||
def set_iid
|
||||
max_iid = project.send(self.class.name.tableize).maximum(:iid)
|
||||
records = project.send(self.class.name.tableize)
|
||||
records = records.with_deleted if self.paranoid?
|
||||
max_iid = records.maximum(:iid)
|
||||
|
||||
self.iid = max_iid.to_i + 1
|
||||
end
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ module Issuable
|
||||
|
||||
scope :join_project, -> { joins(:project) }
|
||||
scope :references_project, -> { references(:project) }
|
||||
scope :non_archived, -> { join_project.merge(Project.non_archived) }
|
||||
scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
|
||||
|
||||
delegate :name,
|
||||
:email,
|
||||
@@ -58,6 +58,8 @@ module Issuable
|
||||
attr_mentionable :description, cache: true
|
||||
participant :author, :assignee, :notes_with_associations
|
||||
strip_attributes :title
|
||||
|
||||
acts_as_paranoid
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
+28
-9
@@ -2,15 +2,16 @@
|
||||
#
|
||||
# Table name: namespaces
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string(255) not null
|
||||
# path :string(255) not null
|
||||
# owner_id :integer
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# type :string(255)
|
||||
# description :string(255) default(""), not null
|
||||
# avatar :string(255)
|
||||
# id :integer not null, primary key
|
||||
# name :string(255) not null
|
||||
# path :string(255) not null
|
||||
# owner_id :integer
|
||||
# visibility_level :integer default(20), not null
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# type :string(255)
|
||||
# description :string(255) default(""), not null
|
||||
# avatar :string(255)
|
||||
#
|
||||
|
||||
require 'carrierwave/orm/activerecord'
|
||||
@@ -18,6 +19,7 @@ require 'file_size_validator'
|
||||
|
||||
class Group < Namespace
|
||||
include Gitlab::ConfigHelper
|
||||
include Gitlab::VisibilityLevel
|
||||
include Referable
|
||||
|
||||
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
|
||||
@@ -27,6 +29,8 @@ class Group < Namespace
|
||||
has_many :shared_projects, through: :project_group_links, source: :project
|
||||
|
||||
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
|
||||
validate :visibility_level_allowed_by_projects
|
||||
|
||||
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
|
||||
|
||||
mount_uploader :avatar, AvatarUploader
|
||||
@@ -74,6 +78,21 @@ class Group < Namespace
|
||||
name
|
||||
end
|
||||
|
||||
def visibility_level_field
|
||||
visibility_level
|
||||
end
|
||||
|
||||
def visibility_level_allowed_by_projects
|
||||
allowed_by_projects = self.projects.where('visibility_level > ?', self.visibility_level).none?
|
||||
|
||||
unless allowed_by_projects
|
||||
level_name = Gitlab::VisibilityLevel.level_name(visibility_level).downcase
|
||||
self.errors.add(:visibility_level, "#{level_name} is not allowed since there are projects with higher visibility.")
|
||||
end
|
||||
|
||||
allowed_by_projects
|
||||
end
|
||||
|
||||
def avatar_url(size = nil)
|
||||
if avatar.present?
|
||||
[gitlab_config.url, avatar.url].join
|
||||
|
||||
+2
-4
@@ -36,9 +36,6 @@ class Issue < ActiveRecord::Base
|
||||
|
||||
validates :project, presence: true
|
||||
|
||||
scope :of_group,
|
||||
->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
|
||||
|
||||
scope :cared, ->(user) { where(assignee_id: user) }
|
||||
scope :open_for, ->(user) { opened.assigned_to(user) }
|
||||
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
|
||||
@@ -149,7 +146,8 @@ class Issue < ActiveRecord::Base
|
||||
return false unless user.can?(:admin_issue, to_project)
|
||||
end
|
||||
|
||||
!moved? && user.can?(:admin_issue, self.project)
|
||||
!moved? && persisted? &&
|
||||
user.can?(:admin_issue, self.project)
|
||||
end
|
||||
|
||||
def to_branch_name
|
||||
|
||||
+4
-4
@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def open_issues_count
|
||||
issues.opened.count
|
||||
def open_issues_count(user = nil)
|
||||
issues.visible_to_user(user).opened.count
|
||||
end
|
||||
|
||||
def closed_issues_count
|
||||
issues.closed.count
|
||||
def closed_issues_count(user = nil)
|
||||
issues.visible_to_user(user).closed.count
|
||||
end
|
||||
|
||||
def open_merge_requests_count
|
||||
|
||||
@@ -131,7 +131,6 @@ class MergeRequest < ActiveRecord::Base
|
||||
validate :validate_branches
|
||||
validate :validate_fork
|
||||
|
||||
scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
|
||||
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
|
||||
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
|
||||
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
|
||||
|
||||
@@ -83,7 +83,7 @@ class Milestone < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.upcoming
|
||||
self.where('due_date > ?', Time.now).order(due_date: :asc).first
|
||||
self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
|
||||
end
|
||||
|
||||
def to_reference(from_project = nil)
|
||||
|
||||
+40
-12
@@ -73,7 +73,7 @@ class Project < ActiveRecord::Base
|
||||
update_column(:last_activity_at, self.created_at)
|
||||
end
|
||||
|
||||
# update visibility_levet of forks
|
||||
# update visibility_level of forks
|
||||
after_update :update_forks_visibility_level
|
||||
def update_forks_visibility_level
|
||||
return unless visibility_level < visibility_level_was
|
||||
@@ -197,6 +197,8 @@ class Project < ActiveRecord::Base
|
||||
validate :avatar_type,
|
||||
if: ->(project) { project.avatar.present? && project.avatar_changed? }
|
||||
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
|
||||
validate :visibility_level_allowed_by_group
|
||||
validate :visibility_level_allowed_as_fork
|
||||
|
||||
add_authentication_token_field :runners_token
|
||||
before_save :ensure_runners_token
|
||||
@@ -215,8 +217,6 @@ class Project < ActiveRecord::Base
|
||||
scope :in_group_namespace, -> { joins(:group) }
|
||||
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
|
||||
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
|
||||
scope :public_only, -> { where(visibility_level: Project::PUBLIC) }
|
||||
scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) }
|
||||
scope :non_archived, -> { where(archived: false) }
|
||||
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
|
||||
|
||||
@@ -246,10 +246,6 @@ class Project < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class << self
|
||||
def public_and_internal_levels
|
||||
[Project::PUBLIC, Project::INTERNAL]
|
||||
end
|
||||
|
||||
def abandoned
|
||||
where('projects.last_activity_at < ?', 6.months.ago)
|
||||
end
|
||||
@@ -308,7 +304,7 @@ class Project < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def find_with_namespace(id)
|
||||
namespace_path, project_path = id.split('/')
|
||||
namespace_path, project_path = id.split('/', 2)
|
||||
|
||||
return nil if !namespace_path || !project_path
|
||||
|
||||
@@ -435,6 +431,7 @@ class Project < ActiveRecord::Base
|
||||
def safe_import_url
|
||||
result = URI.parse(self.import_url)
|
||||
result.password = '*****' unless result.password.nil?
|
||||
result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user
|
||||
result.to_s
|
||||
rescue
|
||||
self.import_url
|
||||
@@ -442,10 +439,25 @@ class Project < ActiveRecord::Base
|
||||
|
||||
def check_limit
|
||||
unless creator.can_create_project? or namespace.kind == 'group'
|
||||
errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
|
||||
self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
|
||||
end
|
||||
rescue
|
||||
errors[:base] << ("Can't check your ability to create project")
|
||||
self.errors.add(:base, "Can't check your ability to create project")
|
||||
end
|
||||
|
||||
def visibility_level_allowed_by_group
|
||||
return if visibility_level_allowed_by_group?
|
||||
|
||||
level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
|
||||
group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase
|
||||
self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.")
|
||||
end
|
||||
|
||||
def visibility_level_allowed_as_fork
|
||||
return if visibility_level_allowed_as_fork?
|
||||
|
||||
level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
|
||||
self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
|
||||
end
|
||||
|
||||
def to_param
|
||||
@@ -961,9 +973,25 @@ class Project < ActiveRecord::Base
|
||||
issues.opened.count
|
||||
end
|
||||
|
||||
def visibility_level_allowed?(level)
|
||||
def visibility_level_allowed_as_fork?(level = self.visibility_level)
|
||||
return true unless forked?
|
||||
Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
|
||||
|
||||
# self.forked_from_project will be nil before the project is saved, so
|
||||
# we need to go through the relation
|
||||
original_project = forked_project_link.forked_from_project
|
||||
return true unless original_project
|
||||
|
||||
level <= original_project.visibility_level
|
||||
end
|
||||
|
||||
def visibility_level_allowed_by_group?(level = self.visibility_level)
|
||||
return true unless group
|
||||
|
||||
level <= group.visibility_level
|
||||
end
|
||||
|
||||
def visibility_level_allowed?(level = self.visibility_level)
|
||||
visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
|
||||
end
|
||||
|
||||
def runners_token
|
||||
|
||||
@@ -43,12 +43,9 @@ class BaseService
|
||||
def deny_visibility_level(model, denied_visibility_level = nil)
|
||||
denied_visibility_level ||= model.visibility_level
|
||||
|
||||
level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level)
|
||||
level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level).downcase
|
||||
|
||||
model.errors.add(
|
||||
:visibility_level,
|
||||
"#{level_name} visibility has been restricted by your GitLab administrator"
|
||||
)
|
||||
model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Ci
|
||||
class CreateBuildsService
|
||||
def execute(commit, stage, ref, tag, user, trigger_request, status)
|
||||
builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
|
||||
builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request)
|
||||
|
||||
# check when to create next build
|
||||
builds_attrs = builds_attrs.select do |build_attrs|
|
||||
|
||||
@@ -6,8 +6,7 @@ class CreateSnippetService < BaseService
|
||||
snippet = project.snippets.build(params)
|
||||
end
|
||||
|
||||
unless Gitlab::VisibilityLevel.allowed_for?(current_user,
|
||||
params[:visibility_level])
|
||||
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
|
||||
deny_visibility_level(snippet)
|
||||
return snippet
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module Groups
|
||||
class BaseService < ::BaseService
|
||||
attr_accessor :group, :current_user, :params
|
||||
|
||||
def initialize(group, user, params = {})
|
||||
@group, @current_user, @params = group, user, params.dup
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
module Groups
|
||||
class CreateService < Groups::BaseService
|
||||
def initialize(user, params = {})
|
||||
@current_user, @params = user, params.dup
|
||||
end
|
||||
|
||||
def execute
|
||||
@group = Group.new(params)
|
||||
|
||||
unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
|
||||
deny_visibility_level(@group)
|
||||
return @group
|
||||
end
|
||||
|
||||
@group.name ||= @group.path.dup
|
||||
@group.save
|
||||
@group.add_owner(current_user)
|
||||
@group
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user