Commit 2f545a15 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into ci-lfs-fetch

parents 95a10f45 44b8b77e
......@@ -7,7 +7,8 @@ services:
cache:
key: "ruby21"
paths:
- vendor
- vendor/apt
- vendor/ruby
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
......@@ -91,9 +92,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/
......@@ -130,56 +129,51 @@ spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.2
.ruby-22: &ruby-22
image: "ruby:2.2"
# Execute all testing suites against Ruby 2.3
.ruby-23: &ruby-23
image: "ruby:2.3"
only:
- master
cache:
key: "ruby22"
paths:
- vendor
.rspec-knapsack-ruby22: &rspec-knapsack-ruby22
.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
<<: *rspec-knapsack
<<: *ruby-22
<<: *ruby-23
.spinach-knapsack-ruby22: &spinach-knapsack-ruby22
.spinach-knapsack-ruby23: &spinach-knapsack-ruby23
<<: *spinach-knapsack
<<: *ruby-22
rspec 0 20 ruby22: *rspec-knapsack-ruby22
rspec 1 20 ruby22: *rspec-knapsack-ruby22
rspec 2 20 ruby22: *rspec-knapsack-ruby22
rspec 3 20 ruby22: *rspec-knapsack-ruby22
rspec 4 20 ruby22: *rspec-knapsack-ruby22
rspec 5 20 ruby22: *rspec-knapsack-ruby22
rspec 6 20 ruby22: *rspec-knapsack-ruby22
rspec 7 20 ruby22: *rspec-knapsack-ruby22
rspec 8 20 ruby22: *rspec-knapsack-ruby22
rspec 9 20 ruby22: *rspec-knapsack-ruby22
rspec 10 20 ruby22: *rspec-knapsack-ruby22
rspec 11 20 ruby22: *rspec-knapsack-ruby22
rspec 12 20 ruby22: *rspec-knapsack-ruby22
rspec 13 20 ruby22: *rspec-knapsack-ruby22
rspec 14 20 ruby22: *rspec-knapsack-ruby22
rspec 15 20 ruby22: *rspec-knapsack-ruby22
rspec 16 20 ruby22: *rspec-knapsack-ruby22
rspec 17 20 ruby22: *rspec-knapsack-ruby22
rspec 18 20 ruby22: *rspec-knapsack-ruby22
rspec 19 20 ruby22: *rspec-knapsack-ruby22
spinach 0 10 ruby22: *spinach-knapsack-ruby22
spinach 1 10 ruby22: *spinach-knapsack-ruby22
spinach 2 10 ruby22: *spinach-knapsack-ruby22
spinach 3 10 ruby22: *spinach-knapsack-ruby22
spinach 4 10 ruby22: *spinach-knapsack-ruby22
spinach 5 10 ruby22: *spinach-knapsack-ruby22
spinach 6 10 ruby22: *spinach-knapsack-ruby22
spinach 7 10 ruby22: *spinach-knapsack-ruby22
spinach 8 10 ruby22: *spinach-knapsack-ruby22
spinach 9 10 ruby22: *spinach-knapsack-ruby22
<<: *ruby-23
rspec 0 20 ruby23: *rspec-knapsack-ruby23
rspec 1 20 ruby23: *rspec-knapsack-ruby23
rspec 2 20 ruby23: *rspec-knapsack-ruby23
rspec 3 20 ruby23: *rspec-knapsack-ruby23
rspec 4 20 ruby23: *rspec-knapsack-ruby23
rspec 5 20 ruby23: *rspec-knapsack-ruby23
rspec 6 20 ruby23: *rspec-knapsack-ruby23
rspec 7 20 ruby23: *rspec-knapsack-ruby23
rspec 8 20 ruby23: *rspec-knapsack-ruby23
rspec 9 20 ruby23: *rspec-knapsack-ruby23
rspec 10 20 ruby23: *rspec-knapsack-ruby23
rspec 11 20 ruby23: *rspec-knapsack-ruby23
rspec 12 20 ruby23: *rspec-knapsack-ruby23
rspec 13 20 ruby23: *rspec-knapsack-ruby23
rspec 14 20 ruby23: *rspec-knapsack-ruby23
rspec 15 20 ruby23: *rspec-knapsack-ruby23
rspec 16 20 ruby23: *rspec-knapsack-ruby23
rspec 17 20 ruby23: *rspec-knapsack-ruby23
rspec 18 20 ruby23: *rspec-knapsack-ruby23
rspec 19 20 ruby23: *rspec-knapsack-ruby23
spinach 0 10 ruby23: *spinach-knapsack-ruby23
spinach 1 10 ruby23: *spinach-knapsack-ruby23
spinach 2 10 ruby23: *spinach-knapsack-ruby23
spinach 3 10 ruby23: *spinach-knapsack-ruby23
spinach 4 10 ruby23: *spinach-knapsack-ruby23
spinach 5 10 ruby23: *spinach-knapsack-ruby23
spinach 6 10 ruby23: *spinach-knapsack-ruby23
spinach 7 10 ruby23: *spinach-knapsack-ruby23
spinach 8 10 ruby23: *spinach-knapsack-ruby23
spinach 9 10 ruby23: *spinach-knapsack-ruby23
# Other generic tests
......
......@@ -349,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.
......@@ -52,7 +52,7 @@ gem "browser", '~> 2.0.3'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", '~> 10.0'
gem "gitlab_git", '~> 10.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
......@@ -210,6 +210,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'
......@@ -218,13 +221,12 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'font-awesome-rails', '~> 4.6.1'
gem 'gitlab_emoji', '~> 0.3.0'
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'
......@@ -245,7 +247,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)
......@@ -253,7 +246,7 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
font-awesome-rails (4.5.0.1)
font-awesome-rails (4.6.1.0)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
thor (~> 0.19.1)
......@@ -284,7 +277,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
gitlab_git (10.1.0)
gitlab_git (10.2.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
......@@ -338,7 +331,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
......@@ -408,7 +400,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)
......@@ -424,6 +416,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)
......@@ -563,7 +556,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)
......@@ -642,10 +634,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)
......@@ -655,7 +644,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)
......@@ -704,9 +693,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
......@@ -747,10 +733,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)
......@@ -759,7 +743,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)
......@@ -848,7 +832,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
......@@ -857,6 +841,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)
......@@ -881,7 +866,7 @@ DEPENDENCIES
fog-google (~> 0.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
font-awesome-rails (~> 4.2)
font-awesome-rails (~> 4.6.1)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
......@@ -889,7 +874,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0)
gitlab_git (~> 10.0)
gitlab_git (~> 10.2)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
......@@ -952,7 +937,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)
......
......@@ -27,6 +27,11 @@ class @LabelManager
$btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}")
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
# Make sure tooltip will hide
$tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
$tooltip.tooltip 'destroy'
_this.toggleLabelPriority($label, action)
toggleLabelPriority: ($label, action, persistState = true) ->
......@@ -42,10 +47,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 +59,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)
......
......@@ -32,10 +32,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
......@@ -125,9 +121,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", ->
......@@ -162,19 +159,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()
......@@ -207,6 +191,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")
......@@ -241,7 +226,6 @@ $ ->
$this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle')
$navIconToggle = $('.toggle-nav-collapse')
$(document)
.off 'breakpoint:change'
......@@ -251,10 +235,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()
......@@ -274,3 +254,31 @@ $ ->
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 @BlobGitignoreSelector
constructor: (opts) ->
{
@dropdown
@editor
@$wrapper = @dropdown.closest('.gitignore-selector')
@$filenameInput = $('#file_name')
@data = @dropdown.data('filenames')
} = opts
#= require blob/template_selector
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (gitignore) ->
gitignore.name
)
@toggleGitignoreSelector()
@bindEvents()
bindEvents: ->
@$filenameInput
.on 'keyup blur', (e) =>
@toggleGitignoreSelector()
toggleGitignoreSelector: ->
filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
@$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
onClick: (item, el, e) =>
e.preventDefault()
@requestIgnoreFile(item.name)
requestIgnoreFile: (name) ->
Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
requestIgnoreFileSuccess: (gitignore) ->
@editor.setValue(gitignore.content, 1)
@editor.focus()
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
dropdown: $dropdown,
editor: @editor
)
class @BlobGitignoreSelector extends TemplateSelector
requestFile: (query) ->
Api.gitignoreText query.name, @requestFileSuccess.bind(@)
class @BlobGitignoreSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-gitignore-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobGitignoreSelector(
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
class @BlobLicenseSelector
licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
#= require blob/template_selector
constructor: (editor) ->
@$licenseSelector = $('.js-license-selector')
$fileNameInput = $('#file_name')
initialFileNameValue = if $fileNameInput.length
$fileNameInput.val()
else if $('.editor-file-name').length
$('.editor-file-name').text().trim()
@toggleLicenseSelector(initialFileNameValue)
if $fileNameInput
$fileNameInput.on 'keyup blur', (e) =>
@toggleLicenseSelector($(e.target).val())
$('select.license-select').on 'change', (e) ->
class @BlobLicenseSelector extends TemplateSelector
requestFile: (query) ->
data =
project: $(this).data('project')
fullname: $(this).data('fullname')
Api.licenseText $(this).val(), data, (license) ->
editor.setValue(license.content, -1)
project: @dropdown.data('project')
fullname: @dropdown.data('fullname')
toggleLicenseSelector: (fileName) =>
if @licenseRegex.test(fileName)
@$licenseSelector.show()
else
@$licenseSelector.hide()
Api.licenseText query.id, data, @requestFileSuccess.bind(@)
class @BlobLicenseSelectors
constructor: (opts) ->
{
@$dropdowns = $('.js-license-selector')
@editor
} = opts
@$dropdowns.each (i, dropdown) =>
$dropdown = $(dropdown)
new BlobLicenseSelector(
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
editor: @editor
)
......@@ -12,8 +12,9 @@ class @EditBlob
$("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
new BlobLicenseSelector(@editor)
new BlobGitignoreSelectors(editor: @editor)
new BlobLicenseSelectors { @editor }
new BlobGitignoreSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
......
class @TemplateSelector
constructor: (opts = {}) ->
{
@dropdown,
@data,
@pattern,
@wrapper,
@editor,
@fileEndpoint,
@$input = $('#file_name')
} = opts
@buildDropdown()
@bindEvents()
@onFilenameUpdate()
buildDropdown: ->
@dropdown.glDropdown(
data: @data,
filterable: true,
selectable: true,
search:
fields: ['name']
clicked: @onClick
text: (item) ->
item.name
)
bindEvents: ->
@$input.on('keyup blur', (e) =>
@onFilenameUpdate()
)
onFilenameUpdate: ->
return unless @$input.length
filenameMatches = @pattern.test(@$input.val().trim())
if not filenameMatches
@wrapper.addClass('hidden')
return
@wrapper.removeClass('hidden')
onClick: (item, el, e) =>
e.preventDefault()
@requestFile(item)
requestFile: (item) ->
# To be implemented on the extending class
# e.g.
# Api.gitignoreText item.name, @requestFileSuccess.bind(@)
requestFileSuccess: (file) ->
@editor.setValue(file.content, 1)
@editor.focus()
......@@ -17,6 +17,8 @@ class @CiBuild
.off 'resize.build'
.on 'resize.build', @hideSidebar
@updateArtifactRemoveDate()
if $('#build-trace').length
@getInitialBuildTrace()
@initScrollButtonAffix()
......@@ -103,3 +105,10 @@ class @CiBuild
$('.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,13 +73,12 @@ 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()
new NotificationsForm()
new TreeView() if $('#tree-slider').length
when 'groups:activity'
new Activities()
......@@ -96,6 +100,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'
......@@ -125,19 +130,21 @@ class Dispatcher
shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles'
new Profile()
new NotificationsForm()
new NotificationsDropdown()
when 'projects'
new Project()
new ProjectAvatar()
switch path[1]
when 'compare'
shortcut_handler = new ShortcutsNavigation()
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
when 'new'
new ProjectNew()
when 'show'
new ProjectNew()
new ProjectShow()
new NotificationsDropdown()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
......@@ -146,9 +153,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 ''
......
......@@ -15,6 +15,9 @@ GitLab.GfmAutoComplete =
Members:
template: '<li>${username} <small>${title}</small></li>'
Labels:
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
# Issues and MergeRequests
Issues:
template: '<li><small>${id}</small> ${title}</li>'
......@@ -176,6 +179,25 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
@input.atwho
at: '~'
alias: 'labels'
searchKey: 'search'
displayTpl: @Labels.template
insertTpl: '${atwho-at}${title}'
callbacks:
beforeSave: (merges) ->
sanitizeLabelTitle = (title)->
if /\w+\s+\w+/g.test(title)
"\"#{sanitize(title)}\""
else
sanitize(title)
$.map merges, (m) ->
title: sanitizeLabelTitle(m.title)
color: m.color
search: "#{m.title}"
destroyAtWho: ->
@input.atwho('destroy')
......@@ -195,6 +217,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
# load labels
@input.atwho 'load', '~', data.labels
# This trigger at.js again
# otherwise we would be stuck with loading until the user types
......
......@@ -302,6 +302,9 @@ class GitLabDropdown
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
if @options.setActiveIds
@options.setActiveIds.call(@)
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
......
......@@ -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
......@@ -145,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data)
labelCount = data.labels.length
else
template = labelNoneHTMLTemplate()
template = labelNoneHTMLTemplate
$value
.removeAttr('style')
.html(template)
......@@ -210,9 +210,21 @@ class @LabelsSelect
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
active = instance.activeIds
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
if active.indexOf(label.id) isnt -1
# Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf 'is-indeterminate'
selectedClass.splice i, 1 unless i is -1
selectedClass.push 'is-active'
# Add input manually
instance.addInput @fieldName, label.id
if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
......@@ -328,6 +340,10 @@ class @LabelsSelect
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
setActiveIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@activeIds = _this.getActiveIds()
)
@bindEvents()
......@@ -352,3 +368,12 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
getActiveIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.intersection.apply _, label_ids
class @LayoutNav
$ ->
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)
$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
maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
$el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
$el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
$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
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()
......@@ -9,7 +9,7 @@ class @MergeRequest
# Options:
# action - String, current controller action
#
constructor: (@opts) ->
constructor: (@opts = {}) ->
this.$el = $('.merge-request')
this.$('.show-all-commits').on 'click', =>
......
......@@ -88,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) ->
if window.location.hash
navBarHeight = $('.navbar-gitlab').outerHeight()
navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
......
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 @NotificationsDropdown
$ ->
$(document)
.off 'click', '.update-notification'
.on 'click', '.update-notification', (e) ->
e.preventDefault()
return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
notificationLevel = $(@).data 'notification-level'
label = $(@).data 'notification-title'
form = $(this).parents('.notification-form:first')
form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
form.find('#notification_setting_level').val(notificationLevel)
form.submit()
$(document)
.off 'ajax:success', '.notification-form'
.on 'ajax:success', '.notification-form', (e, data) ->
if data.saved
new Flash('Notification settings saved', 'notice')
$(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html)
else
new Flash('Failed to save new settings', 'alert')
class @NotificationsForm
constructor: ->
@removeEventListeners()
@initEventListeners()
removeEventListeners: ->
$(document).off 'change', '.js-custom-notification-event'
initEventListeners: ->
$(document).on 'change', '.js-custom-notification-event', @toggleCheckbox
toggleCheckbox: (e) =>
$checkbox = $(e.currentTarget)
$parent = $checkbox.closest('.checkbox')
@saveEvent($checkbox, $parent)
showCheckboxLoadingSpinner: ($parent) ->
$parent
.addClass 'is-loading'
.find '.custom-notification-event-loading'
.removeClass 'fa-check'
.addClass 'fa-spin fa-spinner'
.removeClass 'is-done'
saveEvent: ($checkbox, $parent) ->
form = $parent.parents('form:first')
$.ajax(
url: form.attr('action')
method: form.attr('method')
dataType: 'json'
data: form.serialize()
beforeSend: =>
@showCheckboxLoadingSpinner($parent)
).done (data) ->
$checkbox.enable()
if data.saved
$parent
.find '.custom-notification-event-loading'
.toggleClass 'fa-spin fa-spinner fa-check is-done'
setTimeout(->
$parent
.removeClass 'is-loading'
.find '.custom-notification-event-loading'
.toggleClass 'fa-spin fa-spinner fa-check is-done'
, 2000)
......@@ -8,6 +8,10 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
# Automatically submit email form when it changes
$('#user_notification_email').on 'change', ->
$(this).parents('form').submit()
$('.update-username').on 'ajax:before', ->
$('.loading-username').show()
$(this).find('.update-success').hide()
......
......@@ -34,22 +34,6 @@ class @Project
$(@).parents('.no-password-message').remove()
e.preventDefault()
$('.update-notification').on 'click', (e) ->
e.preventDefault()
notification_level = $(@).data 'notification-level'
label = $(@).data 'notification-title'
$('#notification_setting_level').val(notification_level)
$('#notification-form').submit()
$('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
$(@).parents('ul').find('li.active').removeClass 'active'
$(@).parent().addClass 'active'
$('#notification-form').on 'ajax:success', (e, data) ->
if data.saved
new Flash("Notification settings saved", "notice")
else
new Flash("Failed to save new settings", "alert")
@projectSelectDropdown()
......
......@@ -43,6 +43,59 @@ 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-delete-path') then 'DELETE' else 'POST'
if $this.attr('data-delete-path')
url = "#{$this.attr('data-delete-path')}"
else
url = "#{$this.data('url')}"
$.ajax(
url: url
type: ajaxType
dataType: 'json'
data:
issuable_id: $this.data('issuable-id')
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.delete_path?
$btn
.attr 'aria-label', $btn.data('mark-text')
.attr 'data-delete-path', data.delete_path
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-delete-path'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
......@@ -117,5 +170,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
......
......@@ -6,12 +6,6 @@ class @Calendar
@daySizeWithSpace = @daySize + (@daySpace * 2)
@monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
@months = []
@highestValue = 0
# Get the highest value from the timestampes
_.each timestamps, (count) =>
if count > @highestValue
@highestValue = count
# Loop through the timestamps to create a group of objects
# The group of objects will be grouped based on the day of the week they are
......@@ -39,8 +33,8 @@ class @Calendar
i++
# Init color functions
@color = @initColor()
@colorKey = @initColorKey()
@color = @initColor()
# Init the svg element
@renderSvg(group)
......@@ -104,7 +98,7 @@ class @Calendar
.attr 'class', 'user-contrib-cell js-tooltip'
.attr 'fill', (stamp) =>
if stamp.count isnt 0
@color(stamp.count)
@color(Math.min(stamp.count, 40))
else
'#ededed'
.attr 'data-container', 'body'
......@@ -164,10 +158,11 @@ class @Calendar
color
initColor: ->
colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
d3.scale
.linear()
.range(['#acd5f2', '#254e77'])
.domain([0, @highestValue])
.threshold()
.domain([0, 10, 20, 30])
.range(colorRange)
initColorKey: ->
d3.scale
......
......@@ -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
......
......@@ -91,6 +91,10 @@
background-color: $white-light;
border-top: none;
}
&.top-block .container-fluid {
background-color: inherit;
}
}
.cover-block {
......
......@@ -8,33 +8,15 @@
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.header-logo {
background: $color-darker;
a {
color: $color-light;
h3 {
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
}
}
background: $color;
&:hover {
background-color: $color-dark;
a {
color: $white-light;
h3 {
color: $white-light;
}
}
}
}
.collapse-nav a {
color: $white-light;
background: $color;
}
.sidebar-wrapper {
background: $color-darker;
......
......@@ -2,8 +2,19 @@
* Application Header
*
*/
@mixin tanuki-logo-colors($path-color) {
fill: $path-color;
transition: all 0.8s;
&:hover,
&.highlight {
fill: lighten($path-color, 25%);
transition: all 0.1s;
}
}
header {
transition-duration: .3s;
transition: padding $sidebar-transition-duration;
&.navbar-empty {
height: $header-height;
......@@ -79,14 +90,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 +114,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 +124,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 +155,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 +186,10 @@ header {
.navbar-collapse {
float: right;
border-top: none;
@media (max-width: $screen-xs-max) {
float: none;
}
}
}
......@@ -172,22 +202,24 @@ header {
}
}
.header-collapsed {
margin-left: 0;
#tanuki-logo {
.header-content {
padding-left: 30px;
transition-duration: .3s;
#tanuki-left-ear,
#tanuki-right-ear,
#tanuki-nose {
@include tanuki-logo-colors($tanuki-red);
}
}
.header-expanded {
margin-left: 0;
#tanuki-left-eye,
#tanuki-right-eye {
@include tanuki-logo-colors($tanuki-orange);
}
.header-content {
margin-left: $sidebar_width;
transition-duration: .3s;
#tanuki-left-cheek,
#tanuki-right-cheek {
@include tanuki-logo-colors($tanuki-yellow);
}
}
@media (max-width: $screen-xs-max) {
......
......@@ -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;
}
}
}
......
......@@ -52,6 +52,19 @@
.git-clone-holder {
display: none;
}
// Display Star and Fork buttons without counters on mobile.
.project-action-buttons {
display: block;
.count-buttons .btn {
margin: 0 10px;
}
.count-buttons .count-with-arrow {
display: none;
}
}
}
.project-stats {
......
@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,83 +39,63 @@
}
}
.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;
.sidebar-user {
padding: 15px;
position: absolute;
left: 0;
bottom: 0;
width: $sidebar_width;
overflow: hidden;
transition-duration: .3s;
.username {
margin-left: 10px;
width: $sidebar_width - 2 * 10px;
font-size: 16px;
line-height: 34px;
}
}
}
line-height: 36px;
transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
.tanuki-shape {
transition: all 0.8s;
&:hover, &.highlight {
fill: rgb(255, 255, 255);
transition: all 0.1s;
@media (min-width: $sidebar-breakpoint) {
bottom: 50px;
}
}
.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,141 +103,100 @@
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;
}
}
.page-sidebar-collapsed {
padding-left: 0;
.sidebar-wrapper {
width: 0;
.header-logo {
width: 0;
padding: 8px 0;
a {
padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
.pin-nav-btn {
display: none;
}
}
}
position: absolute;
left: 0;
bottom: 0;
height: 50px;
width: $sidebar_width;
line-height: 30px;
#logo {
display: none;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.nav-sidebar {
width: $sidebar_collapsed_width;
li {
width: auto;
a {
span {
display: none;
}
}
}
.fa {
transition: transform .15s;
}
.collapse-nav a {
width: 0;
&.is-active {
.fa {
transform: rotate(90deg);
}
}
}
.sidebar-user {
width: 0;
.page-sidebar-collapsed {
padding-left: 0;
padding-right: 0;
.username {
display: none;
}
}
.sidebar-wrapper {
width: 0;
}
}
.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 (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
}
}
}
.layout-nav {
@media (max-width: $screen-xs-min) {
padding-right: 0;
}
header.header-pinned-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
@media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
padding-right: 90px;
.side-nav-toggle {
display: none;
}
@media (min-width: $screen-md-min) {
padding-right: $sidebar_width;
.header-content {
padding-left: 0;
}
}
}
......
......@@ -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
......@@ -154,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
$tanuki-yellow: #fca326;
/*
* State colors:
*/
......@@ -261,5 +268,10 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
/*
* Personal Access Tokens
*/
$personal-access-tokens-disabled-label-color: #bbb;
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
......@@ -38,6 +38,10 @@ table {
margin: 0 auto;
text-align: left;
width: 600px;
& > td {
text-align: center;
}
}
&#body {
......
......@@ -26,6 +26,8 @@
.commit-info-row {
margin-bottom: 10px;
line-height: 24px;
padding-top: 6px;
&.commit-info-row-header {
line-height: 34px;
......
......@@ -7,84 +7,111 @@
margin-right: 9px;
}
.lists-separator {
margin: 10px 0;
border-color: #ddd;
}
.commits-row {
ul {
margin: 0;
li.commit {
padding: 8px 0;
}
}
.commit-header {
padding: 5px 10px;
background-color: $background-color;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
font-size: 14px;
.commits-row-date {
font-size: 15px;
line-height: 20px;
margin-bottom: 5px;
&:first-child {
border-top-width: 0;
}
}
li.commit {
list-style: none;
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
margin-bottom: 2px;
.btn-clipboard {
margin-top: -1px;
}
.commit-row-title {
line-height: 1;
margin-bottom: 7px;
.notes_count {
float: right;
margin-right: 10px;
}
.commit_short_id {
min-width: 65px;
color: $gl-dark-link-color;
font-family: $monospace_font;
}
.str-truncated {
max-width: 70%;
}
.commit-row-message {
color: $gl-dark-link-color;
&:hover {
text-decoration: underline;
}
}
.text-expander {
background: #eee;
color: #555;
display: inline-block;
background: $gray-light;
color: $gl-placeholder-color;
padding: 0 5px;
cursor: pointer;
margin-left: 4px;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
&:hover {
background-color: #ddd;
background-color: darken($gray-light, 10%);
text-decoration: none;
}
}
}
.commit-actions {
@media (min-width: $screen-sm-min) {
float: right;
margin-left: $gl-padding;
margin-top: 2px;
font-size: 0;
}
.btn-transparent {
padding-left: 0;
padding-right: 0;
}
.btn {
&:not(:first-child) {
margin-left: $gl-padding;
}
}
}
.commit-short-id {
font-family: $monospace_font;
font-weight: 600;
}
.commit {
padding: 10px 0;
@media (min-width: $screen-sm-min) {
padding-left: 46px;
}
&:not(:last-child) {
border-bottom: 1px solid #eee;
}
a,
button {
color: $gl-dark-link-color;
vertical-align: baseline;
}
.avatar {
margin-left: -46px;
}
.item-title {
display: inline-block;
@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 {
......
......@@ -66,8 +66,7 @@
font-family: $regular_font;
}
.gitignore-selector {
.gitignore-selector, .license-selector {
.dropdown {
line-height: 21px;
}
......
.environments {
.commit-title {
margin: 0;
}
}
......@@ -136,9 +136,10 @@
.event-last-push {
overflow: auto;
width: 100%;
.event-last-push-text {
@include str-truncated(100%);
padding: 5px 0;
padding: 4px 0;
font-size: 13px;
float: left;
margin-right: -150px;
......
......@@ -39,3 +39,20 @@
}
}
}
.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;
}
}
......@@ -34,6 +34,10 @@
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;
......@@ -145,7 +145,6 @@
.assign-yourself {
margin-top: 10px;
font-weight: normal;
display: block;
}
}
......@@ -158,6 +157,10 @@
font-weight: normal;
}
.no-value {
color: $gl-placeholder-color;
}
.sidebar-collapsed-icon {
display: none;
}
......@@ -248,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;
......@@ -263,7 +271,7 @@
}
}
a:not(.issuable-pager) {
a {
&:hover {
color: $md-link-color;
text-decoration: none;
......@@ -322,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 {
......
......@@ -244,6 +244,10 @@
.panel-footer {
padding: 5px 10px;
.btn {
min-width: auto;
}
}
.commit {
......@@ -252,9 +256,7 @@
}
.avatar {
width: 20px;
height: 20px;
margin-right: 5px;
margin-left: 0;
}
.commit-row-info {
......@@ -313,3 +315,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 {
......@@ -259,6 +265,10 @@ ul.notes {
right: 0;
top: 0;
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) {
position: relative;
}
......
......@@ -192,6 +192,25 @@
}
}
.personal-access-tokens-never-expires-label {
color: $personal-access-tokens-disabled-label-color;
}
.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
text-align: center;
}
.created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
}
.user-profile {
@media (max-width: $screen-xs-max) {
.cover-block {
......
......@@ -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;
......@@ -33,7 +28,7 @@
.container-fluid {
position: relative;
@media (min-width: $screen-md-max) {
@media (min-width: $screen-lg-min) {
.row {
display: flex;
-ms-flex-align: center;
......@@ -133,11 +128,6 @@
}
}
.btn-group:not(:first-child):not(:last-child) > .btn {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
form {
margin-left: 10px;
}
......@@ -229,13 +219,20 @@
right: 16px;
bottom: 0;
.btn {
padding: 3px 10px;
background-color: $background-color;
@media (max-width: $screen-md-max) {
top: 0;
}
@media (max-width: 1304px) {
top: 0;
.access-request-button {
position: absolute;
right: 0;
bottom: 61px;
@media (max-width: $screen-md-max) {
position: relative;
bottom: 0;
margin-right: 10px;
}
}
}
......@@ -286,10 +283,6 @@
color: #555;
}
.project_member_row form {
margin: 0;
}
.transfer-project .select2-container {
min-width: 200px;
}
......@@ -373,6 +366,7 @@ a.deploy-project-label {
.project-import .btn {
float: left;
margin-bottom: 10px;
margin-right: 10px;
}
......@@ -500,7 +494,8 @@ pre.light-well {
.activity-filter-block {
.controls {
padding-bottom: 10px;
padding-bottom: 7px;
margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
......@@ -604,3 +599,20 @@ pre.light-well {
}
}
}
.custom-notifications-form {
.is-loading {
.custom-notification-event-loading {
display: inline-block;
}
}
}
.custom-notification-event-loading {
display: none;
margin-left: 5px;
&.is-done {
color: $gl-text-green;
}
}
......@@ -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;
}
......@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper
include WorkhorseHelper
before_action :authenticate_user_from_token!
before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
......@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
......@@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base
end
end
# From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
# https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
def authenticate_user_from_token!
user_token = if params[:authenticity_token].presence
params[:authenticity_token].presence
elsif params[:private_token].presence
params[:private_token].presence
elsif request.headers['PRIVATE-TOKEN'].present?
request.headers['PRIVATE-TOKEN']
end
user = user_token && User.find_by_authentication_token(user_token.to_s)
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
if user
# Notice we are passing store false, so the user is not
......@@ -326,6 +319,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
def gitlab_project_import_enabled?
current_application_settings.import_sources.include?('gitlab_project')
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
......
......@@ -35,6 +35,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
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
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
end
class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy, :destroy_all]
include TodosHelper
before_action :find_todos, only: [:index, :destroy_all]
def index
@todos = @todos.page(params[:page])
end
def destroy
todo.done
todo_notice = 'Todo was successfully marked as done.'
TodoService.new.mark_todos_as_done([todo], current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { head :ok }
format.json do
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
def destroy_all
@todos.each(&:done)
TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
format.json do
find_todos
render json: { count: @todos.size, done_count: current_user.todos.done.count }
end
format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
private
def todo
@todo ||= current_user.todos.find(params[:id])
@todo ||= find_todos.find(params[:id])
end
def find_todos
@todos = TodosFinder.new(current_user, params).execute
@todos ||= TodosFinder.new(current_user, params).execute
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
......@@ -58,25 +60,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
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
# MembershipActions concern
alias_method :membershipable, :group
def cannot_leave?
@group.last_owner?(current_user)
end
end
class Groups::NotificationSettingsController < Groups::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(group)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
def new
@namespace_id = project_params[:namespace_id]
@namespace_name = Namespace.find(project_params[:namespace_id]).name
@path = project_params[:path]
end
def create
unless file_is_valid?
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
File.expand_path(project_params[:file].path),
project_params[:path]).execute
if @project.saved?
redirect_to(
project_path(@project),
notice: "Project '#{@project.name}' is being imported."
)
else
redirect_to(
new_import_gitlab_project_path,
alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}"
)
end
end
private
def file_is_valid?
project_params[:file] && project_params[:file].respond_to?(:read)
end
def verify_gitlab_project_import_enabled
render_404 unless gitlab_project_import_enabled?
end
def project_params
params.permit(
:path, :namespace_id, :file
)
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
......
class NotificationSettingsController < ApplicationController
before_action :authenticate_user!
def create
project = Project.find(params[:project][:id])
return render_404 unless can?(current_user, :read_project, project)
@notification_setting = current_user.notification_settings_for(project)
@saved = @notification_setting.update_attributes(notification_setting_params)
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update_attributes(notification_setting_params)
render_response
end
private
def render_response
render json: {
html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
saved: @saved
}
end
def notification_setting_params
allowed_fields = NotificationSetting::EMAIL_EVENTS.dup
allowed_fields << :level
params.require(:notification_setting).permit(allowed_fields)
end
end
......@@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
def unlink
provider = params[:provider]
current_user.identities.find_by(provider: provider).destroy
current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
redirect_to profile_account_path
end
end
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
@group_notifications = current_user.notification_settings.for_groups.order(:id)
@project_notifications = current_user.notification_settings.for_projects.order(:id)
@global_notification_setting = current_user.global_notification_setting
end
def update
......@@ -16,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
params.require(:user).permit(:notification_email, :notification_level)
params.require(:user).permit(:notification_email)
end
end
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
before_action :load_personal_access_tokens, only: :index
def index
@personal_access_token = current_user.personal_access_tokens.build
end
def create
@personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
load_personal_access_tokens
render :index
end
end
def revoke
@personal_access_token = current_user.personal_access_tokens.find(params[:id])
if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
else
flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
end
redirect_to profile_personal_access_tokens_path
end
private
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at)
end
def load_personal_access_tokens
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
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
......
......@@ -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
......@@ -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
......
......@@ -204,10 +204,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?
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
......
class Projects::NotificationSettingsController < Projects::ApplicationController
before_action :authenticate_user!
def update
notification_setting = current_user.notification_settings_for(project)
saved = notification_setting.update_attributes(notification_setting_params)
render json: { saved: saved }
end
private
def notification_setting_params
params.require(:notification_setting).permit(:level)
end
end
......@@ -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
......@@ -73,26 +76,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
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])
......@@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
# MembershipActions concern
alias_method :membershipable, :project
def cannot_leave?
current_user == @project.owner
end
end
......@@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
sorted = VersionSorter.rsort(@repository.tag_names)
@tags = Kaminari.paginate_array(sorted).page(params[:page])
@sort = params[:sort] || 'name'
@tags = @repository.tags_sorted_by(@sort)
@tags = Kaminari.paginate_array(@tags).page(params[:page])
@releases = project.releases.where(tag: @tags)
end
......
class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create]
def create
todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
count: current_user.todos_pending_count,
delete_path: dashboard_todo_path(todo)
}
end
private
def issuable
@issuable ||= begin
case params[:issuable_type]
when "issue"
issue = @project.issues.find(params[:issuable_id])
if can?(current_user, :read_issue, issue)
issue
else
render_404
end
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
......
......@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
......@@ -143,6 +143,7 @@ class ProjectsController < Projects::ApplicationController
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
members: participants
}
......@@ -185,6 +186,48 @@ class ProjectsController < Projects::ApplicationController
)
end
def export
@project.add_export_job(current_user: current_user)
redirect_to(
edit_project_path(@project),
notice: "Project export started. A download link will be sent by email."
)
end
def download_export
export_project_path = @project.export_project_path
if export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
edit_project_path(@project),
alert: "Project export link has expired. Please generate a new export from your project settings."
)
end
end
def remove_export
if @project.remove_exports
flash[:notice] = "Project export has been deleted."
else
flash[:alert] = "Project export could not be deleted."
end
redirect_to(edit_project_path(@project))
end
def generate_new_export
if @project.remove_exports
export
else
redirect_to(
edit_project_path(@project),
alert: "Project export could not be deleted."
)
end
end
def toggle_star
current_user.toggle_star(@project)
@project.reload
......
......@@ -40,7 +40,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
......
......@@ -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
......@@ -123,7 +123,7 @@ class TodosFinder
end
def by_state(items)
case params[:state]
case params[:state].to_s
when 'done'
items.done
else
......
......@@ -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)
......@@ -180,8 +180,8 @@ module BlobHelper
licenses = Licensee::License.all
@licenses_for_select = {
Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
}
end
......
......@@ -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,25 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
class: 'btn btn-clipboard',
class: "btn btn-clipboard",
data: data,
type: :button
end
# Output a "Copy to Clipboard" button with a custom CSS class
#
# data - Data attributes passed to `content_tag`
# css_class - Class passed to the `content_tag`
#
# Examples:
#
# # Define the target element
# clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
# # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data,
type: :button
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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