Commit 6638a403 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'origin' into check-ff-more-precisely

* origin: (429 commits)
  UpdateMirror service return an error status when no mirror
  Remove explicit Gitlab::Metrics.action assignments, are already automatic.
  Fix Error 500 when attempting to sort merge requests by weight
  Fix JenkinsService test button
  File Lock
  Show flash notice when Git Hooks are updated successfully
  Set GL_ID in MergeRequests::RebaseService
  Update CHANGELOG
  ES: Project members with guest role can't access confidential issues
  Projects without import_url cannot be mirrors
  update schema
  Make project_id nullable
  update schema
  [Elastic] Move ES settings to application settings
  Update CHANGELOG-EE for 8.8.5 release
  Update CHANGELOG-EE for 8.7.7 release
  Update CHANGELOG-EE for 8.6.9 release
  Update CHANGELOG-EE for 8.5.13 release
  Update CHANGELOG-EE for 8.4.11 release
  Update CHANGELOG-EE for 8.3.10 release
  ...
parents 0bbf2665 a7025851
We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly.
We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account.
Thank you for taking the time to contribute back to GitLab!
Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
......@@ -8,7 +8,8 @@ services:
cache:
key: "ruby21"
paths:
- vendor
- vendor/apt
- vendor/ruby
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
......@@ -93,9 +94,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun"
# retry failed tests 3 times
- retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
- knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
paths:
- knapsack/
......
......@@ -13,7 +13,8 @@ AllCops:
# Exclude some GitLab files
Exclude:
- 'vendor/**/*'
- 'db/**/*'
- 'db/*'
- 'db/fixtures/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'lib/backup/**/*'
......@@ -348,7 +349,7 @@ Style/MultilineArrayBraceLayout:
# Avoid multi-line chains of blocks.
Style/MultilineBlockChain:
Enabled: false
Enabled: true
# Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout:
......
This diff is collapsed.
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
- Fix JenkinsService test button
- Fix nil user handling in UpdateMirrorService
- Allow LDAP to mark users as external based on their group membership. !432
- Forbid MR authors from approving their own MRs
- Instrument instance methods of Gitlab::InsecureKeyFingerprint class
- Add API endpoint for Merge Request Approvals !449
- Distribute RepositoryUpdateMirror jobs in time and add exclusive lease on them by project_id
- [Elastic] Move ES settings to application settings
- Disable mirror flag for projects without import_url
- UpdateMirror service return an error status when no mirror
- Show flash notice when Git Hooks are updated successfully
- Remove explicit Gitlab::Metrics.action assignments, are already automatic.
- [Elastic] Project members with guest role can't access confidential issues
- Ability to lock file or folder in the repository
v 8.8.5
- Make sure OAuth routes that we generate for Geo matches with the ones in Rails routes !444
v 8.8.4
- Remove license overusage message
......@@ -43,6 +56,9 @@ v 8.8.0
- API requests to /internal/authorized_keys are now tagged properly
- Geo: Single Sign Out support !380
v 8.7.7
- No EE-specific changes
v 8.7.6
- Bump GitLab Pages to 0.2.4 to fix Content-Type for predefined 404
......@@ -71,6 +87,9 @@ v 8.7.0
- Add ability to sync to remote mirrors. !249
- GitLab Geo: Many replication improvements and fixes !354
v 8.6.9
- No EE-specific changes
v 8.6.8
- No EE-specific changes
......@@ -130,6 +149,9 @@ v 8.6.0
- Allow SSL verification to be configurable when importing GitHub projects
- Disable git-hooks for git annex commits
v 8.5.13
- No EE-specific changes
v 8.5.12
- No EE-specific changes
......@@ -185,6 +207,9 @@ v 8.5.0
- Fix of Elastic indexer. Stabilze indexer when serialized data is corrupted
- [Elastic] Don't index unnecessary data into elastic
v 8.4.11
- No EE-specific changes
v 8.4.10
- No EE-specific changes
......@@ -235,6 +260,9 @@ v 8.4.0
- Add option to trigger builds when branches or tags are updated from a mirrored upstream repository
- Ability to use Elasticsearch as a search engine
v 8.3.10
- No EE-specific changes
v 8.3.9
- No EE-specific changes
......@@ -272,6 +300,9 @@ v 8.3.0
- Automatically import Kerberos identities from Active Directory when Kerberos is enabled (Alex Lossent)
- Canonicalization of Kerberos identities to always include realm (Alex Lossent)
v 8.2.6
- No EE-specific changes
v 8.2.5
- No EE-specific changes
......
......@@ -220,6 +220,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
# Parse duration
gem 'chronic_duration', '~> 0.10.6'
gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
......@@ -234,7 +237,6 @@ gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
......@@ -256,7 +258,7 @@ end
group :development do
gem "foreman"
gem 'brakeman', '~> 3.2.0', require: false
gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'quiet_assets', '~> 1.0.2'
......
......@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.4)
allocations (1.0.5)
arel (6.0.3)
asana (0.4.0)
faraday (~> 0.9)
......@@ -97,16 +97,7 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.2.1)
erubis (~> 2.6)
haml (>= 3.0, < 5.0)
highline (>= 1.6.20, < 2.0)
ruby2ruby (~> 2.3.0)
ruby_parser (~> 3.8.1)
safe_yaml (>= 1.0)
sass (~> 3.0)
slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
brakeman (3.3.2)
browser (2.0.3)
builder (3.2.2)
bullet (5.0.0)
......@@ -133,6 +124,8 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.3)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
......@@ -306,7 +299,7 @@ GEM
gitlab-license (0.0.4)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.1.0)
gitlab_git (10.1.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -362,7 +355,6 @@ GEM
hashie (3.4.3)
health_check (1.5.1)
rails (>= 2.3.0)
highline (1.7.8)
hipchat (1.5.2)
httparty
mimemagic
......@@ -432,7 +424,7 @@ GEM
mime-types (>= 1.16, < 4)
mail_room (0.7.0)
method_source (0.8.2)
mime-types (2.99.1)
mime-types (2.99.2)
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
......@@ -448,6 +440,7 @@ GEM
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
numerizer (0.1.1)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
......@@ -587,7 +580,6 @@ GEM
rainbow (2.1.0)
raindrops (0.15.0)
rake (10.5.0)
raphael-rails (2.1.2)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
......@@ -666,10 +658,7 @@ GEM
ruby-saml (1.1.2)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
ruby2ruby (2.3.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
ruby_parser (3.8.1)
ruby_parser (3.8.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -679,7 +668,7 @@ GEM
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.21)
sass (3.4.22)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
......@@ -728,9 +717,6 @@ GEM
tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.2.1)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
spinach (0.8.10)
colorize
......@@ -771,10 +757,8 @@ GEM
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
temple (0.7.6)
term-ansicolor (1.3.2)
tins (~> 1.0)
terminal-table (1.5.2)
test_after_commit (0.4.2)
activerecord (>= 3.2)
thin (1.6.4)
......@@ -783,7 +767,7 @@ GEM
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.2)
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
......@@ -875,7 +859,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0)
brakeman (~> 3.3.0)
browser (~> 2.0.3)
bullet
bundler-audit
......@@ -884,6 +868,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
......@@ -985,7 +970,6 @@ DEPENDENCIES
rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
recaptcha (~> 3.0)
......
......@@ -42,10 +42,10 @@ class @LabelManager
$from = @prioritizedLabels
if $from.find('li').length is 1
$from.find('.empty-message').show()
$from.find('.empty-message').removeClass('hidden')
if not $target.find('li').length
$target.find('.empty-message').hide()
$target.find('.empty-message').addClass('hidden')
$label.detach().appendTo($target)
......@@ -54,6 +54,9 @@ class @LabelManager
if action is 'remove'
xhr = $.ajax url: url, type: 'DELETE'
# Restore empty message
$from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
else
xhr = @savePrioritySort($label, action)
......
......@@ -33,10 +33,6 @@
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2
#= require raphael
#= require g.raphael
#= require g.bar
#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
#= require underscore
......@@ -128,9 +124,10 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize()
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
$(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
$(".js-select-on-focus").on "focusin", ->
......@@ -165,19 +162,6 @@ $ ->
$el.data('placement') || 'bottom'
)
$('.header-logo .home').tooltip(
placement: (_, el) ->
$el = $(el)
if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom'
container: 'body'
)
$('.page-with-sidebar').tooltip(
selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user'
placement: 'right'
container: 'body'
)
# Form submitter
$('.trigger-submit').on 'change', ->
$(@).parents('form').submit()
......@@ -210,6 +194,7 @@ $ ->
$('.navbar-toggle').on 'click', ->
$('.header-content .title').toggle()
$('.header-content .header-logo').toggle()
$('.header-content .navbar-collapse').toggle()
$('.navbar-toggle').toggleClass('active')
$('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
......@@ -245,7 +230,6 @@ $ ->
$this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle')
$navIconToggle = $('.toggle-nav-collapse')
$(document)
.off 'breakpoint:change'
......@@ -255,10 +239,6 @@ $ ->
if $gutterIcon.hasClass('fa-angle-double-right')
$sidebarGutterToggle.trigger('click')
$navIcon = $navIconToggle.find('.fa')
if $navIcon.hasClass('fa-angle-left')
$navIconToggle.trigger('click')
fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint
bootstrapBreakpoint = bp.getBreakpointSize()
......@@ -271,10 +251,38 @@ $ ->
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
$(window)
.off "resize"
.on "resize", (e) ->
.off "resize.app"
.on "resize.app", (e) ->
fitSidebarForSize()
gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize()
new Aside()
# Sidenav pinning
if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
$.cookie('pin_nav', 'false')
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
.removeClass('page-sidebar-pinned')
$('.navbar-fixed-top').removeClass('header-pinned-nav')
$(document)
.off 'click', '.js-nav-pin'
.on 'click', '.js-nav-pin', (e) ->
e.preventDefault()
$(this).toggleClass 'is-active'
if $.cookie('pin_nav') is 'true'
$.cookie 'pin_nav', 'false'
$('.page-with-sidebar')
.removeClass('page-sidebar-pinned')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.removeClass('header-pinned-nav')
.toggleClass('header-collapsed header-expanded')
else
$.cookie 'pin_nav', 'true'
$('.page-with-sidebar').addClass('page-sidebar-pinned')
$('.navbar-fixed-top').addClass('header-pinned-nav')
......@@ -40,7 +40,7 @@ class @AwardsHandler
$menu = $ '.emoji-menu'
if $addBtn.hasClass 'js-note-emoji'
$addBtn.parents('.note').find('.js-awards-block').addClass 'current'
$addBtn.closest('.note').find('.js-awards-block').addClass 'current'
else
$addBtn.closest('.js-awards-block').addClass 'current'
......
class CiBuild
class @CiBuild
@interval: null
@state: null
constructor: (build_url, build_status, build_state) ->
constructor: (@build_url, @build_status, @state) ->
clearInterval(CiBuild.interval)
@state = build_state
# Init breakpoint checker
@bp = Breakpoints.get()
@hideSidebar()
$('.js-build-sidebar').niceScroll()
$(document)
.off 'click', '.js-sidebar-build-toggle'
.on 'click', '.js-sidebar-build-toggle', @toggleSidebar
@initScrollButtonAffix()
$(window)
.off 'resize.build'
.on 'resize.build', @hideSidebar
if build_status == "running" || build_status == "pending"
@updateArtifactRemoveDate()
if $('#build-trace').length
@getInitialBuildTrace()
@initScrollButtonAffix()
if @build_status is "running" or @build_status is "pending"
#
# Bind autoscroll button to follow build output
#
$("#autoscroll-button").bind "click", ->
$('#autoscroll-button').on 'click', ->
state = $(this).data("state")
if "enabled" is state
$(this).data "state", "disabled"
......@@ -27,26 +41,37 @@ class CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
if window.location.href.split("#").first() is build_url
last_state = @state
$.ajax
url: build_url + "/trace.json?state=" + encodeURIComponent(@state)
dataType: "json"
success: (log) =>
return unless last_state is @state
if log.state and log.status is "running"
@state = log.state
if log.append
$('.fa-refresh').before log.html
else
$('#build-trace code').html log.html
$('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
@checkAutoscroll()
else if log.status isnt build_status
Turbolinks.visit build_url
if window.location.href.split("#").first() is @build_url
@getBuildTrace()
, 4000
getInitialBuildTrace: ->
$.ajax
url: @build_url
dataType: 'json'
success: (build_data) ->
$('.js-build-output').html build_data.trace_html
if build_data.status is 'success' or build_data.status is 'failed'
$('.js-build-refresh').remove()
getBuildTrace: ->
$.ajax
url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}"
dataType: "json"
success: (log) =>
if log.state
@state = log.state
if log.status is "running"
if log.append
$('.js-build-output').append log.html
else
$('.js-build-output').html log.html
@checkAutoscroll()
else if log.status isnt @build_status
Turbolinks.visit @build_url
checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
......@@ -61,4 +86,29 @@ class CiBuild
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
@CiBuild = CiBuild
shouldHideSidebar: ->
bootstrapBreakpoint = @bp.getBreakpointSize()
bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm'
toggleSidebar: =>
if @shouldHideSidebar()
$('.js-build-sidebar')
.toggleClass 'right-sidebar-expanded right-sidebar-collapsed'
hideSidebar: =>
if @shouldHideSidebar()
$('.js-build-sidebar')
.removeClass 'right-sidebar-expanded'
.addClass 'right-sidebar-collapsed'
else
$('.js-build-sidebar')
.removeClass 'right-sidebar-collapsed'
.addClass 'right-sidebar-expanded'
updateArtifactRemoveDate: ->
$date = $('.js-artifacts-remove')
if $date.length
date = $date.text()
$date.text $.timefor(new Date(date), ' ')
......@@ -29,6 +29,7 @@ class Dispatcher
new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DueDateSelect()
new GLForm($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
......@@ -53,9 +54,13 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
new MergedButtons()
when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
new MergedButtons()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
new MergedButtons()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
Issuable.init()
......@@ -68,9 +73,7 @@ class Dispatcher
new Diff()
new ZenMode()
shortcut_handler = new ShortcutsNavigation()
when 'projects:commits:show'
shortcut_handler = new ShortcutsNavigation()
when 'projects:activity'
when 'projects:commits:show', 'projects:activity'
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
......@@ -96,6 +99,7 @@ class Dispatcher
when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
new ShortcutsBlob true
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
when 'projects:labels:index'
......@@ -133,15 +137,11 @@ class Dispatcher
new Project()
new ProjectAvatar()
switch path[1]
when 'compare'
shortcut_handler = new ShortcutsNavigation()
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
when 'new'
when 'new', 'show'
new ProjectNew()
when 'show'
new ProjectShow()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
......@@ -150,9 +150,9 @@ class Dispatcher
when 'snippets'
shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show'
when 'labels', 'graphs'
shortcut_handler = new ShortcutsNavigation()
when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
'milestones', 'project_members', 'deploy_keys', 'builds', \
'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one
......
class @DueDateSelect
constructor: ->
# Milestone edit/new form
$datePicker = $('.datepicker')
if $datePicker.length
$dueDate = $('#milestone_due_date')
$datePicker.datepicker
dateFormat: 'yy-mm-dd'
onSelect: (dateText, inst) ->
$dueDate.val(dateText)
.datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()))
$('.js-clear-due-date').on 'click', (e) ->
e.preventDefault()
$.datepicker._clearDate($datePicker)
# Issuable sidebar
$loading = $('.js-issuable-update .due_date')
.find('.block-loading')
.hide()
......@@ -32,7 +48,7 @@ class @DueDateSelect
date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date
else
mediumDate = 'None'
mediumDate = 'No due date'
data = {}
data[abilityName] = {}
......@@ -50,7 +66,8 @@ class @DueDateSelect
$selectbox.hide()
$value.css('display', '')
$valueContent.html(mediumDate)
cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
$valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
$sidebarValue.html(mediumDate)
if value isnt ''
......
......@@ -56,13 +56,6 @@ issuable_created = false
Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label')
toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0
$filteredLabels.removeClass('hidden')
else
$filteredLabels.addClass('hidden')
filterResults: (form) =>
formData = form.serialize()
......@@ -71,58 +64,16 @@ issuable_created = false
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: 'GET'
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issuable.reload()
Issuable.updateStateFilters()
$filteredLabels = $('.filtered-labels')
if typeof Issuable.labelRow is 'function'
$filteredLabels.html(Issuable.labelRow(data))
Issuable.toggleLabelFilters()
dataType: "json"
reload: ->
if Issuable.created
Issuable.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
Turbolinks.visit(issuesUrl);
initChecks: ->
$('.check_all_issues').on 'click', ->
$('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked)
Issuable.checkChanged()
)
$('.selected_issue').on 'change', Issuable.checkChanged
updateStateFilters: ->
stateFilters = $('.issues-state-filters, .dropdown-menu-sort')
newParams = {}
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
labelNameValues = gl.utils.getParameterValues('label_name[]')
if labelNameValues
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
else
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
$(this).attr 'href', newUrl
$('.selected_issue').off('change').on('change', Issuable.checkChanged)
checkChanged: ->
checked_issues = $('.selected_issue:checked')
......
......@@ -102,6 +102,10 @@ class @IssuableForm
return {
results: data
}
data: (query) ->
{
search: query
}
formatResult: (project) ->
project.name_with_namespace
formatSelection: (project) ->
......
......@@ -9,6 +9,9 @@ class @IssuableBulkActions
@bindEvents()
# Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
getElement: (selector) ->
@container.find selector
......@@ -97,13 +100,22 @@ class @IssuableBulkActions
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
labelIds.push $(label).val() if label
labelIds.push parseInt($(label).val()) if label
labelIds
###*
* Just an alias of @getUnmarkedIndeterminedLabels
* @return {Array} Array of labels
* Returns Label IDs that will be removed from issue selection
* @return {Array} Array of labels IDs
###
getLabelsToRemove: ->
@getUnmarkedIndeterminedLabels()
result = []
indeterminatedLabels = @getUnmarkedIndeterminedLabels()
labelsToApply = @getLabelsToApply()
indeterminatedLabels.map (id) ->
# We need to exclude label IDs that will be applied
# By not doing this will cause issues from selection to not add labels at all
result.push(id) if labelsToApply.indexOf(id) is -1
result
......@@ -39,7 +39,7 @@ class @LabelsSelect
</a>
<% }); %>'
)
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
labelNoneHTMLTemplate = '<span class="no-value">None</span>'
if newLabelField.length
......@@ -95,8 +95,11 @@ class @LabelsSelect
$newLabelCreateButton.enable()
if label.message?
errors = _.map label.message, (value, key) ->
"#{key} #{value[0]}"
$newLabelError
.text label.message
.html errors.join("<br/>")
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
......@@ -142,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data)
labelCount = data.labels.length
else
template = labelNoneHTMLTemplate()
template = labelNoneHTMLTemplate
$value
.removeAttr('style')
.html(template)
......@@ -254,7 +257,7 @@ class @LabelsSelect
search:
fields: ['title']
selectable: true
filterable: true
toggleLabel: (selected, el) ->
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
......
class @LayoutNav
$ ->
$('.fade-left').addClass('end-scroll')
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this)
$el = $(event.target)
currentPosition = $this.scrollLeft()
size = bp.getBreakpointSize()
controlBtnWidth = $('.controls').width()
maxPosition = $this.get(0).scrollWidth - $this.parent().width()
maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length
$el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
hideEndFade = ($scrollingTabs) ->
$scrollingTabs.each ->
$this = $(@)
$this
.find('.fade-right')
.toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
$ ->
$('.fade-left').addClass('end-scroll')
hideEndFade($('.scrolling-tabs'))
$(window)
.off 'resize.nav'
.on 'resize.nav', ->
hideEndFade($('.scrolling-tabs'))
$('.scrolling-tabs').on 'scroll', (event) ->
$this = $(this)
currentPosition = $this.scrollLeft()
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
$this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
((w) ->
w.gl or= {}
w.gl.utils or= {}
w.gl.utils.isInGroupsPage = ->
return $('body').data('page').split(':')[0] is 'groups'
w.gl.utils.isInProjectPage = ->
return $('body').data('page').split(':')[0] is 'projects'
w.gl.utils.getProjectSlug = ->
return if @isInProjectPage() then $('body').data 'project' else null
w.gl.utils.getGroupSlug = ->
return if @isInGroupsPage() then $('body').data 'group' else null
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
$tooltipEl
.tooltip 'destroy'
.attr 'title', newTitle
.tooltip 'fixTitle'
gl.utils.preventDisabledButtons = ->
$('.btn').click (e) ->
if $(this).hasClass 'disabled'
e.preventDefault()
e.stopImmediatePropagation()
return false
gl.utils.capitalize = (str) ->
return str[0].toUpperCase() + str.slice(1);
jQuery.timefor = (time, suffix, expiredLabel) ->
return '' unless time
......
......@@ -42,9 +42,3 @@ work = ->
$(document).on('page:fetch', start)
$(document).on('page:change', stop)
$ ->
# Make logo clickable as part of a workaround for Safari visited
# link behaviour (See !2690).
$('#logo').on 'click', ->
$('#js-shortcuts-home').get(0).click()
class @MergedButtons
constructor: ->
@$removeBranchWidget = $('.remove_source_branch_widget')
@$removeBranchProgress = $('.remove_source_branch_in_progress')
@$removeBranchFailed = $('.remove_source_branch_widget.failed')
@cleanEventListeners()
@initEventListeners()
cleanEventListeners: ->
$(document).off 'click', '.remove_source_branch'
$(document).off 'ajax:success', '.remove_source_branch'
$(document).off 'ajax:error', '.remove_source_branch'
initEventListeners: ->
$(document).on 'click', '.remove_source_branch', @removeSourceBranch
$(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
$(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
removeSourceBranch: =>
@$removeBranchWidget.hide()
@$removeBranchProgress.show()
removeBranchSuccess: ->
location.reload()
removeBranchError: ->
@$removeBranchWidget.hide()
@$removeBranchProgress.hide()
@$removeBranchFailed.show()
......@@ -24,14 +24,10 @@ class @MilestoneSelect
if issueUpdateURL
milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>">
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
<%= _.escape(title) %>
</span>
</a>'
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>'
)
milestoneLinkNoneTemplate = '<div class="light">None</div>'
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
collapsedSidebarLabelTemplate = _.template(
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
......@@ -116,7 +112,7 @@ class @MilestoneSelect
.val()
data = {}
data[abilityName] = {}
data[abilityName].milestone_id = selected
data[abilityName].milestone_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
......
# This is a manifest file that'll be compiled into including all the files listed below.
# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
# be included in the compiled file accessible from http://example.com/assets/application.js
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file.
#
#= require raphael
#= require g.raphael
#= require g.bar
#= require_tree .
$ ->
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
commit_url: $(".network-graph").attr('data-commit-url'),
ref: $(".network-graph").attr('data-ref'),
commit_id: $(".network-graph").attr('data-commit-id')
})
new ShortcutsNetwork(network_graph.branch_graph)
......@@ -115,12 +115,14 @@ class @Notes
, @pollingInterval
refresh: =>
return if @refreshing is true
@refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
return if @refreshing
@refreshing = true
$.ajax
url: @notes_url
data: "last_fetched_at=" + @last_fetched_at
......
class @PathLocks
@init: (url, path) ->
$('.path-lock').on 'click', ->
$lockBtn = $(this)
currentState = $lockBtn.data('state')
toggleAction = if currentState is 'lock' then 'unlock' else 'lock'
$.post url, {
path: path
}, ->
$lockBtn.text(gl.utils.capitalize(toggleAction))
$lockBtn.data('state', toggleAction)
......@@ -7,24 +7,18 @@ class @ProjectNew
@toggleSettingsOnclick()
toggleSettings: ->
checked = $("#project_merge_requests_enabled").prop("checked")
if checked
$('.merge-request-feature').show()
else
$('.merge-request-feature').hide()
checked = $("#project_issues_enabled").prop("checked")
if checked
$('.issues-feature').show()
else
$('.issues-feature').hide()
checked = $("#project_builds_enabled").prop("checked")
if checked
$('.builds-feature').show()
else
$('.builds-feature').hide()
toggleSettings: =>
@_showOrHide('#project_builds_enabled', '.builds-feature')
@_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature')
@_showOrHide('#project_issues_enabled', '.issues-feature')
toggleSettingsOnclick: ->
$("#project_merge_requests_enabled").on 'click', @toggleSettings
$("#project_issues_enabled").on 'click', @toggleSettings
$("#project_builds_enabled").on 'click', @toggleSettings
$('#project_builds_enabled, #project_merge_requests_enabled, #project_issues_enabled').on 'click', @toggleSettings
_showOrHide: (checkElement, container) ->
$container = $(container)
if $(checkElement).prop('checked')
$container.show()
else
$container.hide()
......@@ -43,6 +43,55 @@ class @Sidebar
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
$(document)
.off 'click', '.js-issuable-todo'
.on 'click', '.js-issuable-todo', @toggleTodo
toggleTodo: (e) =>
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
$.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}"
type: ajaxType
dataType: 'json'
data:
issuable_id: $this.data('issuable')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
).done (data) =>
@todoUpdateDone(data, $this, $btnText, $todoLoading)
beforeTodoSend: ($btn, $todoLoading) ->
$btn.disable()
$todoLoading.removeClass 'hidden'
todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
$todoPendingCount = $('.todos-pending-count')
$todoPendingCount.text data.count
$btn.enable()
$todoLoading.addClass 'hidden'
if data.count is 0
$todoPendingCount.addClass 'hidden'
else
$todoPendingCount.removeClass 'hidden'
if data.todo?
$btn
.attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
......@@ -117,5 +166,3 @@ class @Sidebar
getBlock: (name) ->
@sidebar.find(".block.#{name}")
......@@ -67,8 +67,12 @@ class @SearchAutocomplete
getData: (term, callback) ->
_this = @
# Do not trigger request if input is empty
return if @searchInput.val() is ''
unless term
if contents = @getCategoryContents()
@searchInput.data('glDropdown').filter.options.callback contents
@enableAutocomplete()
return
# Prevent multiple ajax calls
return if @loadingSuggestions
......@@ -122,6 +126,37 @@ class @SearchAutocomplete
).always ->
_this.loadingSuggestions = false
getCategoryContents: ->
userId = gon.current_user_id
{ utils, projectOptions, groupOptions, dashboardOptions } = gl
if utils.isInGroupsPage() and groupOptions
options = groupOptions[utils.getGroupSlug()]
else if utils.isInProjectPage() and projectOptions
options = projectOptions[utils.getProjectSlug()]
else if dashboardOptions
options = dashboardOptions
{ issuesPath, mrPath, name } = options
items = [
{ header: "#{name}" }
{ text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
{ text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
'separator'
{ text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
{ text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
]
items.splice 0, 1 unless name
return items
serializeState: ->
{
# Search Criteria
......@@ -209,6 +244,12 @@ class @SearchAutocomplete
@isFocused = true
@wrap.addClass('search-active')
@getData() if @getValue() is ''
getValue: -> return @searchInput.val()
onClearInputClick: (e) =>
e.preventDefault()
@searchInput.val('').focus()
......@@ -229,6 +270,10 @@ class @SearchAutocomplete
@locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge')
hasLocationBadge: -> return @wrap.is '.has-location-badge'
restoreOriginalState: ->
inputs = Object.keys @originalState
......@@ -257,13 +302,14 @@ class @SearchAutocomplete
@getElement("##{input}").val('')
removeLocationBadge: ->
@locationBadgeEl.hide()
# Reset state
@locationBadgeEl.hide()
@resetSearchState()
@wrap.removeClass('has-location-badge')
@disableAutocomplete()
disableAutocomplete: ->
@searchInput.addClass('disabled')
......
class @Shortcuts
constructor: ->
constructor: (skipResetBindings) ->
@enabledHelp = []
Mousetrap.reset()
Mousetrap.reset() if not skipResetBindings
Mousetrap.bind('?', @onToggleHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
......
#= require shortcuts
class @ShortcutsBlob extends Shortcuts
constructor: (skipResetBindings) ->
super skipResetBindings
Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
@copyToClipboard: ->
clipboardButton = $('.btn-clipboard')
clipboardButton.click() if clipboardButton
......@@ -3,24 +3,35 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
$('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
if $.cookie('pin_nav') is 'true'
$('.navbar-fixed-top').toggleClass('header-pinned-nav')
$('.page-with-sidebar').toggleClass('page-sidebar-pinned')
setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll();
niceScrollBars = $('.nav-sidebar').niceScroll();
niceScrollBars.updateScrollBar();
), 300
$(document)
.off 'click', 'body'
.on 'click', 'body', (e) ->
unless $.cookie('pin_nav') is 'true'
$target = $(e.target)
$nav = $target.closest('.sidebar-wrapper')
pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
$toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
if $nav.length is 0 and pageExpanded and $toggle.length is 0
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
$('.navbar-fixed-top')
.toggleClass('header-collapsed header-expanded')
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
e.preventDefault()
toggleSidebar()
)
$ ->
size = bp.getBreakpointSize()
if size is "xs" or size is "sm"
if $('.page-with-sidebar').hasClass(expanded)
toggleSidebar()
......@@ -9,9 +9,11 @@ class @Star
$this.parent().find('.star-count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
gl.utils.updateTooltipTitle $this, 'Star project'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
else
$starSpan.addClass('starred').text 'Unstar'
gl.utils.updateTooltipTitle $this, 'Unstar project'
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
return
......
......@@ -31,7 +31,7 @@ class @UsersSelect
assignTo = (selected) ->
data = {}
data[abilityName] = {}
data[abilityName].assignee_id = selected
data[abilityName].assignee_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
......@@ -72,7 +72,7 @@ class @UsersSelect
assigneeTemplate = _.template(
'<% if (username) { %>
<a class="author_link " href="/u/<%= username %>">
<a class="author_link bold" href="/u/<%= username %>">
<% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %>
......@@ -82,7 +82,7 @@ class @UsersSelect
</span>
</a>
<% } else { %>
<span class="assign-yourself">
<span class="no-value assign-yourself">
No assignee -
<a href="#" class="js-assign-yourself">
assign yourself
......@@ -95,7 +95,7 @@ class @UsersSelect
data: (term, callback) =>
isAuthorFilter = $('.js-author-search')
@users term, term is '' and isAuthorFilter, (users) =>
@users term, (users) =>
if term.length is 0
showDivider = 0
......@@ -223,7 +223,7 @@ class @UsersSelect
multiple: $(select).hasClass('multiselect')
minimumInputLength: 0
query: (query) =>
@users query.term, @projectId?, (users) =>
@users query.term, (users) =>
data = { results: users }
if query.term.length == 0
......@@ -306,7 +306,7 @@ class @UsersSelect
# Return users list. Filtered by query
# Only active users retrieved
users: (query, fromProject, callback) =>
users: (query, callback) =>
url = @buildUrl(@usersPath)
$.ajax(
......@@ -315,7 +315,7 @@ class @UsersSelect
search: query
per_page: 20
active: true
project_id: @projectId if fromProject
project_id: @projectId
group_id: @groupId
skip_ldap: @skipLdap
current_user: @showCurrentUser
......
......@@ -13,7 +13,7 @@ class @WeightSelect
updateWeight = (selected) ->
data = {}
data[abilityName] = {}
data[abilityName].weight = selected
data[abilityName].weight = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
......@@ -27,7 +27,10 @@ class @WeightSelect
$loading.fadeOut()
$selectbox.hide()
$value.html(data.weight)
if data.weight?
$value.html(data.weight)
else
$value.html('None')
$sidebarCollapsedValue.html(data.weight)
$dropdown.glDropdown(
......@@ -38,7 +41,8 @@ class @WeightSelect
# display:block overrides the hide-collapse rule
$value.css('display', '')
id: (obj, el) ->
$(el).data "id"
if not $(el).data("none")?
$(el).data "id"
clicked: (selected) ->
if $(dropdown).is ".js-filter-submit"
$(dropdown).parents('form').submit()
......
......@@ -8,34 +8,16 @@
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.header-logo {
background: $color-darker;
a {
color: $color-light;
h3 {
color: $color-light;
}
}
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
background: $color;
&:hover {
background-color: $color-dark;
a {
color: $white-light;
h3 {
color: $white-light;
}
}
color: $white-light;
}
}
.collapse-nav a {
color: $white-light;
background: $color;
}
.sidebar-wrapper {
background: $color-darker;
......
......@@ -3,7 +3,7 @@
*
*/
header {
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
&.navbar-empty {
height: $header-height;
......@@ -79,14 +79,9 @@ header {
&.header-collapsed {
padding: 0 16px;
.side-nav-toggle {
display: block;
}
}
.side-nav-toggle {
display: none;
position: absolute;
left: -10px;
margin: 6px 0;
......@@ -108,11 +103,7 @@ header {
.header-content {
position: relative;
height: $header-height;
padding-right: 40px;
@media (max-width: $screen-xs-min) {
padding-left: 40px;
}
padding-left: 30px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
......@@ -122,9 +113,29 @@ header {
margin-top: -5px;
}
.header-logo {
position: absolute;
left: 50%;
margin-left: -18px;
top: 7px;
transition-duration: .3s;
z-index: 999;
&:hover {
cursor: pointer;
}
@media (max-width: $screen-xs-max) {
right: 25px;
left: auto;
}
}
.title {
margin: 0;
font-size: 19px;
max-width: 400px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
......@@ -133,6 +144,10 @@ header {
vertical-align: top;
white-space: nowrap;
@media (max-width: $screen-sm-max) {
max-width: 190px;
}
a {
color: $gl-text-color;
&:hover {
......@@ -160,6 +175,10 @@ header {
.navbar-collapse {
float: right;
border-top: none;
@media (max-width: $screen-xs-max) {
float: none;
}
}
}
......@@ -172,21 +191,12 @@ header {
}
}
.header-collapsed {
margin-left: 0;
.tanuki-shape {
transition: all 0.8s;
.header-content {
padding-left: 30px;
transition-duration: .3s;
}
}
.header-expanded {
margin-left: 0;
.header-content {
padding-left: $sidebar_width;
transition-duration: .3s;
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
}
}
......
......@@ -159,7 +159,7 @@ ul.content-list {
background-color: $gray-light;
border: dotted 1px $gray-dark;
margin: 1px 0;
min-height: 30px;
min-height: 52px;
}
}
}
......
@mixin fade($gradient-direction, $rgba, $gradient-color) {
visibility: visible;
opacity: 1;
z-index: 2;
position: absolute;
bottom: 12px;
width: 43px;
......@@ -68,10 +69,12 @@
}
&.sub-nav {
text-align: center;
background-color: $background-color;
.container-fluid {
background-color: $background-color;
margin-bottom: 0;
}
li {
......@@ -171,7 +174,6 @@
> form {
display: inline-block;
margin-top: -1px;
margin-bottom: 12px;
}
.icon-label {
......@@ -240,6 +242,12 @@
}
}
}
&.adjust {
.nav-text, .nav-controls {
width: auto;
}
}
}
.layout-nav {
......@@ -249,7 +257,8 @@
z-index: 11;
background: $background-color;
border-bottom: 1px solid $border-color;
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
text-align: center;
.container-fluid {
position: relative;
......@@ -278,11 +287,10 @@
}
.dropdown {
margin-left: 7px;
@media (max-width: $screen-xs-min) {
margin-left: 0;
}
position: absolute;
top: 7px;
right: 15px;
z-index: 2;
li.active {
font-weight: bold;
......@@ -345,6 +353,12 @@
.badge {
color: $gl-icon-color;
}
&:hover {
a, i {
color: $black;
}
}
}
}
......@@ -352,7 +366,7 @@
.fade-right {
@media (min-width: $screen-xs-max) {
right: 67px;
right: 68px;
}
@media (max-width: $screen-xs-min) {
right: 0;
......
.page-with-sidebar {
padding-top: $header-height;
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
left: 0;
height: 100%;
transition-duration: .3s;
overflow: hidden;
transition: width $sidebar-transition-duration;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $background-color;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
}
.content-wrapper {
width: 100%;
transition: padding $sidebar-transition-duration;
.container-fluid {
background: #fff;
......@@ -34,35 +39,19 @@
}
}
.sidebar-wrapper {
.header-logo {
height: $header-height;
padding: 8px 26px;
width: $sidebar_width;
position: fixed;
z-index: 999;
overflow: hidden;
transition-duration: .3s;
&:hover {
background-color: #eee;
}
}
.sidebar-user {
padding: 15px 22px;
position: fixed;
bottom: 40px;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
.sidebar-user {
padding: 15px;
position: absolute;
left: 0;
bottom: 0;
width: $sidebar_width;
overflow: hidden;
font-size: 16px;
line-height: 36px;
transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px;
line-height: 34px;
}
@media (min-width: $sidebar-breakpoint) {
bottom: 50px;
}
}
......@@ -78,39 +67,46 @@
.nav-sidebar {
margin-top: 22 + $header-height;
margin-bottom: 116px;
transition-duration: .3s;
list-style: none;
overflow: hidden;
position: absolute;
top: 50px;
bottom: 65px;
width: $sidebar_width;
overflow-y: auto;
overflow-x: hidden;
@media (min-width: $sidebar-breakpoint) {
bottom: 115px;
}
&.navbar-collapse {
padding: 0 !important;
}
li {
width: $sidebar_width;
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
a {
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
padding: 8px;
}
a {
padding: 7px 15px 7px 12px;
font-size: $gl-font-size;
color: $gray;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
outline: none;
white-space: nowrap;
&:hover {
text-decoration: none;
}
&:active, &:focus {
&:hover,
&:active,
&:focus {
text-decoration: none;
}
......@@ -118,49 +114,64 @@
font-size: 16px;
}
.nav-link-text {
margin-top: 3px;
font-size: 13px;
line-height: 18px;
}
&.back-link i {
transition-duration: .3s;
i,
svg {
margin-right: 13px;
}
}
}
}
.sidebar-subnav {
margin-left: 0;
padding-left: 0;
li {
list-style: none;
.count {
float: right;
padding: 0 8px;
@include border-radius(6px);
}
}
.collapse-nav a {
.toggle-nav-collapse {
width: $sidebar_width;
position: fixed;
bottom: 0;
position: absolute;
top: 0;
left: 0;
font-size: 13px;
background: transparent;
height: 40px;
text-align: center;
line-height: 40px;
min-height: 50px;
padding: 5px 0;
font-size: 18px;
line-height: 30px;
}
.nav-header-btn {
padding: 10px 5px;
color: inherit;
transition-duration: .3s;
outline: none;
&:hover {
&:hover,
&:focus {
color: $white-light;
text-decoration: none;
}
}
.sidebar-wrapper {
&.hidden-nav {
width: 0;
.pin-nav-btn {
display: none;
position: absolute;
left: 0;
bottom: 0;
height: 50px;
width: $sidebar_width;
line-height: 30px;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.fa {
transition: transform .15s;
}
&.is-active {
.fa {
transform: rotate(90deg);
}
}
}
......@@ -169,90 +180,34 @@
.sidebar-wrapper {
width: 0;
.header-logo {
width: 0;
padding: 8px 0;
a {
padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
}
}
}
#logo {
display: none;
}
.nav-sidebar {
width: $sidebar_collapsed_width;
li {
width: auto;
a {
span {
display: none;
}
}
}
}
.collapse-nav a {
width: 0;
}
.sidebar-user {
width: 0;
padding-left: 0;
padding-right: 0;
.username {
display: none;
}
}
}
}
.page-sidebar-expanded {
padding-left: $sidebar_width;
@media (max-width: $screen-xs-min) {
padding-left: 0;
}
.sidebar-wrapper {
width: $sidebar_width;
.nav-sidebar {
width: $sidebar_width;
}
.nav-sidebar li a {
width: $sidebar_width;
&.back-link {
i {
opacity: 0;
}
}
}
}
}
.page-sidebar-pinned {
.content-wrapper,
.layout-nav {
@media (max-width: $screen-xs-min) {
padding-right: 0;
@media (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
}
}
}
@media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
padding-right: 90px;
header.header-pinned-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
.side-nav-toggle {
display: none;
}
@media (min-width: $screen-md-min) {
padding-right: $sidebar_width;
.header-content {
padding-left: 0;
}
}
}
......@@ -273,7 +228,9 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
padding-right: $sidebar_collapsed_width;
&:not(.build-sidebar) {
padding-right: $sidebar_collapsed_width;
}
}
@media (min-width: $screen-md-min) {
......
......@@ -2,10 +2,12 @@
* Layout
*/
$sidebar_collapsed_width: 62px;
$sidebar_width: 90px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1440px;
/*
* UI elements
......@@ -260,3 +262,6 @@ $calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
......@@ -101,13 +101,21 @@
line-height: 20px;
outline: 0;
&:hover,
&.active,
&:active {
background-color: $white-dark;
background-color: $row-hover;
border-color: $row-hover-border;
box-shadow: none;
outline: 0;
}
&.btn {
&:focus {
outline: 0;
}
}
&.is-loading {
.award-control-icon-normal,
.emoji-icon {
......
......@@ -53,37 +53,92 @@
left: 70px;
}
}
}
.build-widget {
padding: 10px;
background: $background-color;
margin-bottom: 20px;
border-radius: 4px;
.build-header {
position: relative;
padding-right: 40px;
.title {
margin-top: 0;
color: #666;
line-height: 1.5;
}
.attr-name {
color: #777;
}
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
.alert-disabled {
background: $background-color;
a {
color: $gl-gray;
a {
color: #3084bb !important;
&:hover {
color: $gl-link-color;
text-decoration: none;
}
}
code {
color: $code-color;
}
.avatar {
float: none;
margin-right: 2px;
margin-left: 2px;
}
}
table.builds {
.build-link {
a {
color: $gl-dark-link-color;
}
}
}
.build-trace {
background: $ci-output-bg;
color: $ci-text-color;
white-space: pre;
overflow-x: auto;
font-size: 12px;
.fa-refresh {
font-size: 24px;
}
.bash {
display: block;
}
}
.right-sidebar.build-sidebar {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
&.right-sidebar-collapsed {
display: none;
}
.block {
width: 100%;
}
.build-sidebar-header {
padding-top: 0;
.gutter-toggle {
margin-top: 0;
}
}
}
.build-detail-row {
margin-bottom: 5px;
}
.build-light-text {
color: $gl-placeholder-color;
}
.build-gutter-toggle {
position: absolute;
top: 50%;
right: 0;
margin-top: -17px;
}
......@@ -7,84 +7,111 @@
margin-right: 9px;
}
.lists-separator {
margin: 10px 0;
border-color: #ddd;
.commit-header {
padding: 5px 10px;
background-color: $background-color;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
font-size: 14px;
&:first-child {
border-top-width: 0;
}
}
.commits-row {
ul {
margin: 0;
.commit-row-title {
line-height: 1;
margin-bottom: 7px;
li.commit {
padding: 8px 0;
}
.notes_count {
float: right;
margin-right: 10px;
}
.str-truncated {
max-width: 70%;
}
.commits-row-date {
font-size: 15px;
line-height: 20px;
margin-bottom: 5px;
.commit-row-message {
color: $gl-dark-link-color;
}
.text-expander {
display: inline-block;
background: $gray-light;
color: $gl-placeholder-color;
padding: 0 5px;
cursor: pointer;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
&:hover {
background-color: darken($gray-light, 10%);
text-decoration: none;
}
}
}
li.commit {
list-style: none;
.commit-actions {
@media (min-width: $screen-sm-min) {
float: right;
margin-left: $gl-padding;
margin-top: 2px;
font-size: 0;
}
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
margin-bottom: 2px;
.btn-transparent {
padding-left: 0;
padding-right: 0;
}
.btn-clipboard {
margin-top: -1px;
.btn {
&:not(:first-child) {
margin-left: $gl-padding;
}
}
}
.notes_count {
float: right;
margin-right: 10px;
}
.commit-short-id {
font-family: $monospace_font;
font-weight: 600;
}
.commit_short_id {
min-width: 65px;
color: $gl-dark-link-color;
font-family: $monospace_font;
}
.commit {
padding: 10px 0;
.str-truncated {
max-width: 70%;
}
@media (min-width: $screen-sm-min) {
padding-left: 46px;
}
.commit-row-message {
color: $gl-dark-link-color;
&:not(:last-child) {
border-bottom: 1px solid #eee;
}
&:hover {
text-decoration: underline;
}
}
a,
button {
color: $gl-dark-link-color;
vertical-align: baseline;
}
.text-expander {
background: #eee;
color: #555;
padding: 0 5px;
cursor: pointer;
margin-left: 4px;
&:hover {
background-color: #ddd;
}
}
.avatar {
margin-left: -46px;
}
.item-title {
display: inline-block;
max-width: 70%;
@media (min-width: $screen-sm-min) {
max-width: 70%;
}
}
.commit-row-description {
font-size: 14px;
border-left: 1px solid #eee;
padding: 10px 15px;
margin: 5px 0 10px 5px;
margin: 10px 0;
background: #f9f9f9;
display: none;
......@@ -93,6 +120,7 @@ li.commit {
background: inherit;
padding: 0;
margin: 0;
white-space: pre-wrap;
}
a {
......@@ -102,7 +130,7 @@ li.commit {
.commit-row-info {
color: $gl-gray;
line-height: 24px;
line-height: 1;
a {
color: $gl-gray;
......@@ -111,10 +139,6 @@ li.commit {
.avatar {
margin-right: 8px;
}
.committed_ago {
display: inline-block;
}
}
&.inline-commit {
......
.environments {
.commit-title {
margin: 0;
}
}
......@@ -45,6 +45,23 @@
}
}
.groups-cover-block {
.container-fluid {
position: relative;
}
.access-request-button {
@include btn-gray;
position: absolute;
right: 16px;
bottom: 32px;
padding: 3px 10px;
text-transform: none;
background-color: $background-color;
}
}
.ldap-group-links {
.form-actions {
margin-bottom: $gl-padding;
......
......@@ -29,11 +29,15 @@
}
}
.issuable-sidebar {
.right-sidebar {
a {
color: inherit;
}
.issuable-header-text {
margin-top: 7px;
}
.block {
@include clearfix;
padding: $gl-padding 0;
......@@ -60,10 +64,6 @@
margin-top: 0;
}
.issuable-count {
margin-top: 7px;
}
.gutter-toggle {
margin-left: 20px;
padding-left: 10px;
......@@ -74,6 +74,10 @@
}
}
.block-first {
padding-top: 0;
}
.title {
color: $gl-text-color;
margin-bottom: 10px;
......@@ -141,7 +145,6 @@
.assign-yourself {
margin-top: 10px;
font-weight: normal;
display: block;
}
}
......@@ -154,6 +157,10 @@
font-weight: normal;
}
.no-value {
color: $gl-placeholder-color;
}
.sidebar-collapsed-icon {
display: none;
}
......@@ -244,11 +251,16 @@
padding-bottom: 0;
margin-bottom: 10px;
}
.issuable-header-btn {
display: none;
}
}
.issuable-pager {
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
......@@ -259,7 +271,7 @@
}
}
a:not(.issuable-pager) {
a {
&:hover {
color: $md-link-color;
text-decoration: none;
......@@ -318,7 +330,7 @@
margin-left: 5px;
a {
color: #8c8c8c;
color: $gl-placeholder-color;
}
}
......
......@@ -115,6 +115,13 @@
}
}
.draggable-handler {
display: inline-block;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
}
.prioritized-labels {
margin-bottom: 30px;
......@@ -122,6 +129,13 @@
display: none;
color: $gray-light;
}
li:hover {
.draggable-handler {
display: inline-block;
opacity: 1;
}
}
}
.other-labels {
......
......@@ -313,3 +313,13 @@
}
}
}
.merged-buttons {
.btn {
float: left;
&:not(:last-child) {
margin-right: 10px;
}
}
}
......@@ -139,6 +139,12 @@ ul.notes {
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
}
.note-emoji-button {
......@@ -258,7 +264,11 @@ ul.notes {
position: absolute;
right: 0;
top: 0;
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) {
position: relative;
}
......
......@@ -5,10 +5,12 @@
font-weight: normal;
}
}
.no-ssh-key-message, .project-limit-message {
background-color: #f28d35;
margin-bottom: 0;
}
.new_project,
.edit-project {
fieldset.features {
......@@ -18,13 +20,6 @@
}
}
.project-name-holder {
.help-inline {
vertical-align: top;
padding: 7px;
}
}
.project-home-panel {
background: $white-light;
text-align: left;
......@@ -229,13 +224,20 @@
right: 16px;
bottom: 0;
.btn {
padding: 3px 10px;
background-color: $background-color;
@media (max-width: $screen-lg-min) {
top: 0;
}
@media (max-width: 1304px) {
top: 0;
.access-request-button {
position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-lg-min) {
position: relative;
bottom: 0;
margin-right: 10px;
}
}
}
......@@ -286,10 +288,6 @@
color: #555;
}
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container {
min-width: 200px;
}
......@@ -373,6 +371,7 @@ a.deploy-project-label {
.project-import .btn {
float: left;
margin-bottom: 10px;
margin-right: 10px;
}
......
......@@ -101,7 +101,7 @@
margin: 0;
.commit {
padding: 0;
padding: 0 0 0 55px;
.commit-row-title {
.commit-row-message {
......@@ -129,4 +129,6 @@
.tree-controls {
float: right;
margin-top: 11px;
position: relative;
z-index: 2;
}
......@@ -11,18 +11,15 @@
$magenta: #cd00cd;
$cyan: #00cdcd;
$white: #e5e5e5;
$l-black: #7f7f7f;
$l-red: #f00;
$l-green: #0f0;
$l-yellow: #ff0;
$l-blue: #5c5cff;
$l-magenta: #f0f;
$l-cyan: #0ff;
$l-white: #fff;
$l-black: #373b41;
$l-red: #c66;
$l-green: #b5bd68;
$l-yellow: #f0c674;
$l-blue: #81a2be;
$l-magenta: #b294bb;
$l-cyan: #8abeb7;
$l-white: $ci-text-color;
.term-bold {
font-weight: bold;
}
.term-italic {
font-style: italic;
}
......
......@@ -3,9 +3,17 @@
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin!
before_action :display_geo_information
layout 'admin'
def authenticate_admin!
render_404 unless current_user.is_admin?
end
def display_geo_information
return unless Gitlab::Geo.secondary?
primary_node = view_context.link_to('primary node', Gitlab::Geo.primary_node.url)
flash.now[:notice] = "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the #{primary_node}.".html_safe
end
end
......@@ -42,6 +42,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def application_setting_params
restricted_levels = params[:application_setting][:restricted_visibility_levels]
if restricted_levels.nil?
params[:application_setting][:restricted_visibility_levels] = []
else
......@@ -51,6 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
import_sources = params[:application_setting][:import_sources]
if import_sources.nil?
params[:application_setting][:import_sources] = []
else
......@@ -111,6 +113,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_packet_size,
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:elasticsearch_indexing,
:elasticsearch_search,
:elasticsearch_host,
:elasticsearch_port,
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: []
......
......@@ -10,7 +10,7 @@ class Admin::GitHooksController < Admin::ApplicationController
@git_hook.update_attributes(git_hook_params.merge(is_sample: true))
if @git_hook.valid?
redirect_to admin_git_hooks_path
redirect_to admin_git_hooks_path, notice: 'Git Hooks updated successfully.'
else
render :index
end
......
......@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
include WorkhorseHelper
before_action :authenticate_user_from_token!
before_action :authenticate_user!
......
......@@ -43,6 +43,7 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id])
projects = current_user.authorized_projects
projects = projects.search(params[:search]) if params[:search].present?
projects = projects.select do |project|
current_user.can?(:admin_issue, project)
end
......
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
redirect_to polymorphic_path(membershipable),
notice: 'Your request for access has been queued for review.'
end
def approve_access_request
@member = membershipable.members.request.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
log_audit_event(@member, action: :create)
redirect_to polymorphic_url([membershipable, :members])
end
def leave
@member = membershipable.members.find_by(user_id: current_user)
return render_403 unless @member
source_type = @member.real_source_type.humanize(capitalize: false)
if can?(current_user, action_member_permission(:destroy, @member), @member)
notice =
if @member.request?
"Your access request to the #{source_type} has been withdrawn."
else
"You left the \"#{@member.source.human_name}\" #{source_type}."
end
@member.destroy
log_audit_event(@member, action: :destroy) unless @member.request?
redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
else
if cannot_leave?
alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
alert << " Transfer or delete the #{source_type}."
redirect_to polymorphic_url(membershipable), alert: alert
else
render_403
end
end
end
protected
def membershipable
raise NotImplementedError
end
def cannot_leave?
raise NotImplementedError
end
def log_audit_event(member, options = {})
AuditEventService.new(current_user, membershipable, options).
for_member(member).security_event
end
end
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
# Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave]
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -71,31 +73,16 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
def leave
@group_member = @group.group_members.find_by(user_id: current_user)
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
log_audit_event(@group_member, action: :destroy)
redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
else
return render_403
end
end
end
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
def log_audit_event(member, options = {})
AuditEventService.new(current_user, @group, options).
for_member(member).security_event
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end
......@@ -42,7 +42,7 @@ class JwtController < ApplicationController
end
def authenticate_user(login, password)
user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
user = Gitlab::Auth.find_with_user_password(login, password)
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user
end
......
......@@ -25,9 +25,7 @@ class Oauth::GeoAuthController < ActionController::Base
user = User.find_by(id: remote_user['id'])
if user && sign_in(user, bypass: true)
session[:access_token] = token
return_to = oauth.get_oauth_state_return_to
redirect_to(return_to || root_path)
after_sign_in_with_gitlab(token, oauth.get_oauth_state_return_to)
else
invalid_credentials
end
......@@ -46,6 +44,16 @@ class Oauth::GeoAuthController < ActionController::Base
private
def after_sign_in_with_gitlab(token, return_to)
if Gitlab::Geo.primary_node
primary_node = view_context.link_to('primary node', Gitlab::Geo.primary_node.url)
flash[:notice] = "You are on a secondary (read-only) Geo node. If you want to make any changes, you must visit the #{primary_node}.".html_safe
end
session[:access_token] = token
redirect_to(return_to || root_path)
end
def invalid_credentials
@error = 'Cannot find user to login. Your account may have been deleted.'
render :error, layout: 'errors'
......
class Profiles::NotificationsController < Profiles::ApplicationController
def show
@user = current_user
@group_notifications = current_user.notification_settings.for_groups
@project_notifications = current_user.notification_settings.for_projects
@user = current_user
@group_notifications = current_user.notification_settings.for_groups
@project_notifications = current_user.notification_settings.for_projects
@global_notification_setting = current_user.global_notification_setting
end
def update
if current_user.update_attributes(user_params)
if current_user.update_attributes(user_params) && update_notification_settings
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
......@@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
params.require(:user).permit(:notification_email, :notification_level)
params.require(:user).permit(:notification_email)
end
def global_notification_setting_params
params.require(:global_notification_setting).permit(:level)
end
private
def update_notification_settings
return true unless global_notification_setting_params
current_user.global_notification_setting.update_attributes(global_notification_setting_params)
end
end
class Projects::ArtifactsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :validate_artifacts!
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
unless artifacts_file.exists?
return render_404
end
send_file artifacts_file.path, disposition: 'attachment'
end
def browse
return render_404 unless build.artifacts?
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
......@@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
end
def keep
build.keep_artifacts!
redirect_to namespace_project_build_path(project.namespace, project, build)
end
private
def validate_artifacts!
render_404 unless build.artifacts?
end
def build
@build ||= project.builds.find_by!(id: params[:build_id])
end
......
......@@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController
return if cached_blob?
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob)
head :ok # 'render nothing: true' messes up the Content-Type
send_git_blob @repository, @blob
else
render_404
end
......
......@@ -41,7 +41,7 @@ class Projects::BuildsController < Projects::ApplicationController
def trace
respond_to do |format|
format.json do
render json: @build.trace_with_state(params[:state]).merge!(id: @build.id, status: @build.status)
render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
end
end
end
......@@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController
return render_404
end
build = Ci::Build.retry(@build)
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
......
......@@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController
def retry_builds
ci_builds.latest.failed.each do |build|
if build.retryable?
Ci::Build.retry(build)
Ci::Build.retry(build, current_user)
end
end
......
class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_update_environment!, only: [:destroy]
before_action :environment, only: [:show, :destroy]
def index
@environments = project.environments
end
def show
@deployments = environment.deployments.order(id: :desc).page(params[:page])
end
def new
@environment = project.environments.new
end
def create
@environment = project.environments.create(create_params)
if @environment.persisted?
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
else
render 'new'
end
end
def destroy
if @environment.destroy
flash[:notice] = 'Environment was successfully removed.'
else
flash[:alert] = 'Failed to remove environment.'
end
redirect_to namespace_project_environments_path(project.namespace, project)
end
private
def create_params
params.require(:environment).permit(:name)
end
def environment
@environment ||= project.environments.find(params[:id])
end
end
......@@ -17,7 +17,7 @@ class Projects::GitHooksController < Projects::ApplicationController
@git_hook.update_attributes(git_hook_params)
if @git_hook.valid?
redirect_to namespace_project_git_hooks_path(@project.namespace, @project)
redirect_to namespace_project_git_hooks_path(@project.namespace, @project), notice: 'Git Hooks updated successfully.'
else
render :index
end
......
......@@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController
return if project && project.public? && upload_pack?
authenticate_or_request_with_http_basic do |login, password|
auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip)
auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
if auth_result.type == :ci && upload_pack?
@ci = true
......
......@@ -62,12 +62,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.json { render json: @merge_request, methods: :rebase_in_progress? }
format.patch { render text: @merge_request.to_patch }
format.diff do
headers.store(*Gitlab::Workhorse.send_git_diff(@project.repository,
@merge_request.diff_base_commit.id,
@merge_request.last_commit.id))
headers['Content-Disposition'] = 'inline'
return render_404 unless @merge_request.diff_refs
head :ok
send_git_diff @project.repository, @merge_request.diff_refs
end
end
end
......@@ -220,10 +217,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
if params[:merge_when_build_succeeds].present?
if @merge_request.pipeline && @merge_request.pipeline.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_build_succeeds
elsif @merge_request.pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
else
@status = :failed
end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
......@@ -379,8 +385,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def set_suggested_approvers
if @merge_request.requires_approve?
@suggested_approvers = Gitlab::AuthorityAnalyzer.new(
@merge_request,
current_user
@merge_request
).calculate(@merge_request.approvals_required)
end
end
......
class Projects::PathLocksController < Projects::ApplicationController
include PathLocksHelper
# Authorize
before_action :require_non_empty_project
before_action :authorize_push_code!, only: [:toggle]
before_action :check_license
def index
@path_locks = @project.path_locks.page(params[:page])
end
def toggle
path_lock = @project.path_locks.find_by(path: params[:path])
if path_lock
PathLocks::UnlockService.new(project, current_user).execute(path_lock)
else
PathLocks::LockService.new(project, current_user).execute(params[:path])
end
head :ok
rescue PathLocks::UnlockService::AccessDenied, PathLocks::LockService::AccessDenied
return access_denied!
end
def destroy
path_lock = @project.path_locks.find(params[:id])
begin
PathLocks::UnlockService.new(project, current_user).execute(path_lock)
rescue PathLocks::UnlockService::AccessDenied
return access_denied!
end
respond_to do |format|
format.html do
redirect_to namespace_project_locks_path(@project.namespace, @project)
end
format.js
end
end
private
def check_license
unless license_allows_file_locks?
flash[:alert] = 'You need a different license to enable FileLocks feature'
redirect_to admin_license_path
end
end
end
......@@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def retry
pipeline.retry_failed
pipeline.retry_failed(current_user)
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
......
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
# Authorize
before_action :authorize_admin_project_member!, except: [:leave, :index]
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
@project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
......@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC')
@group = @project.group
if @group
@group_members = @group.group_members
@group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
@group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
......@@ -84,28 +87,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
def leave
@project_member = @project.project_members.find_by(user_id: current_user)
if can?(current_user, :destroy_project_member, @project_member)
@project_member.destroy
log_audit_event(@project_member, action: :destroy)
respond_to do |format|
format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { head :ok }
end
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
else
render_403
end
end
end
def apply_import
source_project = Project.find(params[:source_project_id])
......@@ -126,8 +107,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
params.require(:project_member).permit(:user_id, :access_level)
end
def log_audit_event(member, options = {})
AuditEventService.new(current_user, @project, options).
for_member(member).security_event
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end
......@@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController
if @blob.lfs_pointer?
send_lfs_object
else
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob)
head :ok # 'render nothing: true' messes up the Content-Type
send_git_blob @repository, @blob
end
else
render_404
......
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
include TreeHelper
include PathLocksHelper
before_action :require_non_empty_project
before_action :validate_ref_id
......@@ -57,16 +58,22 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs)
contents.push(*tree.submodules)
show_path_locks = license_allows_file_locks? && @project.path_locks.any?
@logs = contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
path_lock_info = show_path_locks && @project.path_lock_info(file, exact_match: true)
{
file_name: content.name,
commit: last_commit
commit: last_commit,
path_lock_info: path_lock_info
}
end
offset = (@offset + @limit)
offset = @offset + @limit
if contents.size > offset
@more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: offset)
end
......
......@@ -11,8 +11,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
head :ok
send_git_archive @repository, ref: params[:ref], format: params[:format]
rescue => ex
logger.error("#{self.class.name}: #{ex}")
return git_not_found!
......
class Projects::TodosController < Projects::ApplicationController
def create
todos = TodoService.new.mark_todo(issuable, current_user)
render json: {
todo: todos,
count: current_user.todos.pending.count,
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
count: current_user.todos.pending.count,
}
end
private
def issuable
@issuable ||= begin
case params[:issuable_type]
when "issue"
@project.issues.find(params[:issuable_id])
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
end
end
end
......@@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
if file.on_disk?
send_file file.on_disk_path, disposition: 'inline'
else
......
......@@ -235,7 +235,7 @@ class ProjectsController < Projects::ApplicationController
:issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds,
:public_builds, :only_allow_merge_if_build_succeeds,
# EE-only
:approvals_before_merge,
......
......@@ -41,7 +41,7 @@ class SessionsController < Devise::SessionsController
# Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change.
def check_initial_setup
return unless User.count == 1
return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
user = User.admins.last
......
......@@ -295,7 +295,8 @@ class IssuableFinder
end
def weights?
params[:weight].present? && params[:weight] != Issue::WEIGHT_ALL
params[:weight].present? && params[:weight] != Issue::WEIGHT_ALL &&
klass.column_names.include?('weight')
end
def filter_by_no_weight?
......
......@@ -12,7 +12,7 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
project.issues.find(target_id).notes.inc_author
project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
......
......@@ -51,7 +51,7 @@ class SnippetsFinder
snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user.id) || current_user.admin?
if project.team.member?(current_user) || current_user.admin?
snippets
else
snippets.public_and_internal
......
......@@ -36,7 +36,7 @@ class TodosFinder
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
end
def action_id
......
......@@ -29,7 +29,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", 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 btn-file-option'
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
......@@ -116,7 +116,7 @@ module BlobHelper
end
def blob_text_viewable?(blob)
blob && blob.text? && !blob.lfs_pointer?
blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
end
def blob_size(blob)
......
......@@ -14,4 +14,8 @@ module BranchesHelper
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
end
......@@ -17,7 +17,15 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
class: 'btn btn-clipboard',
class: "btn",
data: data,
type: :button
end
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data,
type: :button
end
......
......@@ -38,10 +38,10 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
def render_commit_status(commit, tooltip_placement: 'auto left')
def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement)
render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
......@@ -57,10 +57,10 @@ module CiStatusHelper
private
def render_status_with_link(type, status, path, tooltip_placement)
def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
link_to ci_icon_for_status(status),
path,
class: "ci-status-link ci-status-icon-#{status.dasherize}",
class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
......
......@@ -16,6 +16,16 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
def commit_author_avatar(commit, options = {})
options = options.merge(source: :author)
user = commit.send(options[:source])
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_email = user.try(:email) || source_email
image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
end
def image_diff_class(diff)
if diff.deleted_file
"deleted"
......@@ -102,24 +112,24 @@ module CommitsHelper
if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to(
"Browse File »",
"Browse File",
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
class: "btn btn-default"
)
elsif @path.present?
return link_to(
"Browse Directory »",
"Browse Directory",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
class: "btn btn-default"
)
end
end
link_to(
"Browse Files",
namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right"
class: "btn btn-default"
)
end
......@@ -129,7 +139,7 @@ module CommitsHelper
tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
if can_collaborate_with_project?
btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil?
btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
......@@ -141,7 +151,7 @@ module CommitsHelper
namespace_key: current_user.namespace.id,
continue: continue_params)
btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
......@@ -153,7 +163,7 @@ module CommitsHelper
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project?
btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil?
btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
......@@ -187,12 +197,10 @@ module CommitsHelper
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name
person_email = user.try(:email) || source_email
text =
if options[:avatar]
avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
%Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
%Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
else
person_name
end
......
......@@ -135,6 +135,11 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
def diff_compare_whitespace_link(project, from, to, options)
url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace)
toggle_whitespace_link(url, options)
end
private
def hide_whitespace?
......
......@@ -67,9 +67,9 @@ module DropdownsHelper
end
end
def dropdown_filter(placeholder)
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search', class: "dropdown-input-search")
filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
......
......@@ -13,10 +13,23 @@
# merge_request_path(merge_request)
#
module GitlabRoutingHelper
# Project
def project_path(project, *args)
namespace_project_path(project.namespace, project, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end
......@@ -29,6 +42,10 @@ module GitlabRoutingHelper
namespace_project_pipelines_path(project.namespace, project, *args)
end
def project_environments_path(project, *args)
namespace_project_environments_path(project.namespace, project, *args)
end
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
......@@ -41,10 +58,6 @@ module GitlabRoutingHelper
activity_namespace_project_path(project.namespace, project, *args)
end
def edit_project_path(project, *args)
edit_namespace_project_path(project.namespace, project, *args)
end
def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args)
end
......@@ -65,14 +78,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
def edit_project_url(project, *args)
edit_namespace_project_url(project.namespace, project, *args)
end
def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end
......@@ -92,4 +97,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
end
def project_member_path(project_member, *args)
namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def request_access_project_members_path(project, *args)
request_access_namespace_project_project_members_path(project.namespace, project)
end
def leave_project_members_path(project, *args)
leave_namespace_project_project_members_path(project.namespace, project)
end
def approve_access_request_project_member_path(project_member, *args)
approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
def resend_invite_project_member_path(project_member, *args)
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
# Groups
## Members
def group_members_url(group, *args)
group_group_members_url(group, *args)
end
def group_member_path(group_member, *args)
group_group_member_path(group_member.source, group_member)
end
def request_access_group_members_path(group, *args)
request_access_group_group_members_path(group)
end
def leave_group_members_path(group, *args)
leave_group_group_members_path(group)
end
def approve_access_request_group_member_path(group_member, *args)
approve_access_request_group_group_member_path(group_member.source, group_member)
end
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
end
module GroupsHelper
def remove_user_from_group_message(group, member)
if member.user
"Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
else
"Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
end
end
def leave_group_message(group)
"Are you sure you want to leave \"#{group}\" group?"
end
def should_user_see_group_roles?(user, group)
if user && group
user.is_admin? || group.members.exists?(user_id: user.id)
else
false
end
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
......
......@@ -67,6 +67,12 @@ module IssuablesHelper
end
end
def has_todo(issuable)
unless current_user.nil?
current_user.todos.find_by(target_id: issuable.id, state: :pending)
end
end
private
def sidebar_gutter_collapsed?
......
module MembersHelper
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member
def action_member_permission(action, member)
"#{action}_#{member.type.underscore}".to_sym
end
def can_see_member_roles?(source:, user: nil)
return false unless user
user.is_admin? || source.members.exists?(user_id: user.id)
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
text = 'Are you sure you want to '
action =
if member.request?
if member.user == user
'withdraw your access request for'
else
"deny #{member.user.name}'s request to join"
end
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else
"remove #{member.user.name} from"
end
text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
end
def remove_member_title(member)
text = " from #{member.real_source_type.humanize(capitalize: false)}"
text.prepend(member.request? ? 'Deny access request' : 'Remove user')
end
def leave_confirmation_message(member_source)
"Are you sure you want to leave the " \
"\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
end
end
......@@ -12,10 +12,10 @@ module NavHelper
end
def page_sidebar_class
if nav_menu_collapsed?
"page-sidebar-collapsed"
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
else
"page-sidebar-expanded"
"page-sidebar-collapsed"
end
end
......@@ -30,17 +30,21 @@ module NavHelper
else
"page-gutter right-sidebar-expanded"
end
elsif current_path?('builds#show')
"page-gutter build-sidebar right-sidebar-expanded"
end
end
def nav_header_class
class_name =
if nav_menu_collapsed?
"header-collapsed"
else
"header-expanded"
end
class_name += " with-horizontal-nav" if defined?(nav) && nav
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-expanded header-pinned-nav"
else
class_name << " header-collapsed"
end
class_name
end
......@@ -51,4 +55,8 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end
......@@ -61,4 +61,23 @@ module NotificationsHelper
end
end
end
def notification_level_radio_buttons
html = ""
NotificationSetting.levels.each_key do |level|
level = level.to_sym
next if level == :global
html << content_tag(:div, class: "radio") do
content_tag(:label, { value: level }) do
radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) +
content_tag(:div, level.to_s.capitalize, class: "level-title") +
content_tag(:p, notification_description(level))
end
end
end
html.html_safe
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment