Commit 44c12744 authored by Felipe Artur's avatar Felipe Artur

Merge 4009-external-users into issue_12658

parents ec20fdf3 59064aee
{
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
"color-shorthand": true,
"element-case": "lower",
"space-before-colon": "",
"space-after-colon": " ",
"space-before-combinator": " ",
"space-after-combinator": " ",
"space-between-declarations": "\n",
"space-before-opening-brace": " ",
"space-after-opening-brace": "\n",
"space-before-closing-brace": "\n",
"unitless-zero": true
}
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
.sass-cache/ .sass-cache/
.secret .secret
.vagrant .vagrant
.byebug_history
Vagrantfile Vagrantfile
backups/* backups/*
config/aws.yml config/aws.yml
......
...@@ -71,15 +71,6 @@ spec:services: ...@@ -71,15 +71,6 @@ spec:services:
- ruby - ruby
- mysql - mysql
spec:benchmark:
stage: test
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
tags:
- ruby
- mysql
allow_failure: true
spec:other: spec:other:
stage: test stage: test
script: script:
...@@ -131,6 +122,14 @@ rubocop: ...@@ -131,6 +122,14 @@ rubocop:
- ruby - ruby
- mysql - mysql
scss-lint:
stage: test
script:
- bundle exec rake scss_lint
tags:
- ruby
allow_failure: true
brakeman: brakeman:
stage: test stage: test
script: script:
...@@ -157,13 +156,14 @@ flay: ...@@ -157,13 +156,14 @@ flay:
bundler:audit: bundler:audit:
stage: test stage: test
only:
- master
script: script:
- "bundle exec bundle-audit update" - "bundle exec bundle-audit update"
- "bundle exec bundle-audit check" - "bundle exec bundle-audit check --ignore OSVDB-115941"
tags: tags:
- ruby - ruby
- mysql - mysql
allow_failure: true
# Ruby 2.2 jobs # Ruby 2.2 jobs
...@@ -243,22 +243,6 @@ spec:services:ruby22: ...@@ -243,22 +243,6 @@ spec:services:ruby22:
- ruby - ruby
- mysql - mysql
spec:benchmark:ruby22:
stage: test
image: ruby:2.2
only:
- master
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
cache:
key: "ruby22"
paths:
- vendor
tags:
- ruby
- mysql
allow_failure: true
spec:other:ruby22: spec:other:ruby22:
stage: test stage: test
image: ruby:2.2 image: ruby:2.2
......
# Linter Documentation:
# https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
scss_files: 'app/assets/stylesheets/**/*.scss'
exclude:
- 'app/assets/stylesheets/pages/emojis.scss'
linters:
BangFormat:
enabled: false
BorderZero:
enabled: false
ColorKeyword:
enabled: false
ColorVariable:
enabled: false
Comment:
enabled: false
DeclarationOrder:
enabled: false
# `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file.
# See https://github.com/brigade/scss-lint#disabling-linters-via-source for
# more information.
DisableLinterReason:
enabled: true
DuplicateProperty:
enabled: false
EmptyLineBetweenBlocks:
enabled: false
EmptyRule:
enabled: false
FinalNewline:
enabled: false
# HEX colors should use three-character values where possible.
HexLength:
enabled: true
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
IdSelector:
enabled: false
ImportPath:
enabled: false
ImportantRule:
enabled: false
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
LeadingZero:
enabled: false
MergeableSelector:
enabled: false
NameFormat:
enabled: false
NestingDepth:
enabled: false
PlaceholderInExtend:
enabled: false
PropertySortOrder:
enabled: false
PropertySpelling:
enabled: false
PseudoElement:
enabled: false
QualifyingElement:
enabled: false
SelectorDepth:
enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: true
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
SingleLinePerSelector:
enabled: false
SpaceAfterComma:
enabled: false
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
enabled: true
# Properties should be formatted with no space between the name and the
# colon.
SpaceAfterPropertyName:
enabled: true
SpaceAroundOperator:
enabled: false
SpaceBeforeBrace:
enabled: false
StringQuotes:
enabled: false
TrailingSemicolon:
enabled: false
TrailingWhitespace:
enabled: false
UnnecessaryMantissa:
enabled: false
UnnecessaryParentReference:
enabled: false
VendorPrefix:
enabled: false
# Omit length units on zero values, e.g. `0px` vs. `0`.
ZeroUnit:
enabled: true
This diff is collapsed.
...@@ -427,6 +427,7 @@ merge request: ...@@ -427,6 +427,7 @@ merge request:
1. [Rails](https://github.com/bbatsov/rails-style-guide) 1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) 1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab 1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security contributors to enhance security
1. [Database Migrations](doc/development/migration_style_guide.md) 1. [Database Migrations](doc/development/migration_style_guide.md)
...@@ -494,6 +495,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -494,6 +495,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ [`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
......
...@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1' ...@@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-google-oauth2', '~> 0.2.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.2' gem 'omniauth-saml', '~> 1.5.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
...@@ -77,9 +77,6 @@ gem "haml-rails", '~> 0.9.0' ...@@ -77,9 +77,6 @@ gem "haml-rails", '~> 0.9.0'
# Files attachments # Files attachments
gem "carrierwave", '~> 0.10.0' gem "carrierwave", '~> 0.10.0'
# Image editing
gem "mini_magick", '~> 4.4.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
...@@ -273,7 +270,7 @@ group :development, :test do ...@@ -273,7 +270,7 @@ group :development, :test do
# Generate Fake data # Generate Fake data
gem 'ffaker', '~> 2.0.0' gem 'ffaker', '~> 2.0.0'
gem 'capybara', '~> 2.4.0' gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
...@@ -286,6 +283,7 @@ group :development, :test do ...@@ -286,6 +283,7 @@ group :development, :test do
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.35.0', require: false gem 'rubocop', '~> 0.35.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'coveralls', '~> 0.8.2', require: false gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false gem 'simplecov', '~> 0.10.0', require: false
gem 'flog', require: false gem 'flog', require: false
......
...@@ -108,7 +108,8 @@ GEM ...@@ -108,7 +108,8 @@ GEM
thor (~> 0.18) thor (~> 0.18)
byebug (8.2.1) byebug (8.2.1)
cal-heatmap-rails (3.5.1) cal-heatmap-rails (3.5.1)
capybara (2.4.4) capybara (2.6.2)
addressable
mime-types (>= 1.16) mime-types (>= 1.16)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
...@@ -358,7 +359,7 @@ GEM ...@@ -358,7 +359,7 @@ GEM
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_emoji (0.3.1) gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1) gemojione (~> 2.2, >= 2.2.1)
gitlab_git (9.0.0) gitlab_git (9.0.3)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -468,7 +469,6 @@ GEM ...@@ -468,7 +469,6 @@ GEM
method_source (0.8.2) method_source (0.8.2)
mime-types (1.25.1) mime-types (1.25.1)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_magick (4.4.0)
mini_portile2 (2.0.0) mini_portile2 (2.0.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
...@@ -532,8 +532,8 @@ GEM ...@@ -532,8 +532,8 @@ GEM
omniauth-oauth2 (1.3.1) omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0) oauth2 (~> 1.0)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-saml (1.4.2) omniauth-saml (1.5.0)
omniauth (~> 1.1) omniauth (~> 1.3)
ruby-saml (~> 1.1, >= 1.1.1) ruby-saml (~> 1.1, >= 1.1.1)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
...@@ -692,7 +692,7 @@ GEM ...@@ -692,7 +692,7 @@ GEM
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-progressbar (1.7.5) ruby-progressbar (1.7.5)
ruby-saml (1.1.1) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.2.0) ruby2ruby (2.2.0)
...@@ -717,6 +717,9 @@ GEM ...@@ -717,6 +717,9 @@ GEM
sawyer (0.6.0) sawyer (0.6.0)
addressable (~> 2.3.5) addressable (~> 2.3.5)
faraday (~> 0.8, < 0.10) faraday (~> 0.8, < 0.10)
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
sdoc (0.3.20) sdoc (0.3.20)
json (>= 1.1.3) json (>= 1.1.3)
rdoc (~> 3.10) rdoc (~> 3.10)
...@@ -901,7 +904,7 @@ DEPENDENCIES ...@@ -901,7 +904,7 @@ DEPENDENCIES
bundler-audit bundler-audit
byebug byebug
cal-heatmap-rails (~> 3.5.0) cal-heatmap-rails (~> 3.5.0)
capybara (~> 2.4.0) capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
...@@ -956,7 +959,6 @@ DEPENDENCIES ...@@ -956,7 +959,6 @@ DEPENDENCIES
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.6.1) mail_room (~> 0.6.1)
method_source (~> 0.8) method_source (~> 0.8)
mini_magick (~> 4.4.0)
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16) mysql2 (~> 0.3.16)
...@@ -975,7 +977,7 @@ DEPENDENCIES ...@@ -975,7 +977,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0) omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0) omniauth-google-oauth2 (~> 0.2.0)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.4.2) omniauth-saml (~> 1.5.0)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
...@@ -1008,6 +1010,7 @@ DEPENDENCIES ...@@ -1008,6 +1010,7 @@ DEPENDENCIES
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.0) sass-rails (~> 5.0.0)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20) sdoc (~> 0.3.20)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
...@@ -1046,6 +1049,3 @@ DEPENDENCIES ...@@ -1046,6 +1049,3 @@ DEPENDENCIES
web-console (~> 2.0) web-console (~> 2.0)
webmock (~> 1.21.0) webmock (~> 1.21.0)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH
1.11.2
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json" namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json" group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json" projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) -> group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path) url = Api.buildUrl(Api.group_path)
...@@ -61,6 +62,19 @@ ...@@ -61,6 +62,19 @@
).done (projects) -> ).done (projects) ->
callback(projects) callback(projects)
newLabel: (project_id, data, callback) ->
url = Api.buildUrl(Api.labels_path)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
$.ajax(
url: url
type: "POST"
data: data
dataType: "json"
).done (label) ->
callback(label)
# Return group projects list. Filtered by query # Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) -> groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path) url = Api.buildUrl(Api.group_projects_path)
......
...@@ -42,7 +42,6 @@ ...@@ -42,7 +42,6 @@
#= require jquery.nicescroll #= require jquery.nicescroll
#= require_tree . #= require_tree .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper.js
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
...@@ -108,6 +107,8 @@ window.onload = -> ...@@ -108,6 +107,8 @@ window.onload = ->
setTimeout shiftWindow, 100 setTimeout shiftWindow, 100
$ -> $ ->
bootstrapBreakpoint = bp.getBreakpointSize()
$(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents # Click a .js-select-on-focus field, select the contents
...@@ -220,17 +221,17 @@ $ -> ...@@ -220,17 +221,17 @@ $ ->
.off 'breakpoint:change' .off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) -> .on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs' if breakpoint is 'sm' or breakpoint is 'xs'
$gutterIcon = $('aside .gutter-toggle').find('i') $gutterIcon = $('.js-sidebar-toggle').find('i')
if $gutterIcon.hasClass('fa-angle-double-right') if $gutterIcon.hasClass('fa-angle-double-right')
$gutterIcon.closest('a').trigger('click') $gutterIcon.closest('a').trigger('click')
$(document) $(document)
.off 'click', 'aside .gutter-toggle' .off 'click', '.js-sidebar-toggle'
.on 'click', 'aside .gutter-toggle', (e, triggered) -> .on 'click', '.js-sidebar-toggle', (e, triggered) ->
e.preventDefault() e.preventDefault()
$this = $(this) $this = $(this)
$thisIcon = $this.find 'i' $thisIcon = $this.find 'i'
$allGutterToggleIcons = $('.gutter-toggle i') $allGutterToggleIcons = $('.js-sidebar-toggle i')
if $thisIcon.hasClass('fa-angle-double-right') if $thisIcon.hasClass('fa-angle-double-right')
$allGutterToggleIcons $allGutterToggleIcons
.removeClass('fa-angle-double-right') .removeClass('fa-angle-double-right')
...@@ -256,35 +257,14 @@ $ -> ...@@ -256,35 +257,14 @@ $ ->
$('.right-sidebar') $('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' }) .hasClass('right-sidebar-collapsed'), { path: '/' })
bootstrapBreakpoint = undefined;
checkBootstrapBreakpoints = ->
if $('.device-xs').is(':visible')
bootstrapBreakpoint = "xs"
else if $('.device-sm').is(':visible')
bootstrapBreakpoint = "sm"
else if $('.device-md').is(':visible')
bootstrapBreakpoint = "md"
else if $('.device-lg').is(':visible')
bootstrapBreakpoint = "lg"
setBootstrapBreakpoints = ->
if $('.device-xs').length
return
$("body")
.append('<div class="device-xs visible-xs"></div>'+
'<div class="device-sm visible-sm"></div>'+
'<div class="device-md visible-md"></div>'+
'<div class="device-lg visible-lg"></div>')
checkBootstrapBreakpoints()
fitSidebarForSize = -> fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint oldBootstrapBreakpoint = bootstrapBreakpoint
checkBootstrapBreakpoints() bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint if bootstrapBreakpoint != oldBootstrapBreakpoint
$(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = -> checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm" if bootstrapBreakpoint is "xs" or "sm"
$(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
...@@ -293,6 +273,5 @@ $ -> ...@@ -293,6 +273,5 @@ $ ->
.on "resize", (e) -> .on "resize", (e) ->
fitSidebarForSize() fitSidebarForSize()
setBootstrapBreakpoints()
checkInitialSidebarSize() checkInitialSidebarSize()
new Aside() new Aside()
class @AwardsHandler class @AwardsHandler
constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
$(".add-award").click (event) => $(".js-add-award").on "click", (event) =>
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
...@@ -9,27 +9,46 @@ class @AwardsHandler ...@@ -9,27 +9,46 @@ class @AwardsHandler
$("html").on 'click', (event) -> $("html").on 'click', (event) ->
if !$(event.target).closest(".emoji-menu").length if !$(event.target).closest(".emoji-menu").length
if $(".emoji-menu").is(":visible") if $(".emoji-menu").is(":visible")
$(".emoji-menu").hide() $(".emoji-menu").removeClass "is-visible"
$(".awards")
.off "click"
.on "click", ".js-emoji-btn", @handleClick
@renderFrequentlyUsedBlock() @renderFrequentlyUsedBlock()
@setupSearch()
handleClick: (e) ->
e.preventDefault()
emoji = $(this)
.find(".icon")
.data "emoji"
awards_handler.addAward emoji
showEmojiMenu: -> showEmojiMenu: ->
if $(".emoji-menu").length if $(".emoji-menu").length
$(".emoji-menu").show() if $(".emoji-menu").is ".is-visible"
$(".emoji-menu").removeClass "is-visible"
$("#emoji_search").blur()
else
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus() $("#emoji_search").focus()
else else
$.get "/emojis", (response) -> $('.js-add-award').addClass "is-loading"
$(".add-award").after response $.get "/emojis", (response) =>
$(".emoji-menu").show() $('.js-add-award').removeClass "is-loading"
$(".js-award-holder").append response
setTimeout =>
$(".emoji-menu").addClass "is-visible"
$("#emoji_search").focus() $("#emoji_search").focus()
@setupSearch()
, 200
addAward: (emoji) -> addAward: (emoji) ->
emoji = @normilizeEmojiName(emoji) emoji = @normilizeEmojiName(emoji)
@postEmoji emoji, => @postEmoji emoji, =>
@addAwardToEmojiBar(emoji) @addAwardToEmojiBar(emoji)
$(".emoji-menu").hide() $(".emoji-menu").removeClass "is-visible"
addAwardToEmojiBar: (emoji) -> addAwardToEmojiBar: (emoji) ->
@addEmojiToFrequentlyUsedList(emoji) @addEmojiToFrequentlyUsedList(emoji)
...@@ -39,7 +58,7 @@ class @AwardsHandler ...@@ -39,7 +58,7 @@ class @AwardsHandler
if @isActive(emoji) if @isActive(emoji)
@decrementCounter(emoji) @decrementCounter(emoji)
else else
counter = @findEmojiIcon(emoji).siblings(".counter") counter = @findEmojiIcon(emoji).siblings(".js-counter")
counter.text(parseInt(counter.text()) + 1) counter.text(parseInt(counter.text()) + 1)
counter.parent().addClass("active") counter.parent().addClass("active")
@addMeToAuthorList(emoji) @addMeToAuthorList(emoji)
...@@ -53,7 +72,7 @@ class @AwardsHandler ...@@ -53,7 +72,7 @@ class @AwardsHandler
@findEmojiIcon(emoji).parent().hasClass("active") @findEmojiIcon(emoji).parent().hasClass("active")
decrementCounter: (emoji) -> decrementCounter: (emoji) ->
counter = @findEmojiIcon(emoji).siblings(".counter") counter = @findEmojiIcon(emoji).siblings(".js-counter")
emojiIcon = counter.parent() emojiIcon = counter.parent()
if parseInt(counter.text()) > 1 if parseInt(counter.text()) > 1
counter.text(parseInt(counter.text()) - 1) counter.text(parseInt(counter.text()) - 1)
...@@ -70,9 +89,13 @@ class @AwardsHandler ...@@ -70,9 +89,13 @@ class @AwardsHandler
removeMeFromAuthorList: (emoji) -> removeMeFromAuthorList: (emoji) ->
award_block = @findEmojiIcon(emoji).parent() award_block = @findEmojiIcon(emoji).parent()
authors = award_block.attr("data-original-title").split(", ") authors = award_block
.attr("data-original-title")
.split(", ")
authors.splice(authors.indexOf("me"),1) authors.splice(authors.indexOf("me"),1)
award_block.closest(".award").attr("data-original-title", authors.join(", ")) award_block
.closest(".js-emoji-btn")
.attr("data-original-title", authors.join(", "))
@resetTooltip(award_block) @resetTooltip(award_block)
addMeToAuthorList: (emoji) -> addMeToAuthorList: (emoji) ->
...@@ -98,14 +121,18 @@ class @AwardsHandler ...@@ -98,14 +121,18 @@ class @AwardsHandler
emojiCssClass = @resolveNameToCssClass(emoji) emojiCssClass = @resolveNameToCssClass(emoji)
nodes = [] nodes = []
nodes.push("<div class='award active' title='me'>") nodes.push(
nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>",
nodes.push("<div class='counter'>1</div>") "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
nodes.push("</div>") "<span class='award-control-text js-counter'>1</span>",
"</button>"
emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) )
$(".award").tooltip() emoji_node = $(nodes.join("\n"))
.insertBefore(".js-award-holder")
.find(".emoji-icon")
.data("emoji", emoji)
$('.award-control').tooltip()
resolveNameToCssClass: (emoji) -> resolveNameToCssClass: (emoji) ->
emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
...@@ -128,7 +155,7 @@ class @AwardsHandler ...@@ -128,7 +155,7 @@ class @AwardsHandler
callback.call() callback.call()
findEmojiIcon: (emoji) -> findEmojiIcon: (emoji) ->
$(".award [data-emoji='#{emoji}']") $(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
scrollToAwards: -> scrollToAwards: ->
$('body, html').animate({ $('body, html').animate({
...@@ -164,13 +191,13 @@ class @AwardsHandler ...@@ -164,13 +191,13 @@ class @AwardsHandler
term = $(ev.target).val() term = $(ev.target).val()
# Clean previous search results # Clean previous search results
$("ul.emoji-search,h5.emoji-search").remove() $("ul.emoji-menu-search, h5.emoji-search").remove()
if term if term
# Generate a search result block # Generate a search result block
h5 = $("<h5>").text("Search results").addClass("emoji-search") h5 = $("<h5>").text("Search results").addClass("emoji-search")
found_emojis = @searchEmojis(term).show() found_emojis = @searchEmojis(term).show()
ul = $("<ul>").addClass("emoji-search").append(found_emojis) ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis)
$(".emoji-menu-content ul, .emoji-menu-content h5").hide() $(".emoji-menu-content ul, .emoji-menu-content h5").hide()
$(".emoji-menu-content").append(h5).append(ul) $(".emoji-menu-content").append(h5).append(ul)
else else
......
class @Breakpoints
instance = null;
class BreakpointInstance
BREAKPOINTS = ["xs", "sm", "md", "lg"]
constructor: ->
@setup()
setup: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}"
return if $(allDeviceSelector.join(",")).length
# Create all the elements
els = $.map BREAKPOINTS, (breakpoint) ->
"<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
$("body").append els.join('')
visibleDevice: ->
allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
".device-#{breakpoint}"
$(allDeviceSelector.join(",")).filter(":visible")
getBreakpointSize: ->
$visibleDevice = @visibleDevice
# the page refreshed via turbolinks
if not $visibleDevice().length
@setup()
$visibleDevice = @visibleDevice()
return $visibleDevice.attr("class").split("visible-")[1]
@get: ->
return instance ?= new BreakpointInstance
$ =>
@bp = Breakpoints.get()
...@@ -4,6 +4,8 @@ class CiBuild ...@@ -4,6 +4,8 @@ class CiBuild
constructor: (build_url, build_status) -> constructor: (build_url, build_status) ->
clearInterval(CiBuild.interval) clearInterval(CiBuild.interval)
@initScrollButtonAffix()
if build_status == "running" || build_status == "pending" if build_status == "running" || build_status == "pending"
# #
# Bind autoscroll button to follow build output # Bind autoscroll button to follow build output
...@@ -38,4 +40,15 @@ class CiBuild ...@@ -38,4 +40,15 @@ class CiBuild
checkAutoscroll: -> checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
initScrollButtonAffix: ->
$buildScroll = $('#js-build-scroll')
$body = $('body')
$buildTrace = $('#build-trace')
$buildScroll.affix(
offset:
bottom: ->
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
)
@CiBuild = CiBuild @CiBuild = CiBuild
...@@ -74,8 +74,9 @@ class Dispatcher ...@@ -74,8 +74,9 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
new TreeView() if $('#tree-slider').length new TreeView() if $('#tree-slider').length
when 'groups:show' when 'groups:activity'
new Activities() new Activities()
when 'groups:show'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'groups:group_members:index' when 'groups:group_members:index'
new GroupMembers() new GroupMembers()
...@@ -103,6 +104,8 @@ class Dispatcher ...@@ -103,6 +104,8 @@ class Dispatcher
new ProjectFork() new ProjectFork()
when 'projects:artifacts:browse' when 'projects:artifacts:browse'
new BuildArtifacts() new BuildArtifacts()
when 'projects:group_links:index'
new GroupsSelect()
switch path.first() switch path.first()
when 'admin' when 'admin'
......
class GitLabDropdownFilter
BLUR_KEYCODES = [27, 40]
constructor: (@dropdown, @options) ->
@input = @dropdown.find(".dropdown-input .dropdown-input-field")
# Key events
timeout = ""
@input.on "keyup", (e) =>
if e.keyCode is 13 && @input.val() isnt ""
if @options.enterCallback
@options.enterCallback()
return
clearTimeout timeout
timeout = setTimeout =>
blur_field = @shouldBlur e.keyCode
search_text = @input.val()
if blur_field
@input.blur()
if @options.remote
@options.query search_text, (data) =>
@options.callback(data)
else
@filter search_text
, 250
shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0
filter: (search_text) ->
data = @options.data()
results = data
if search_text isnt ""
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
@options.callback results
class GitLabDropdownRemote
constructor: (@dataEndpoint, @options) ->
execute: ->
if typeof @dataEndpoint is "string"
@fetchData()
else if typeof @dataEndpoint is "function"
if @options.beforeSend
@options.beforeSend()
# Fetch the data by calling the data funcfion
@dataEndpoint "", (data) =>
if @options.success
@options.success(data)
if @options.beforeSend
@options.beforeSend()
# Fetch the data through ajax if the data is a string
fetchData: ->
$.ajax(
url: @dataEndpoint,
dataType: @options.dataType,
beforeSend: =>
if @options.beforeSend
@options.beforeSend()
success: (data) =>
if @options.success
@options.success(data)
)
class GitLabDropdown
LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active"
constructor: (@el, @options) ->
self = @
@dropdown = $(@el).parent()
search_fields = if @options.search then @options.search.fields else [];
if @options.data
# Remote data
@remote = new GitLabDropdownRemote @options.data, {
dataType: @options.dataType,
beforeSend: @toggleLoading.bind(@)
success: (data) =>
@fullData = data
@parseData @fullData
}
# Init filiterable
if @options.filterable
@filter = new GitLabDropdownFilter @dropdown,
remote: @options.filterRemote
query: @options.data
keys: @options.search.fields
data: =>
return @fullData
callback: (data) =>
@parseData data
enterCallback: =>
@selectFirstRow()
# Event listeners
@dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden
if @dropdown.find(".dropdown-toggle-page").length
@dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
e.preventDefault()
e.stopPropagation()
@togglePage()
if @options.selectable
selector = ".dropdown-content a"
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) ->
self.rowClicked $(@)
if self.options.clicked
self.options.clicked()
toggleLoading: ->
$('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
togglePage: ->
menu = $('.dropdown-menu', @dropdown)
if menu.hasClass(PAGE_TWO_CLASS)
if @remote
@remote.execute()
menu.toggleClass PAGE_TWO_CLASS
parseData: (data) ->
@renderedData = data
# Render each row
html = $.map data, (obj) =>
return @renderItem(obj)
if @options.filterable and data.length is 0
# render no matching results
html = [@noResults()]
# Render the full menu
full_html = @renderMenu(html.join(""))
@appendMenu(full_html)
opened: =>
contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is ""
@remote.execute()
if @options.filterable
@dropdown.find(".dropdown-input-field").focus()
hidden: =>
if @options.filterable
@dropdown.find(".dropdown-input-field").blur().val("")
if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
# Render the full menu
renderMenu: (html) ->
menu_html = ""
if @options.renderMenu
menu_html = @options.renderMenu(html)
else
menu_html = "<ul>#{html}</ul>"
return menu_html
# Append the menu into the dropdown
appendMenu: (html) ->
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
$(selector, @dropdown).html html
# Render the row
renderItem: (data) ->
html = ""
return "<li class='divider'></li>" if data is "divider"
if @options.renderRow
# Call the render function
html = @options.renderRow(data)
else
selected = if @options.isSelected then @options.isSelected(data) else false
url = if @options.url then @options.url(data) else "#"
text = if @options.text then @options.text(data) else ""
cssClass = "";
if selected
cssClass = "is-active"
html = "<li>"
html += "<a href='#{url}' class='#{cssClass}'>"
html += text
html += "</a>"
html += "</li>"
return html
noResults: ->
html = "<li>"
html += "<a href='#' class='is-focused'>"
html += "No matching results."
html += "</a>"
html += "</li>"
rowClicked: (el) ->
fieldName = @options.fieldName
field = @dropdown.parent().find("input[name='#{fieldName}']")
if el.hasClass(ACTIVE_CLASS)
field.remove()
else
fieldName = @options.fieldName
selectedIndex = el.parent().index()
if @renderedData
selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
if !value?
field.remove()
if @options.multiSelect
oldValue = field.val()
if oldValue
value = "#{oldValue},#{value}"
else
@dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
# Toggle active class for the tick mark
el.toggleClass "is-active"
if value?
if !field.length
# Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' />"
@dropdown.before input
@dropdown.parent().find("input[name='#{fieldName}']").val value
selectFirstRow: ->
selector = '.dropdown-content li:first-child a'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content li:first-child a"
# similute a click on the first link
$(selector).trigger "click"
$.fn.glDropdown = (opts) ->
return @.each ->
new GitLabDropdown @, opts
class @IssueStatusSelect
constructor: ->
$('.js-issue-status').each (i, el) ->
fieldName = $(el).data("field-name")
$(el).glDropdown(
selectable: true
fieldName: fieldName
id: (obj, el) ->
$(el).data("id")
)
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
labelUrl = $(dropdown).data("labels")
selectedLabel = $(dropdown).data('selected')
if selectedLabel
selectedLabel = selectedLabel.split(",")
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
if newLabelField.length
$('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
newColorField.val $(this).data("color")
$('.js-dropdown-label-color-preview')
.css 'background-color', $(this).data("color")
.addClass 'is-active'
$('.js-new-label-btn').on "click", (e) ->
e.preventDefault()
e.stopPropagation()
if newLabelField.val() isnt "" && newColorField.val() isnt ""
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
$('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
$(dropdown).glDropdown(
data: (term, callback) ->
# We have to fetch the JS version of the labels list because there is no
# public facing JSON url for labels
$.ajax(
url: labelUrl
).done (data) ->
html = $(data)
data = []
html.find('.label-row a').each ->
data.push(
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No label'
)
if showAny
data.unshift(
title: 'Any label'
)
if data.length > 2
data.splice 2, 0, "divider"
callback data
renderRow: (label) ->
if $.isArray(selectedLabel)
selected = ""
$.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim()
if selected is "" && label.title is selectedLbl
selected = "is-active"
else
selected = if label.title is selectedLabel then "is-active" else ""
"<li>
<a href='#' class='#{selected}'>
#{label.title}
</a>
</li>"
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
id: (label) ->
label.title
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
...@@ -189,7 +189,7 @@ class @MergeRequestTabs ...@@ -189,7 +189,7 @@ class @MergeRequestTabs
$('.container-fluid').removeClass('container-limited') $('.container-fluid').removeClass('container-limited')
shrinkView: -> shrinkView: ->
$gutterIcon = $('.gutter-toggle i') $gutterIcon = $('.js-sidebar-toggle i')
# Wait until listeners are set # Wait until listeners are set
setTimeout( -> setTimeout( ->
...@@ -197,4 +197,3 @@ class @MergeRequestTabs ...@@ -197,4 +197,3 @@ class @MergeRequestTabs
if $gutterIcon.is('.fa-angle-double-right') if $gutterIcon.is('.fa-angle-double-right')
$gutterIcon.closest('a').trigger('click',[true]) $gutterIcon.closest('a').trigger('click',[true])
, 0) , 0)
class @MilestoneSelect
constructor: ->
$('.js-milestone-select').each (i, dropdown) ->
projectId = $(dropdown).data('project-id')
milestonesUrl = $(dropdown).data('milestones')
selectedMilestone = $(dropdown).data('selected')
showNo = $(dropdown).data('show-no')
showAny = $(dropdown).data('show-any')
useId = $(dropdown).data('use-id')
$(dropdown).glDropdown(
data: (term, callback) ->
$.ajax(
url: milestonesUrl
).done (data) ->
html = $(data)
data = []
html.find('.milestone strong a').each ->
link = $(@).attr("href").split("/")
data.push(
id: link[link.length - 1]
title: $(@).text().trim()
)
if showNo
data.unshift(
id: "0"
title: 'No Milestone'
)
if showAny
data.unshift(
title: 'Any Milestone'
)
if data.length > 2
data.splice 2, 0, "divider"
callback(data)
filterable: true
search:
fields: ['title']
selectable: true
fieldName: $(dropdown).data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
if milestone.title isnt "Any milestone"
milestone.title
else
""
else
milestone.id
isSelected: (milestone) ->
milestone.title is selectedMilestone
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
)
...@@ -30,8 +30,11 @@ class @Notes ...@@ -30,8 +30,11 @@ class @Notes
$(document).on "ajax:success", ".js-main-target-form", @addNote $(document).on "ajax:success", ".js-main-target-form", @addNote
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# catch note ajax errors
$(document).on "ajax:error", ".js-main-target-form", @addNoteError
# change note in UI after update # change note in UI after update
$(document).on "ajax:success", "form.edit_note", @updateNote $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link # Edit note link
$(document).on "click", ".js-note-edit", @showEditForm $(document).on "click", ".js-note-edit", @showEditForm
...@@ -51,6 +54,9 @@ class @Notes ...@@ -51,6 +54,9 @@ class @Notes
$(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
$(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
# reset main target form when clicking discard
$(document).on "click", ".js-note-discard", @resetMainTargetForm
# update the file name when an attachment is selected # update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment $(document).on "change", ".js-note-attachment-input", @updateFormAttachment
...@@ -72,7 +78,7 @@ class @Notes ...@@ -72,7 +78,7 @@ class @Notes
cleanBinding: -> cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form" $(document).off "ajax:success", ".js-discussion-note-form"
$(document).off "ajax:success", "form.edit_note" $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit" $(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel" $(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-delete"
...@@ -85,6 +91,7 @@ class @Notes ...@@ -85,6 +91,7 @@ class @Notes
$(document).off "keyup", ".js-note-text" $(document).off "keyup", ".js-note-text"
$(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close" $(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard"
$('.note .js-task-list-container').taskList('disable') $('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container' $(document).off 'tasklist:changed', '.note .js-task-list-container'
...@@ -219,7 +226,7 @@ class @Notes ...@@ -219,7 +226,7 @@ class @Notes
Resets text and preview. Resets text and preview.
Resets buttons. Resets buttons.
### ###
resetMainTargetForm: -> resetMainTargetForm: (e) =>
form = $(".js-main-target-form") form = $(".js-main-target-form")
# remove validation errors # remove validation errors
...@@ -231,6 +238,8 @@ class @Notes ...@@ -231,6 +238,8 @@ class @Notes
form.find(".js-note-text").data("autosave").reset() form.find(".js-note-text").data("autosave").reset()
@updateTargetButtons(e)
reenableTargetFormSubmitButton: -> reenableTargetFormSubmitButton: ->
form = $(".js-main-target-form") form = $(".js-main-target-form")
...@@ -274,8 +283,10 @@ class @Notes ...@@ -274,8 +283,10 @@ class @Notes
form.removeClass "js-new-note-form" form.removeClass "js-new-note-form"
form.find('.div-dropzone').remove() form.find('.div-dropzone').remove()
# hide discard button
form.find('.js-note-discard').hide()
# setup preview buttons # setup preview buttons
form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-md-preview-button") previewButton = form.find(".js-md-preview-button")
textarea = form.find(".js-note-text") textarea = form.find(".js-note-text")
...@@ -309,6 +320,10 @@ class @Notes ...@@ -309,6 +320,10 @@ class @Notes
addNote: (xhr, note, status) => addNote: (xhr, note, status) =>
@renderNote(note) @renderNote(note)
addNoteError: (xhr, note, status) =>
flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
flash.pinTo('.md-area')
### ###
Called in response to the new note form being submitted Called in response to the new note form being submitted
...@@ -347,21 +362,25 @@ class @Notes ...@@ -347,21 +362,25 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").hide() note.find(".note-body > .note-text").hide()
note.find(".note-header").hide() note.find(".note-header").hide()
base_form = note.find(".note-edit-form") form = note.find(".note-edit-form")
form = base_form.clone().insertAfter(base_form) isNewForm = form.is(':not(.gfm-form)')
form.addClass('current-note-edit-form gfm-form') if isNewForm
form.find('.div-dropzone').remove() form.addClass('gfm-form')
form.addClass('current-note-edit-form')
form.show()
# Show the attachment delete link # Show the attachment delete link
note.find(".js-note-attachment-delete").show() note.find(".js-note-attachment-delete").show()
# Setup markdown form # Setup markdown form
if isNewForm
GitLab.GfmAutoComplete.setup() GitLab.GfmAutoComplete.setup()
new DropzoneInput(form) new DropzoneInput(form)
form.show()
textarea = form.find("textarea") textarea = form.find("textarea")
textarea.focus() textarea.focus()
if isNewForm
autosize(textarea) autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
...@@ -371,6 +390,7 @@ class @Notes ...@@ -371,6 +390,7 @@ class @Notes
textarea.val "" textarea.val ""
textarea.val value textarea.val value
if isNewForm
disableButtonIfEmptyField textarea, form.find(".js-comment-button") disableButtonIfEmptyField textarea, form.find(".js-comment-button")
### ###
...@@ -383,7 +403,9 @@ class @Notes ...@@ -383,7 +403,9 @@ class @Notes
note = $(this).closest(".note") note = $(this).closest(".note")
note.find(".note-body > .note-text").show() note.find(".note-body > .note-text").show()
note.find(".note-header").show() note.find(".note-header").show()
note.find(".current-note-edit-form").remove() note.find(".current-note-edit-form")
.removeClass("current-note-edit-form")
.hide()
### ###
Called in response to deleting a note of any kind. Called in response to deleting a note of any kind.
...@@ -462,6 +484,11 @@ class @Notes ...@@ -462,6 +484,11 @@ class @Notes
form.find("#note_line_code").val dataHolder.data("lineCode") form.find("#note_line_code").val dataHolder.data("lineCode")
form.find("#note_noteable_type").val dataHolder.data("noteableType") form.find("#note_noteable_type").val dataHolder.data("noteableType")
form.find("#note_noteable_id").val dataHolder.data("noteableId") form.find("#note_noteable_id").val dataHolder.data("noteableId")
form.find('.js-note-discard')
.show()
.removeClass('js-note-discard')
.addClass('js-close-discussion-note-form')
.text(form.find('.js-close-discussion-note-form').data('cancel-text'))
@setupNoteForm form @setupNoteForm form
form.find(".js-note-text").focus() form.find(".js-note-text").focus()
form.addClass "js-discussion-note-form" form.addClass "js-discussion-note-form"
...@@ -561,21 +588,52 @@ class @Notes ...@@ -561,21 +588,52 @@ class @Notes
updateCloseButton: (e) => updateCloseButton: (e) =>
textarea = $(e.target) textarea = $(e.target)
form = textarea.parents('form') form = textarea.parents('form')
form.find('.js-note-target-close').text('Close') closebtn = form.find('.js-note-target-close')
closebtn.text(closebtn.data('original-text'))
updateTargetButtons: (e) => updateTargetButtons: (e) =>
textarea = $(e.target) textarea = $(e.target)
form = textarea.parents('form') form = textarea.parents('form')
reopenbtn = form.find('.js-note-target-reopen')
closebtn = form.find('.js-note-target-close')
discardbtn = form.find('.js-note-discard')
if textarea.val().trim().length > 0 if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen') reopentext = reopenbtn.data('alternative-text')
form.find('.js-note-target-close').text('Comment & close') closetext = closebtn.data('alternative-text')
form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
form.find('.js-note-target-close').addClass('btn-comment-and-close') if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.addClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.addClass('btn-comment-and-close')
if discardbtn.is(':hidden')
discardbtn.show()
else else
form.find('.js-note-target-reopen').text('Reopen') reopentext = reopenbtn.data('original-text')
form.find('.js-note-target-close').text('Close') closetext = closebtn.data('original-text')
form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
form.find('.js-note-target-close').removeClass('btn-comment-and-close') if reopenbtn.text() isnt reopentext
reopenbtn.text(reopentext)
if closebtn.text() isnt closetext
closebtn.text(closetext)
if reopenbtn.is(':not(.btn-comment-and-reopen)')
reopenbtn.removeClass('btn-comment-and-reopen')
if closebtn.is(':not(.btn-comment-and-close)')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
discardbtn.hide()
initTaskList: -> initTaskList: ->
@enableTaskList() @enableTaskList()
......
...@@ -4,64 +4,27 @@ class @Profile ...@@ -4,64 +4,27 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit() $(this).parents('form').submit()
$('.update-username form').on 'ajax:before', -> $('.update-username').on 'ajax:before', ->
$('.loading-gif').show() $('.loading-username').show()
$(this).find('.update-success').hide() $(this).find('.update-success').hide()
$(this).find('.update-failed').hide() $(this).find('.update-failed').hide()
$('.update-username form').on 'ajax:complete', -> $('.update-username').on 'ajax:complete', ->
$('.loading-username').hide()
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
$(this).find('.loading-gif').hide() $(this).find('.loading-gif').hide()
$('.update-notifications').on 'ajax:complete', -> $('.update-notifications').on 'ajax:complete', ->
$(this).find('.btn-save').enable() $(this).find('.btn-save').enable()
# Avatar management $('.js-choose-user-avatar-button').bind "click", ->
form = $(this).closest("form")
$avatarInput = $('.js-user-avatar-input') form.find(".js-user-avatar-input").click()
$filename = $('.js-avatar-filename')
$modalCrop = $('.modal-profile-crop')
$modalCropImg = $('.modal-profile-crop-image')
$('.js-choose-user-avatar-button').on "click", ->
$form = $(this).closest("form")
$form.find(".js-user-avatar-input").click()
$modalCrop.on 'shown.bs.modal', ->
setTimeout ( -> # The cropper must be asynchronously initialized
$modalCropImg.cropper
aspectRatio: 1
modal: false
scalable: false
rotatable: false
zoomable: false
crop: (event) ->
['x', 'y'].forEach (key) ->
$("#user_avatar_crop_#{key}").val(Math.floor(event[key]))
$("#user_avatar_crop_size").val(Math.floor(event.width))
), 0
$modalCrop.on 'hidden.bs.modal', ->
$modalCropImg.attr('src', '').cropper('destroy')
$avatarInput.val('')
$filename.text($filename.data('label'))
$('.js-upload-user-avatar').on 'click', ->
$('.edit-user').submit()
$avatarInput.on "change", -> $('.js-user-avatar-input').bind "change", ->
form = $(this).closest("form") form = $(this).closest("form")
filename = $(this).val().replace(/^.*[\\\/]/, '') filename = $(this).val().replace(/^.*[\\\/]/, '')
$filename.data('label', $filename.text()).text(filename) form.find(".js-avatar-filename").text(filename)
reader = new FileReader
reader.onload = (event) ->
$modalCrop.modal('show')
$modalCropImg.attr('src', event.target.result)
fileData = reader.readAsDataURL(this.files[0])
$ -> $ ->
# Extract the SSH Key title from its comment # Extract the SSH Key title from its comment
......
$(document).on("click", '.toggle-nav-collapse', (e) -> collapsed = 'page-sidebar-collapsed'
e.preventDefault() expanded = 'page-sidebar-expanded'
collapsed = 'page-sidebar-collapsed'
expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded") $('header').toggleClass("header-collapsed header-expanded")
$('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
...@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> ...@@ -14,4 +13,15 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
niceScrollBars.updateScrollBar(); niceScrollBars.updateScrollBar();
), 300 ), 300
$(document).on("click", '.toggle-nav-collapse', (e) ->
e.preventDefault()
toggleSidebar()
) )
$ ->
size = bp.getBreakpointSize()
if size is "xs" or size is "sm"
if $('.page-with-sidebar').hasClass(expanded)
toggleSidebar()
class @Subscription class @Subscription
constructor: (url) -> constructor: (container) ->
$(".subscribe-button").unbind("click").click (event)=> $container = $(container)
btn = $(event.currentTarget) @url = $container.attr('data-url')
action = btn.find("span").text() @subscribe_button = $container.find('.subscribe-button')
current_status = $(".subscription-status").attr("data-status") @subscription_status = $container.find('.subscription-status')
btn.prop("disabled", true) @subscribe_button.unbind('click').click(@toggleSubscription)
$.post url, =>
btn.prop("disabled", false)
status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
$(".subscription-status").attr("data-status", status)
action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
btn.find("span").text(action)
$(".subscription-status>div").toggleClass("hidden")
toggleSubscription: (event) =>
btn = $(event.currentTarget)
action = btn.find('span').text()
current_status = @subscription_status.attr('data-status')
btn.prop('disabled', true)
$.post @url, =>
btn.prop('disabled', false)
status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
@subscription_status.attr('data-status', status)
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
btn.find('span').text(action)
@subscription_status.find('>div').toggleClass('hidden')
...@@ -3,6 +3,81 @@ class @UsersSelect ...@@ -3,6 +3,81 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json" @usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json" @userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) =>
@projectId = $(dropdown).data('project-id')
@showCurrentUser = $(dropdown).data('current-user')
showNullUser = $(dropdown).data('null-user')
showAnyUser = $(dropdown).data('any-user')
firstUser = $(dropdown).data('first-user')
selectedId = $(dropdown).data('selected')
$(dropdown).glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
showDivider = 0
if firstUser
# Move current user to the front of the list
for obj, index in users
if obj.username == firstUser
users.splice(index, 1)
users.unshift(obj)
break
if showNullUser
showDivider += 1
users.unshift(
name: 'Unassigned',
id: 0
)
if showAnyUser
showDivider += 1
name = showAnyUser
name = 'Any User' if name == true
anyUser = {
name: name,
id: null
}
users.unshift(anyUser)
if showDivider
users.splice(showDivider, 0, "divider")
# Send the data back
callback users
filterable: true
filterRemote: true
search:
fields: ['name', 'username']
selectable: true
fieldName: $(dropdown).data('field-name')
clicked: ->
if $(dropdown).hasClass "js-filter-submit"
$(dropdown).parents('form').submit()
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
selected = if user.id is selectedId then "is-active" else ""
img = ""
if avatar
img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
"<li>
<a href='#' class='dropdown-menu-user-link #{selected}'>
#{img}
<strong class='dropdown-menu-user-full-name'>
#{user.name}
</strong>
<span class='dropdown-menu-user-username'>
#{username}
</span>
</a>
</li>"
)
$('.ajax-users-select').each (i, select) => $('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id') @projectId = $(select).data('project-id')
@groupId = $(select).data('group-id') @groupId = $(select).data('group-id')
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
*= require_self *= require_self
*= require dropzone/basic *= require dropzone/basic
*= require cal-heatmap *= require cal-heatmap
*= require cropper.css
*/ */
/* /*
......
...@@ -28,6 +28,10 @@ ...@@ -28,6 +28,10 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
color: $gl-gray; color: $gl-gray;
a {
color: $md-link-color;
}
&.oneline-block { &.oneline-block {
line-height: 42px; line-height: 42px;
} }
...@@ -116,6 +120,10 @@ ...@@ -116,6 +120,10 @@
.cover-desc { .cover-desc {
padding: 0 $gl-padding 3px; padding: 0 $gl-padding 3px;
color: $gl-text-color; color: $gl-text-color;
&.username:last-child {
padding-bottom: $gl-padding;
}
} }
.cover-controls { .cover-controls {
...@@ -153,3 +161,7 @@ ...@@ -153,3 +161,7 @@
float: right; float: right;
} }
} }
.content-block-small {
padding: 10px 0;
}
...@@ -12,11 +12,13 @@ ...@@ -12,11 +12,13 @@
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px } .prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px } .prepend-left-10 { margin-left:10px }
.prepend-left-default { margin-left:$gl-padding } .prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left:20px } .prepend-left-20 { margin-left:20px }
.append-right-5 { margin-right: 5px } .append-right-5 { margin-right: 5px }
.append-right-10 { margin-right:10px } .append-right-10 { margin-right:10px }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right:20px } .append-right-20 { margin-right:20px }
.append-bottom-0 { margin-bottom:0 }
.append-bottom-10 { margin-bottom:10px } .append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px } .append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px } .append-bottom-20 { margin-bottom:20px }
......
...@@ -17,6 +17,47 @@ ...@@ -17,6 +17,47 @@
.dropdown-menu { .dropdown-menu {
display: block; display: block;
} }
.dropdown-menu-toggle {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
}
.dropdown-menu-toggle {
position: relative;
width: 160px;
padding: 6px 20px 6px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
text-align: left;
border: 1px solid $dropdown-toggle-border-color;
border-radius: 2px;
outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.fa {
position: absolute;
top: 50%;
right: 6px;
margin-top: -4px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
}
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
.fa {
color: $dropdown-toggle-hover-icon-color;
}
}
} }
.dropdown-menu { .dropdown-menu {
...@@ -24,7 +65,7 @@ ...@@ -24,7 +65,7 @@
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
z-index: 9999; z-index: 9;
width: 240px; width: 240px;
margin-top: 2px; margin-top: 2px;
margin-bottom: 0; margin-bottom: 0;
...@@ -36,6 +77,21 @@ ...@@ -36,6 +77,21 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.is-loading {
.dropdown-content {
display: none;
}
.dropdown-loading {
display: block;
}
}
ul {
margin: 0;
padding: 0;
}
li { li {
text-align: left; text-align: left;
list-style: none; list-style: none;
...@@ -61,13 +117,70 @@ ...@@ -61,13 +117,70 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
&:hover { &:hover,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg; background-color: $dropdown-link-hover-bg;
text-decoration: none; text-decoration: none;
outline: 0;
} }
} }
} }
.dropdown-menu-paging {
.dropdown-page-two,
.dropdown-menu-back {
display: none;
}
&.is-page-two {
.dropdown-page-one {
display: none;
}
.dropdown-page-two,
.dropdown-menu-back {
display: block;
}
}
}
.dropdown-menu-user {
.avatar {
float: left;
width: 30px;
height: 30px;
margin: 0 10px 0 0;
}
}
.dropdown-menu-user-link {
padding-top: 7px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
margin-bottom: 2px;
font-weight: 600;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-menu-user-username {
display: block;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.dropdown-select {
width: 280px;
}
.dropdown-menu-align-right { .dropdown-menu-align-right {
left: auto; left: auto;
right: 0; right: 0;
...@@ -101,3 +214,130 @@ ...@@ -101,3 +214,130 @@
font-size: 13px; font-size: 13px;
line-height: 22px; line-height: 22px;
} }
.dropdown-title {
position: relative;
margin-bottom: 10px;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 10px;
font-weight: 600;
line-height: 1;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-bottom: 1px solid $dropdown-divider-color;
overflow: hidden;
}
.dropdown-title-button {
position: absolute;
top: -1px;
padding: 0;
color: $dropdown-title-btn-color;
font-size: 14px;
border: 0;
background: none;
outline: 0;
&:hover {
color: darken($dropdown-title-btn-color, 15%);
}
}
.dropdown-menu-close {
right: 0;
}
.dropdown-menu-back {
left: 0;
}
.dropdown-input {
position: relative;
margin-bottom: 10px;
.fa {
position: absolute;
top: 10px;
right: 10px;
color: #C7C7C7;
font-size: 12px;
pointer-events: none;
}
}
.dropdown-input-field {
width: 100%;
padding: 0 7px;
color: $dropdown-input-color;
line-height: 30px;
border: 1px solid $dropdown-divider-color;
border-radius: 2px;
outline: 0;
&:focus {
color: $dropdown-link-color;
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+ .fa {
color: $dropdown-link-color;
}
}
&:hover {
+ .fa {
color: $dropdown-link-color;
}
}
}
.dropdown-content {
max-height: 215px;
overflow-y: scroll;
}
.dropdown-footer {
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
border-top: 1px solid $dropdown-divider-color;
}
.dropdown-footer-list {
font-size: 14px;
a {
padding-left: 10px;
}
}
.dropdown-loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
z-index: 9;
background-color: $dropdown-loading-bg;
font-size: 28px;
.fa {
position: absolute;
top: 50%;
left: 50%;
margin-top: -14px;
margin-left: -14px;
}
}
.dropdown-menu-labels {
.label {
position: relative;
width: 30px;
margin-right: 5px;
text-indent: -99999px;
}
}
...@@ -169,6 +169,7 @@ ...@@ -169,6 +169,7 @@
*/ */
&.code { &.code {
padding: 0; padding: 0;
-webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
} }
} }
} }
......
.filter-item { .filter-item {
margin-right: 6px; margin-right: 6px;
vertical-align: top;
} }
@media (min-width: 800px) { @media (min-width: 800px) {
......
...@@ -141,22 +141,18 @@ header { ...@@ -141,22 +141,18 @@ header {
margin-left: $sidebar_collapsed_width; margin-left: $sidebar_collapsed_width;
} }
@media (max-width: $screen-md-max) { .header-collapsed {
.header-collapsed {
margin-left: $sidebar_collapsed_width; margin-left: $sidebar_collapsed_width;
}
.header-expanded { @media (min-width: $screen-md-min) {
margin-left: $sidebar_width; @include collapsed-header;
} }
} }
@media(min-width: $screen-md-max) { .header-expanded {
.header-collapsed { margin-left: $sidebar_collapsed_width;
@include collapsed-header;
}
.header-expanded { @media (min-width: $screen-md-min) {
margin-left: $sidebar_width; margin-left: $sidebar_width;
} }
} }
......
...@@ -41,12 +41,6 @@ ...@@ -41,12 +41,6 @@
transition: $transition; transition: $transition;
} }
@mixin transform($transform) {
-webkit-transform: $transform;
-ms-transform: $transform;
transform: $transform;
}
/** /**
* Prefilled mixins * Prefilled mixins
* Mixins with fixed values * Mixins with fixed values
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
border-bottom: none; border-bottom: none;
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-max) {
width: 100%; width: 100%;
} }
} }
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
} }
.sidebar-wrapper { .sidebar-wrapper {
z-index: 99; z-index: 999;
background: $background-color; background: $background-color;
} }
...@@ -203,7 +203,11 @@ ...@@ -203,7 +203,11 @@
} }
@mixin expanded-sidebar { @mixin expanded-sidebar {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width; padding-left: $sidebar_width;
}
&.right-sidebar-collapsed { &.right-sidebar-collapsed {
/* Extra small devices (phones, less than 768px) */ /* Extra small devices (phones, less than 768px) */
......
...@@ -149,13 +149,13 @@ ...@@ -149,13 +149,13 @@
} }
&:hover > a.anchor { &:hover > a.anchor {
$size: 16px; $size: 14px;
position: absolute; position: absolute;
right: 100%; right: 100%;
top: 50%; top: 50%;
margin-top: -$size/2; margin-top: -11px;
margin-right: 0px; margin-right: 0;
padding-right: 20px; padding-right: 15px;
display: inline-block; display: inline-block;
width: $size; width: $size;
height: $size; height: $size;
......
...@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958; ...@@ -34,13 +34,15 @@ $error-exclamation-point: #E62958;
$border-radius-default: 3px; $border-radius-default: 3px;
$list-title-color: #333333; $list-title-color: #333333;
$list-text-color: #555555; $list-text-color: #555555;
$profile-settings-link-color: $md-link-color;
$btn-transparent-color: #8F8F8F; $btn-transparent-color: #8F8F8F;
$ssh-key-icon-color: #8F8F8F; $ssh-key-icon-color: #8F8F8F;
$ssh-key-icon-size: 18px; $ssh-key-icon-size: 18px;
$provider-btn-group-border: #E5E5E5;
$provider-btn-not-active-color: #4688F1;
/* /*
* Color schema * Color schema
*/ */
...@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80); ...@@ -70,7 +72,7 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #E75E40; $orange-normal: #E75E40;
$orange-dark: #CE5237; $orange-dark: #CE5237;
$red-light: #F43263; $red-light: #F06559;
$red-normal: #E52C5A; $red-normal: #E52C5A;
$red-dark: #D22852; $red-dark: #D22852;
...@@ -94,13 +96,17 @@ $border-orange-light: #fc6d26; ...@@ -94,13 +96,17 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #CE5237; $border-orange-normal: #CE5237;
$border-orange-dark: #C14E35; $border-orange-dark: #C14E35;
$border-red-light: #E52C5A; $border-red-light: #F24F41;
$border-red-normal: #D22852; $border-red-normal: #D22852;
$border-red-dark: #CA264F; $border-red-dark: #CA264F;
$help-well-bg: #FAFAFA; $help-well-bg: #FAFAFA;
$help-well-border: #E5E5E5; $help-well-border: #E5E5E5;
$warning-message-bg: #FBF2D9;
$warning-message-color: #9E8E60;
$warning-message-border: #F0E2BB;
/* header */ /* header */
$light-grey-header: #faf9f9; $light-grey-header: #faf9f9;
...@@ -138,3 +144,22 @@ $dropdown-shadow-color: rgba(#000, .1); ...@@ -138,3 +144,22 @@ $dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494; $dropdown-header-color: #959494;
$dropdown-caret-color: #54565B; $dropdown-caret-color: #54565B;
$dropdown-title-btn-color: #BFBFBF;
$dropdown-input-color: #C7C7C7;
$dropdown-input-focus-border: rgb(58, 171, 240);
$dropdown-input-focus-shadow: rgba(#000, .2);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-toggle-bg: #fff;
$dropdown-toggle-color: #626262;
$dropdown-toggle-border-color: #EAEAEA;
$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #C4C4C4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Award emoji
*/
$award-emoji-menu-bg: #FFF;
$award-emoji-menu-border: #F1F2F4;
$award-emoji-new-btn-icon-color: #DCDCDC;
.awards { .awards {
@include clearfix;
line-height: 34px; line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: 7px 0 0 5px;
}
.award {
@include border-radius(5px);
border: 1px solid;
padding: 0px 10px;
float: left;
margin-right: 5px;
border-color: $border-color;
cursor: pointer;
&:hover {
background-color: #dce0e5;
}
&.active {
border-color: $border-gray-light;
background-color: $gray-light;
&:hover {
background-color: #dce0e5;
}
.counter {
font-weight: bold;
}
}
.icon {
float: left;
margin-right: 10px;
}
.counter {
float: left;
}
}
.awards-controls {
position: relative;
margin-left: 10px;
float: left;
.add-award {
font-size: 24px;
color: $gl-gray;
position: relative;
top: 2px;
&:hover,
&:link {
text-decoration: none;
}
} }
}
.emoji-menu{ .emoji-menu {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
margin-top: 3px;
z-index: 1000; z-index: 1000;
display: none;
float: left;
min-width: 160px; min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px; font-size: 14px;
text-align: left; background-color: $award-emoji-menu-bg;
list-style: none; border: 1px solid $award-emoji-menu-border;
background-color: #fff; border-radius: $border-radius-base;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
box-shadow: 0 6px 12px rgba(0,0,0,.175); box-shadow: 0 6px 12px rgba(0,0,0,.175);
pointer-events: none;
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44);
&.is-visible {
pointer-events: all;
opacity: 1;
transform: scale(1);
}
.emoji-menu-content { .emoji-menu-content {
padding: $gl-padding; padding: $gl-padding;
...@@ -90,36 +37,97 @@ ...@@ -90,36 +37,97 @@
height: 300px; height: 300px;
overflow-y: scroll; overflow-y: scroll;
h5 { input.emoji-search{
clear: left; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right 5px center;
background-size: 16px;
} }
ul {
list-style-type: none;
margin-left: -20px;
margin-bottom: 20px;
overflow: auto;
} }
}
input.emoji-search{ .emoji-menu-list {
background: image-url("icon-search.png") 240px no-repeat; list-style: none;
} padding-left: 0;
margin-bottom: 0;
}
.emoji-menu-list-item {
padding: 3px;
margin-left: 1px;
margin-right: 1px;
}
li { .emoji-menu-btn {
display: block;
cursor: pointer; cursor: pointer;
width: 30px; width: 30px;
height: 30px; height: 30px;
text-align: center; padding: 0;
float: left; background: none;
margin: 3px; border: 0;
list-decorate: none; border-radius: $border-radius-base;
@include border-radius(5px); transition: transform .15s cubic-bezier(.3, 0, .2, 2);
&:hover { &:hover {
background-color: #ccc; background-color: transparent;
outline: 0;
transform: scale(1.3);
} }
&:focus,
&:active {
outline: 0;
} }
.emoji-icon {
display: inline-block;
position: relative;
top: 3px;
}
}
.award-menu-holder {
display: inline-block;
position: relative;
}
.award-control {
margin-right: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
outline: 0;
&.active,
&:active {
background-color: $white-dark;
box-shadow: none;
outline: 0;
}
&.is-loading {
.award-control-icon {
display: none;
}
.award-control-icon-loading {
display: block;
} }
} }
.icon,
.award-control-icon {
float: left;
margin-right: 5px;
font-size: 20px;
}
.award-control-icon-loading {
display: none;
}
.award-control-icon {
color: $award-emoji-new-btn-icon-color;
} }
} }
...@@ -27,10 +27,25 @@ ...@@ -27,10 +27,25 @@
} }
.scroll-controls { .scroll-controls {
position: fixed; &.affix-top {
bottom: 10px; position: absolute;
left: 250px; top: 10px;
z-index: 100; right: 25px;
}
&.affix-bottom {
position: absolute;
right: 25px;
}
&.affix {
right: 30px;
bottom: 15px;
@media (min-width: $screen-md-min) {
right: 26%;
}
}
a { a {
display: block; display: block;
......
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
position: relative; position: relative;
font-family: $monospace_font; font-family: $monospace_font;
$left: 12px; $left: 12px;
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker { .max-width-marker {
width: 72ch; width: 72ch;
color: rgba(0, 0, 0, 0.0); color: rgba(0, 0, 0, 0.0);
......
...@@ -7,6 +7,28 @@ ...@@ -7,6 +7,28 @@
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
} }
&.suggest-colors-dropdown {
margin-bottom: 5px;
a {
@include border-radius(0);
width: 36.7px;
margin-right: 0;
margin-bottom: -5px;
}
}
}
.dropdown-label-color-preview {
display: none;
margin-top: 5px;
width: 100%;
height: 25px;
&.is-active {
display: block;
}
} }
.label-row { .label-row {
...@@ -19,3 +41,7 @@ ...@@ -19,3 +41,7 @@
.color-label { .color-label {
padding: 3px 4px; padding: 3px 4px;
} }
.label-subscription {
display: inline-block;
}
.account-page { .profile-avatar-form-option {
fieldset { hr {
margin-bottom: 15px; margin: 10px 0;
padding-bottom: 15px;
} }
} }
...@@ -20,7 +19,7 @@ ...@@ -20,7 +19,7 @@
.account-btn-link, .account-btn-link,
.profile-settings-sidebar a { .profile-settings-sidebar a {
color: $profile-settings-link-color; color: $md-link-color;
} }
.oauth-buttons { .oauth-buttons {
...@@ -110,42 +109,6 @@ ...@@ -110,42 +109,6 @@
} }
} }
.modal-profile-crop {
.modal-dialog {
width: 500px;
}
.modal-body {
p {
display: table;
margin: auto;
overflow: hidden;
}
img {
display: block;
max-width: 400px;
max-height: 400px;
}
.cropper-bg {
background: none;
}
.cropper-crop-box {
box-sizing: content-box;
border: 999px solid transparentize(#ccc, 0.5);
@include transform(translate(-999px, -999px));
}
}
}
@media (max-width: 520px) {
.modal-profile-crop .modal-dialog {
width: auto;
}
}
.key-list-item { .key-list-item {
.key-list-item-info { .key-list-item-info {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
...@@ -172,6 +135,65 @@ ...@@ -172,6 +135,65 @@
.profile-settings-content { .profile-settings-content {
a { a {
color: $profile-settings-link-color; color: $md-link-color;
}
}
.change-username-title {
color: $gl-warning;
}
.remove-account-title {
color: $gl-danger;
}
.provider-btn-group {
display: inline-block;
margin-right: 10px;
border: 1px solid $provider-btn-group-border;
border-radius: 3px;
&:last-child {
margin-right: 0;
}
}
.provider-btn-image {
display: inline-block;
padding: 5px 10px;
border-right: 1px solid $provider-btn-group-border;
> img {
width: 20px;
}
}
.provider-btn {
display: inline-block;
padding: 5px 10px;
margin-left: -3px;
line-height: 22px;
background-color: $gray-light;
&.not-active {
color: $provider-btn-not-active-color;
}
}
.profile-settings-message {
line-height: 32px;
color: $warning-message-color;
background-color: $warning-message-bg;
border: 1px solid $warning-message-border;
border-radius: $border-radius-base;
}
.oauth-applications {
form {
display: inline-block;
}
.last-heading {
width: 105px;
} }
} }
...@@ -28,3 +28,11 @@ ...@@ -28,3 +28,11 @@
border: 1px solid; border: 1px solid;
line-height: 32px; line-height: 32px;
} }
.markdown-snippet-copy {
position: fixed;
top: -10px;
left: -10px;
max-height: 0;
max-width: 0;
}
...@@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController
:email, :remember_me, :bio, :name, :username, :email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
:projects_limit, :can_create_group, :admin, :key_id :projects_limit, :can_create_group, :admin, :key_id, :external
) )
end end
......
...@@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base ...@@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check def ldap_security_check
if current_user && current_user.requires_ldap_check? if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
unless Gitlab::LDAP::Access.allowed?(current_user) unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user sign_out current_user
flash[:alert] = "Access denied for your LDAP account." flash[:alert] = "Access denied for your LDAP account."
......
module ContinueParams
extend ActiveSupport::Concern
def continue_params
continue_params = params[:continue]
return nil unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/')
continue_params
end
end
module ToggleSubscriptionAction
extend ActiveSupport::Concern
def toggle_subscription
return unless current_user
subscribable_resource.toggle_subscription(current_user)
render nothing: true
end
private
def subscribable_resource
raise NotImplementedError
end
end
...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE)
@last_push = current_user.recent_push @last_push = current_user.recent_push
...@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE)
@last_push = current_user.recent_push @last_push = current_user.recent_push
@groups = [] @groups = []
......
...@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
@projects = TrendingProjectsFinder.new.execute(current_user) @projects = TrendingProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = ProjectsFinder.new.execute(current_user) @projects = ProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects) @projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC') @projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController ...@@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController
# Load group projects # Load group projects
before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: [:show, :events] before_action :event_filter, only: [:activity]
layout :determine_layout layout :determine_layout
...@@ -44,6 +44,8 @@ class GroupsController < Groups::ApplicationController ...@@ -44,6 +44,8 @@ class GroupsController < Groups::ApplicationController
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
@shared_projects = @group.shared_projects
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -60,8 +62,10 @@ class GroupsController < Groups::ApplicationController ...@@ -60,8 +62,10 @@ class GroupsController < Groups::ApplicationController
end end
end end
def events def activity
respond_to do |format| respond_to do |format|
format.html
format.json do format.json do
load_events load_events
pager_json("events/_events", @events.count) pager_json("events/_events", @events.count)
...@@ -129,7 +133,7 @@ class GroupsController < Groups::ApplicationController ...@@ -129,7 +133,7 @@ class GroupsController < Groups::ApplicationController
end end
def group_params def group_params
params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level) params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock)
end end
def load_events def load_events
......
...@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -8,7 +8,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
layout 'profile' layout 'profile'
def index def index
head :forbidden and return set_index_vars
end end
def create def create
...@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -20,18 +20,11 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application) redirect_to oauth_application_url(@application)
else else
render :new set_index_vars
render :index
end end
end end
def destroy
if @application.destroy
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :destroy])
end
redirect_to applications_profile_url
end
private private
def verify_user_oauth_applications_enabled def verify_user_oauth_applications_enabled
...@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -40,6 +33,17 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
redirect_to applications_profile_url redirect_to applications_profile_url
end end
def set_index_vars
@applications = current_user.oauth_applications
@authorized_tokens = current_user.oauth_authorized_tokens
@authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
@authorized_apps = @authorized_tokens.map(&:application).uniq.reject(&:nil?)
# Don't overwrite a value possibly set by `create`
@application ||= Doorkeeper::Application.new
end
# Override Doorkeeper to scope to the current user
def set_application def set_application
@application = current_user.oauth_applications.find(params[:id]) @application = current_user.oauth_applications.find(params[:id])
end end
......
...@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -8,13 +8,6 @@ class ProfilesController < Profiles::ApplicationController
def show def show
end end
def applications
@applications = current_user.oauth_applications
@authorized_tokens = current_user.oauth_authorized_tokens
@authorized_anonymous_tokens = @authorized_tokens.reject(&:application)
@authorized_apps = @authorized_tokens.map(&:application).uniq - [nil]
end
def update def update
user_params.except!(:email) if @user.ldap_user? user_params.except!(:email) if @user.ldap_user?
...@@ -65,9 +58,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -65,9 +58,6 @@ class ProfilesController < Profiles::ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:avatar_crop_x,
:avatar_crop_y,
:avatar_crop_size,
:avatar, :avatar,
:bio, :bio,
:email, :email,
......
class Projects::ForksController < Projects::ApplicationController class Projects::ForksController < Projects::ApplicationController
include ContinueParams
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
...@@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -53,15 +55,4 @@ class Projects::ForksController < Projects::ApplicationController
render :error render :error
end end
end end
private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
end end
class Projects::GroupLinksController < Projects::ApplicationController
layout 'project_settings'
before_action :authorize_admin_project!
def index
@group_links = project.project_group_links.all
end
def create
link = project.project_group_links.new
link.group_id = params[:link_group_id]
link.group_access = params[:link_group_access]
link.save
redirect_to namespace_project_group_links_path(project.namespace, project)
end
def destroy
project.project_group_links.find(params[:id]).destroy
redirect_to namespace_project_group_links_path(project.namespace, project)
end
end
class Projects::ImportsController < Projects::ApplicationController class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :require_no_repo, only: [:new, :create] before_action :require_no_repo, only: [:new, :create]
...@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController ...@@ -44,16 +46,6 @@ class Projects::ImportsController < Projects::ApplicationController
private private
def continue_params
continue_params = params[:continue]
if continue_params
continue_params.permit(:to, :notice, :notice_now)
else
nil
end
end
def finished_notice def finished_notice
if @project.forked? if @project.forked?
'The project was successfully forked.' 'The project was successfully forked.'
......
class Projects::IssuesController < Projects::ApplicationController class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :toggle_subscription] before_action :issue, only: [:edit, :update, :show]
# Allow read any issue # Allow read any issue
before_action :authorize_read_issue! before_action :authorize_read_issue!
...@@ -110,12 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -110,12 +112,6 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
end end
def toggle_subscription
@issue.toggle_subscription(current_user)
render nothing: true
end
def closed_by_merge_requests def closed_by_merge_requests
@closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user) @closed_by_merge_requests ||= @issue.closed_by_merge_requests(current_user)
end end
...@@ -129,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -129,6 +125,7 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_old redirect_old
end end
end end
alias_method :subscribable_resource, :issue
def authorize_update_issue! def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue) return render_404 unless can?(current_user, :update_issue, @issue)
......
class Projects::LabelsController < Projects::ApplicationController class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy] before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_read_label! before_action :authorize_read_label!
before_action :authorize_admin_labels!, except: [:index] before_action :authorize_admin_labels!, only: [
:new, :create, :edit, :update, :generate, :destroy
]
respond_to :js, :html respond_to :js, :html
...@@ -73,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -73,8 +77,9 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def label def label
@label = @project.labels.find(params[:id]) @label ||= @project.labels.find(params[:id])
end end
alias_method :subscribable_resource, :label
def authorize_admin_labels! def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project) return render_404 unless can?(current_user, :admin_label, @project)
......
class Projects::MergeRequestsController < Projects::ApplicationController class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
include DiffHelper include DiffHelper
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
:ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds :ci_status, :cancel_merge_when_build_succeeds
] ]
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
...@@ -233,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -233,12 +234,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response render json: response
end end
def toggle_subscription
@merge_request.toggle_subscription(current_user)
render nothing: true
end
protected protected
def selected_target_project def selected_target_project
...@@ -252,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -252,6 +247,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request def merge_request
@merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end end
alias_method :subscribable_resource, :merge_request
def closes_issues def closes_issues
@closes_issues ||= @merge_request.closes_issues @closes_issues ||= @merge_request.closes_issues
......
...@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -27,6 +27,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
@project_member = @project.project_members.new @project_member = @project.project_members.new
@project_group_links = @project.project_group_links
end end
def create def create
......
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
include ExtractsPath include ExtractsPath
prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity] skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create] before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create] before_action :repository, except: [:new, :create]
...@@ -173,10 +172,15 @@ class ProjectsController < ApplicationController ...@@ -173,10 +172,15 @@ class ProjectsController < ApplicationController
def housekeeping def housekeeping
::Projects::HousekeepingService.new(@project).execute ::Projects::HousekeepingService.new(@project).execute
respond_to do |format| redirect_to(
flash[:notice] = "Housekeeping successfully started." project_path(@project),
format.html { redirect_to project_path(@project) } notice: "Housekeeping successfully started"
end )
rescue ::Projects::HousekeepingService::LeaseTaken => ex
redirect_to(
edit_project_path(@project),
alert: ex.to_s
)
end end
def toggle_star def toggle_star
...@@ -242,16 +246,6 @@ class ProjectsController < ApplicationController ...@@ -242,16 +246,6 @@ class ProjectsController < ApplicationController
end end
end end
def render_go_import
return unless params["go-get"] == "1"
@namespace = params[:namespace_id]
@id = params[:project_id] || params[:id]
@id = @id.gsub(/\.git\Z/, "")
render "go_import", layout: false
end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
......
...@@ -244,10 +244,17 @@ class IssuableFinder ...@@ -244,10 +244,17 @@ class IssuableFinder
items items
end end
def filter_by_upcoming_milestone?
params[:milestone_title] == '#upcoming'
end
def by_milestone(items) def by_milestone(items)
if milestones? if milestones?
if filter_by_no_milestone? if filter_by_no_milestone?
items = items.where(milestone_id: [-1, nil]) items = items.where(milestone_id: [-1, nil])
elsif filter_by_upcoming_milestone?
upcoming = Milestone.where(project_id: projects).upcoming
items = items.joins(:milestone).where(milestones: { title: upcoming.title })
else else
items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] })
......
...@@ -40,21 +40,26 @@ class ProjectsFinder ...@@ -40,21 +40,26 @@ class ProjectsFinder
private private
def group_projects(current_user, group) def group_projects(current_user, group)
if current_user return [group.projects.public_only] unless current_user
[
user_group_projects = [
group_projects_for_user(current_user, group), group_projects_for_user(current_user, group),
group.projects.public_and_internal_only group.shared_projects.visible_to_user(current_user)
] ]
if current_user.external?
user_group_projects << group.projects.public_only
else else
[group.projects.public_only] user_group_projects << group.projects.public_and_internal_only
end end
end end
def all_projects(current_user) def all_projects(current_user)
if current_user return [public_projects] unless current_user
[current_user.authorized_projects, public_and_internal_projects]
if current_user.external?
[current_user.authorized_projects, public_projects]
else else
[Project.public_only] [current_user.authorized_projects, public_and_internal_projects]
end end
end end
......
...@@ -72,7 +72,7 @@ module ApplicationHelper ...@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User) if user_or_email.is_a?(User)
user = user_or_email user = user_or_email
else else
user = User.find_by(email: user_or_email.try(:downcase)) user = User.find_by_any_email(user_or_email.try(:downcase))
end end
if user if user
......
...@@ -12,9 +12,13 @@ module CiStatusHelper ...@@ -12,9 +12,13 @@ module CiStatusHelper
ci_label_for_status(ci_commit.status) ci_label_for_status(ci_commit.status)
end end
def ci_status_with_icon(status) def ci_status_with_icon(status, target = nil)
content_tag :span, class: "ci-status ci-#{status}" do content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status) klass = "ci-status ci-#{status}"
if target
link_to content, target, class: klass
else
content_tag :span, content, class: klass
end end
end end
...@@ -42,12 +46,12 @@ module CiStatusHelper ...@@ -42,12 +46,12 @@ module CiStatusHelper
icon(icon_name + ' fw') icon(icon_name + ' fw')
end end
def render_ci_status(ci_commit) def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit), link_to ci_status_icon(ci_commit),
ci_status_path(ci_commit), ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}", title: "Build #{ci_status_label(ci_commit)}",
data: { toggle: 'tooltip', placement: 'left' } data: { toggle: 'tooltip', placement: tooltip_placement }
end end
def no_runners_for_project?(project) def no_runners_for_project?(project)
......
module DropdownsHelper
def dropdown_tag(toggle_text, options: {}, &block)
content_tag :div, class: "dropdown" do
data_attr = { toggle: "dropdown" }
if options.has_key?(:data)
data_attr = options[:data].merge(data_attr)
end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
output = ""
if options.has_key?(:title)
output << dropdown_title(options[:title])
end
if options.has_key?(:filter)
output << dropdown_filter(options[:placeholder])
end
output << content_tag(:div, class: "dropdown-content") do
capture(&block) if block && !options.has_key?(:footer_content)
end
if block && options.has_key?(:footer_content)
output << content_tag(:div, class: "dropdown-footer") do
capture(&block)
end
end
output << dropdown_loading
output.html_safe
end
dropdown_output.html_safe
end
end
def dropdown_toggle(toggle_text, data_attr, options)
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
output << icon('chevron-down')
output.html_safe
end
end
def dropdown_title(title, back: false)
content_tag :div, class: "dropdown-title" do
title_output = ""
if back
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
icon('arrow-left')
end
end
title_output << content_tag(:span, title)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
icon('times')
end
title_output.html_safe
end
end
def dropdown_filter(placeholder)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
filter_output << icon('search')
filter_output.html_safe
end
end
def dropdown_content(&block)
content_tag(:div, class: "dropdown-content") do
if block
capture(&block)
end
end
end
def dropdown_footer(&block)
content_tag(:div, class: "dropdown-footer") do
if block
capture(&block)
end
end
end
def dropdown_loading
content_tag :div, class: "dropdown-loading" do
icon('spinner spin')
end
end
end
...@@ -3,7 +3,7 @@ module EventsHelper ...@@ -3,7 +3,7 @@ module EventsHelper
author = event.author author = event.author
if author if author
link_to author.name, user_path(author.username) link_to author.name, user_path(author.username), title: h(author.name)
else else
event.author_name event.author_name
end end
...@@ -159,7 +159,7 @@ module EventsHelper ...@@ -159,7 +159,7 @@ module EventsHelper
link_to( link_to(
namespace_project_commit_path(event.project.namespace, event.project, namespace_project_commit_path(event.project.namespace, event.project,
event.note_commit_id, event.note_commit_id,
anchor: dom_id(event.target)), anchor: dom_id(event.target), title: h(event.target_title)),
class: "commit_short_id" class: "commit_short_id"
) do ) do
"#{event.note_target_type} #{event.note_short_commit_id}" "#{event.note_target_type} #{event.note_short_commit_id}"
...@@ -167,7 +167,7 @@ module EventsHelper ...@@ -167,7 +167,7 @@ module EventsHelper
elsif event.note_project_snippet? elsif event.note_project_snippet?
link_to(namespace_project_snippet_path(event.project.namespace, link_to(namespace_project_snippet_path(event.project.namespace,
event.project, event.project,
event.note_target)) do event.note_target), title: h(event.project.name)) do
"#{event.note_target_type} #{truncate event.note_target.to_reference}" "#{event.note_target_type} #{truncate event.note_target.to_reference}"
end end
else else
......
...@@ -31,7 +31,11 @@ module IssuablesHelper ...@@ -31,7 +31,11 @@ module IssuablesHelper
end end
def issuable_state_scope(issuable) def issuable_state_scope(issuable)
if issuable.respond_to?(:merged?) && issuable.merged?
:merged
else
issuable.open? ? :opened : :closed issuable.open? ? :opened : :closed
end end
end
end end
...@@ -124,6 +124,14 @@ module LabelsHelper ...@@ -124,6 +124,14 @@ module LabelsHelper
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end end
def label_subscription_status(label)
label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
end
def label_subscription_toggle_button_text(label)
label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
end
# Required for Banzai::Filter::LabelReferenceFilter # Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :render_colored_cross_project_label, module_function :render_colored_label, :render_colored_cross_project_label,
:text_color_for_bg, :escape_once :text_color_for_bg, :escape_once
......
...@@ -59,6 +59,7 @@ module MilestonesHelper ...@@ -59,6 +59,7 @@ module MilestonesHelper
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any) grouped_milestones.unshift(Milestone::Any)
grouped_milestones.unshift(Milestone::Upcoming)
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end end
......
...@@ -8,7 +8,7 @@ module ProjectsHelper ...@@ -8,7 +8,7 @@ module ProjectsHelper
end end
def link_to_project(project) def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project] do link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name') title = content_tag(:span, project.name, class: 'project-name')
if project.namespace if project.namespace
......
...@@ -40,7 +40,7 @@ module SearchHelper ...@@ -40,7 +40,7 @@ module SearchHelper
{ label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
{ label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
{ label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
{ label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") }, { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
{ label: "help: Workflow Help", url: help_page_path("workflow", "README") }, { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
] ]
end end
......
...@@ -16,7 +16,7 @@ module TodosHelper ...@@ -16,7 +16,7 @@ module TodosHelper
def todo_target_link(todo) def todo_target_link(todo)
target = todo.target_type.titleize.downcase target = todo.target_type.titleize.downcase
link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo) link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) }
end end
def todo_target_path(todo) def todo_target_path(todo)
......
...@@ -16,7 +16,15 @@ module Emails ...@@ -16,7 +16,15 @@ module Emails
def closed_issue_email(recipient_id, issue_id, updated_by_user_id) def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
@label_names = label_names
@labels_url = namespace_project_labels_url(@project.namespace, @project)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
...@@ -24,20 +32,12 @@ module Emails ...@@ -24,20 +32,12 @@ module Emails
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@issue_status = status @issue_status = status
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end end
private private
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
def setup_issue_mail(issue_id, recipient_id) def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id) @issue = Issue.find(issue_id)
@project = @issue.project @project = @issue.project
...@@ -45,5 +45,13 @@ module Emails ...@@ -45,5 +45,13 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key) @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end end
def issue_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")
}
end
end end
end end
...@@ -3,50 +3,43 @@ module Emails ...@@ -3,50 +3,43 @@ module Emails
def new_merge_request_email(recipient_id, merge_request_id) def new_merge_request_email(recipient_id, merge_request_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_new_thread(@merge_request, mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
from: sender(@merge_request.author_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id), end
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@label_names = label_names
@labels_url = namespace_project_labels_url(@project.namespace, @project)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status @mr_status = status
@updated_by = User.find updated_by_user_id @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
end end
private private
...@@ -54,11 +47,17 @@ module Emails ...@@ -54,11 +47,17 @@ module Emails
def setup_merge_request_mail(merge_request_id, recipient_id) def setup_merge_request_mail(merge_request_id, recipient_id)
@merge_request = MergeRequest.find(merge_request_id) @merge_request = MergeRequest.find(merge_request_id)
@project = @merge_request.project @project = @merge_request.project
@target_url = namespace_project_merge_request_url(@project.namespace, @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request)
@project,
@merge_request)
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def merge_request_thread_options(sender_id, recipient_id)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
}
end
end end
end end
...@@ -14,7 +14,10 @@ module Emails ...@@ -14,7 +14,10 @@ module Emails
end end
def new_ssh_key_email(key_id) def new_ssh_key_email(key_id)
@key = Key.find(key_id) @key = Key.find_by_id(key_id)
return unless @key
@current_user = @user = @key.user @current_user = @user = @key.user
@target_url = user_url(@user) @target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account")) mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
......
...@@ -109,23 +109,10 @@ class Ability ...@@ -109,23 +109,10 @@ class Ability
key = "/user/#{user.id}/project/#{project.id}" key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin RequestStore.store[key] ||= begin
team = project.team # Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
# Rules based on role in project if project.public? || (project.internal? && !user.external?)
if team.master?(user)
rules.push(*project_master_rules)
elsif team.developer?(user)
rules.push(*project_dev_rules)
elsif team.reporter?(user)
rules.push(*project_report_rules)
elsif team.guest?(user)
rules.push(*project_guest_rules)
end
if project.public? || project.internal?
rules.push(*public_project_rules) rules.push(*public_project_rules)
# Allow to read builds for internal projects # Allow to read builds for internal projects
...@@ -148,6 +135,19 @@ class Ability ...@@ -148,6 +135,19 @@ class Ability
end end
end end
def project_team_rules(team, user)
# Rules based on role in project
if team.master?(user)
project_master_rules
elsif team.developer?(user)
project_dev_rules
elsif team.reporter?(user)
project_report_rules
elsif team.guest?(user)
project_guest_rules
end
end
def public_project_rules def public_project_rules
@public_project_rules ||= project_guest_rules + [ @public_project_rules ||= project_guest_rules + [
:download_code, :download_code,
...@@ -360,7 +360,7 @@ class Ability ...@@ -360,7 +360,7 @@ class Ability
] ]
end end
if snippet.public? || snippet.internal? if snippet.public? || (snippet.internal? && !user.external?)
rules << :read_personal_snippet rules << :read_personal_snippet
end end
......
...@@ -37,8 +37,6 @@ ...@@ -37,8 +37,6 @@
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace'] LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner' belongs_to :runner, class_name: 'Ci::Runner'
...@@ -128,7 +126,7 @@ module Ci ...@@ -128,7 +126,7 @@ module Ci
end end
def retried? def retried?
!self.commit.latest_builds_for_ref(self.ref).include?(self) !self.commit.latest_statuses_for_ref(self.ref).include?(self)
end end
def depends_on_builds def depends_on_builds
...@@ -309,22 +307,6 @@ module Ci ...@@ -309,22 +307,6 @@ module Ci
project.valid_runners_token? token project.valid_runners_token? token
end end
def target_url
namespace_project_build_url(project.namespace, project, self)
end
def cancel_url
if active?
cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
if retryable?
retry_namespace_project_build_path(project.namespace, project, self)
end
end
def can_be_served?(runner) def can_be_served?(runner)
(tag_list - runner.tag_list).empty? (tag_list - runner.tag_list).empty?
end end
...@@ -333,7 +315,7 @@ module Ci ...@@ -333,7 +315,7 @@ module Ci
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end end
def show_warning? def stuck?
pending? && !any_runners_online? pending? && !any_runners_online?
end end
...@@ -348,18 +330,6 @@ module Ci ...@@ -348,18 +330,6 @@ module Ci
artifacts_file.exists? artifacts_file.exists?
end end
def artifacts_download_url
if artifacts?
download_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_browse_url
if artifacts_metadata?
browse_namespace_project_build_artifacts_path(project.namespace, project, self)
end
end
def artifacts_metadata? def artifacts_metadata?
artifacts? && artifacts_metadata.exists? artifacts? && artifacts_metadata.exists?
end end
......
...@@ -25,8 +25,6 @@ module Ci ...@@ -25,8 +25,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
validates_presence_of :sha validates_presence_of :sha
validate :valid_commit_sha validate :valid_commit_sha
...@@ -42,16 +40,6 @@ module Ci ...@@ -42,16 +40,6 @@ module Ci
project.id project.id
end end
def last_build
builds.order(:id).last
end
def retry
latest_builds.each do |build|
Ci::Build.retry(build)
end
end
def valid_commit_sha def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)") self.errors.add(:sha, " cant be 00000000 (branch removal)")
...@@ -121,12 +109,14 @@ module Ci ...@@ -121,12 +109,14 @@ module Ci
@latest_statuses ||= statuses.latest.to_a @latest_statuses ||= statuses.latest.to_a
end end
def latest_builds def latest_statuses_for_ref(ref)
@latest_builds ||= builds.latest.to_a latest_statuses.select { |status| status.ref == ref }
end end
def latest_builds_for_ref(ref) def matrix_builds(build = nil)
latest_builds.select { |build| build.ref == ref } matrix_builds = builds.latest.ordered
matrix_builds = matrix_builds.similar(build) if build
matrix_builds.to_a
end end
def retried def retried
...@@ -170,7 +160,7 @@ module Ci ...@@ -170,7 +160,7 @@ module Ci
end end
def duration def duration
duration_array = latest_statuses.map(&:duration).compact duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i duration_array.reduce(:+).to_i
end end
...@@ -183,16 +173,12 @@ module Ci ...@@ -183,16 +173,12 @@ module Ci
end end
def coverage def coverage
coverage_array = latest_builds.map(&:coverage).compact coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1 if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size) '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end end
end end
def matrix_for_ref?(ref)
latest_builds_for_ref(ref).size > 1
end
def config_processor def config_processor
return nil unless ci_yaml_file return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
...@@ -218,10 +204,6 @@ module Ci ...@@ -218,10 +204,6 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end end
def update_committed!
update!(committed_at: DateTime.now)
end
private private
def save_yaml_error(error) def save_yaml_error(error)
......
...@@ -46,9 +46,23 @@ module Ci ...@@ -46,9 +46,23 @@ module Ci
acts_as_taggable acts_as_taggable
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def self.search(query) def self.search(query)
where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', t = arel_table
query: "%#{query.try(:downcase)}%") pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end end
def set_default_values def set_default_values
......
...@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -125,23 +125,7 @@ class CommitStatus < ActiveRecord::Base
end end
end end
def cancel_url def stuck?
nil
end
def retry_url
nil
end
def show_warning?
false false
end end
def artifacts_download_url
nil
end
def artifacts_browse_url
nil
end
end end
...@@ -8,6 +8,7 @@ module Issuable ...@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Participable include Participable
include Mentionable include Mentionable
include Subscribable
include StripAttribute include StripAttribute
included do included do
...@@ -18,7 +19,6 @@ module Issuable ...@@ -18,7 +19,6 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links has_many :labels, through: :label_links
has_many :subscriptions, dependent: :destroy, as: :subscribable
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 } validates :title, presence: true, length: { within: 0..255 }
...@@ -61,12 +61,29 @@ module Issuable ...@@ -61,12 +61,29 @@ module Issuable
end end
module ClassMethods module ClassMethods
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(title) like :query", query: "%#{query.downcase}%") where(arel_table[:title].matches("%#{query}%"))
end end
# Searches for records with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%") t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
def sort(method) def sort(method)
...@@ -132,28 +149,10 @@ module Issuable ...@@ -132,28 +149,10 @@ module Issuable
notes.awards.where(note: "thumbsup").count notes.awards.where(note: "thumbsup").count
end end
def subscribed?(user) def subscribed_without_subscriptions?(user)
subscription = subscriptions.find_by_user_id(user.id)
if subscription
return subscription.subscribed
end
participants(user).include?(user) participants(user).include?(user)
end end
def toggle_subscription(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: !subscribed?(user))
end
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: false)
end
def to_hook_data(user) def to_hook_data(user)
hook_data = { hook_data = {
object_kind: self.class.name.underscore, object_kind: self.class.name.underscore,
......
# == Subscribable concern
#
# Users can subscribe to these models.
#
# Used by Issue, MergeRequest, Label
#
module Subscribable
extend ActiveSupport::Concern
included do
has_many :subscriptions, dependent: :destroy, as: :subscribable
end
def subscribed?(user)
if subscription = subscriptions.find_by_user_id(user.id)
subscription.subscribed
else
subscribed_without_subscriptions?(user)
end
end
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
def subscribed_without_subscriptions?(user)
false
end
def subscribers
subscriptions.where(subscribed: true).map(&:user)
end
def toggle_subscription(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: !subscribed?(user))
end
def unsubscribe(user)
subscriptions.
find_or_initialize_by(user_id: user.id).
update(subscribed: false)
end
end
...@@ -25,6 +25,8 @@ class Group < Namespace ...@@ -25,6 +25,8 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members alias_method :members, :group_members
has_many :users, through: :group_members has_many :users, through: :group_members
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
...@@ -35,8 +37,18 @@ class Group < Namespace ...@@ -35,8 +37,18 @@ class Group < Namespace
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
class << self class << self
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%") table = Namespace.arel_table
pattern = "%#{query}%"
where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
end end
def sort(method) def sort(method)
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
require 'digest/md5' require 'digest/md5'
class Key < ActiveRecord::Base class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable include Sortable
belongs_to :user belongs_to :user
...@@ -62,7 +63,7 @@ class Key < ActiveRecord::Base ...@@ -62,7 +63,7 @@ class Key < ActiveRecord::Base
end end
def notify_user def notify_user
NotificationService.new.new_key(self) run_after_commit { NotificationService.new.new_key(self) }
end end
def post_create_hook def post_create_hook
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
class Label < ActiveRecord::Base class Label < ActiveRecord::Base
include Referable include Referable
include Subscribable
# Represents a "No Label" state used for filtering Issues and Merge # Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned. # Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name) LabelStruct = Struct.new(:title, :name)
......
...@@ -135,7 +135,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -135,7 +135,6 @@ class MergeRequest < ActiveRecord::Base
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :merged, -> { with_state(:merged) } scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) }
...@@ -161,6 +160,24 @@ class MergeRequest < ActiveRecord::Base ...@@ -161,6 +160,24 @@ class MergeRequest < ActiveRecord::Base
super("merge_requests", /(?<merge_request>\d+)/) super("merge_requests", /(?<merge_request>\d+)/)
end end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
# ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries
# using multiple sub-queries especially when combined with an OR statement.
# UNIONs on the other hand perform much better in these cases.
#
# relation - An ActiveRecord::Relation that returns a list of Projects.
#
# Returns an ActiveRecord::Relation.
def self.in_projects(relation)
source = where(source_project_id: relation).select(:id)
target = where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
where("merge_requests.id IN (#{union.to_sql})")
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
......
...@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base ...@@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base
MilestoneStruct = Struct.new(:title, :name, :id) MilestoneStruct = Struct.new(:title, :name, :id)
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1) Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
include InternalId include InternalId
include Sortable include Sortable
...@@ -58,9 +59,18 @@ class Milestone < ActiveRecord::Base ...@@ -58,9 +59,18 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title alias_attribute :name, :title
class << self class << self
# Searches for milestones matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
query = "%#{query}%" t = arel_table
where("title like ? or description like ?", query, query) pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
end end
...@@ -72,6 +82,10 @@ class Milestone < ActiveRecord::Base ...@@ -72,6 +82,10 @@ class Milestone < ActiveRecord::Base
super("milestones", /(?<milestone>\d+)/) super("milestones", /(?<milestone>\d+)/)
end end
def self.upcoming
self.where('due_date > ?', Time.now).order(due_date: :asc).first
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
escaped_title = self.title.gsub("]", "\\]") escaped_title = self.title.gsub("]", "\\]")
......
...@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base ...@@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end end
# Searches for namespaces matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation
def search(query) def search(query)
where("name LIKE :query OR path LIKE :query", query: "%#{query}%") t = arel_table
pattern = "%#{query}%"
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end end
def clean_path(path) def clean_path(path)
......
...@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base ...@@ -44,6 +44,7 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
before_validation :set_award! before_validation :set_award!
before_validation :clear_blank_line_code!
validates :note, :project, presence: true validates :note, :project, presence: true
validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award }
...@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base ...@@ -63,7 +64,7 @@ class Note < ActiveRecord::Base
scope :nonawards, ->{ where(is_award: false) } scope :nonawards, ->{ where(is_award: false) }
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :inline, ->{ where("line_code IS NOT NULL") } scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) } scope :not_inline, ->{ where(line_code: nil) }
scope :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) } scope :common, ->{ where(noteable_type: ["", nil]) }
...@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base ...@@ -105,8 +106,18 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym [:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end end
# Searches for notes matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%") table = arel_table
pattern = "%#{query}%"
where(table[:note].matches(pattern))
end end
def grouped_awards def grouped_awards
...@@ -162,26 +173,29 @@ class Note < ActiveRecord::Base ...@@ -162,26 +173,29 @@ class Note < ActiveRecord::Base
Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
end end
# Check if such line of code exists in merge request diff # Check if this note is part of an "active" discussion
# If exists - its active discussion #
# If not - its outdated diff # This will always return true for anything except MergeRequest noteables,
# which have special logic.
#
# If the note's current diff cannot be matched in the MergeRequest's current
# diff, it's considered inactive.
def active? def active?
return true unless self.diff return true unless self.diff
return false unless noteable return false unless noteable
return @active if defined?(@active) return @active if defined?(@active)
diffs = noteable.diffs(Commit.max_diff_options) noteable_diff = find_noteable_diff
notable_diff = diffs.find { |d| d.new_path == self.diff.new_path }
return @active = false if notable_diff.nil? if noteable_diff
parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line)
parsed_lines = Gitlab::Diff::Parser.new.parse(notable_diff.diff.each_line)
# We cannot use ||= because @active may be false
@active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line }
else
@active = false
end end
def outdated? @active
!active?
end end
def diff_file_index def diff_file_index
...@@ -365,6 +379,16 @@ class Note < ActiveRecord::Base ...@@ -365,6 +379,16 @@ class Note < ActiveRecord::Base
private private
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
# Find the diff on noteable that matches our own
def find_noteable_diff
diffs = noteable.diffs(Commit.max_diff_options)
diffs.find { |d| d.new_path == self.diff.new_path }
end
def awards_supported? def awards_supported?
(for_issue? || for_merge_request?) && !for_diff_line? (for_issue? || for_merge_request?) && !for_diff_line?
end end
......
...@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base ...@@ -151,6 +151,8 @@ class Project < ActiveRecord::Base
has_many :releases, dependent: :destroy has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
...@@ -250,12 +252,6 @@ class Project < ActiveRecord::Base ...@@ -250,12 +252,6 @@ class Project < ActiveRecord::Base
where('projects.last_activity_at < ?', 6.months.ago) where('projects.last_activity_at < ?', 6.months.ago)
end end
def publicish(user)
visibility_levels = [Project::PUBLIC]
visibility_levels << Project::INTERNAL if user
where(visibility_level: visibility_levels)
end
def with_push def with_push
joins(:events).where('events.action = ?', Event::PUSHED) joins(:events).where('events.action = ?', Event::PUSHED)
end end
...@@ -264,13 +260,38 @@ class Project < ActiveRecord::Base ...@@ -264,13 +260,38 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
# search. On MySQL a regular "LIKE" is used as it's already
# case-insensitive.
#
# query - The search query as a String.
def search(query) def search(query)
ptable = arel_table
ntable = Namespace.arel_table
pattern = "%#{query}%"
projects = select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
# We explicitly remove any eager loading clauses as they're:
#
# 1. Not needed by this query
# 2. Combined with .joins(:namespace) lead to all columns from the
# projects & namespaces tables being selected, leading to a SQL error
# due to the columns of all UNION'd queries no longer being the same.
namespaces = select(:id).
except(:includes).
joins(:namespace). joins(:namespace).
where('LOWER(projects.name) LIKE :query OR where(ntable[:name].matches(pattern))
LOWER(projects.path) LIKE :query OR
LOWER(namespaces.name) LIKE :query OR union = Gitlab::SQL::Union.new([projects, namespaces])
LOWER(projects.description) LIKE :query',
query: "%#{query.try(:downcase)}%") where("projects.id IN (#{union.to_sql})")
end end
def search_by_visibility(level) def search_by_visibility(level)
...@@ -278,7 +299,10 @@ class Project < ActiveRecord::Base ...@@ -278,7 +299,10 @@ class Project < ActiveRecord::Base
end end
def search_by_title(query) def search_by_title(query)
non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") pattern = "%#{query}%"
table = Project.arel_table
non_archived.where(table[:name].matches(pattern))
end end
def find_with_namespace(id) def find_with_namespace(id)
...@@ -483,6 +507,7 @@ class Project < ActiveRecord::Base ...@@ -483,6 +507,7 @@ class Project < ActiveRecord::Base
end end
def external_issue_tracker def external_issue_tracker
return @external_issue_tracker if defined?(@external_issue_tracker)
@external_issue_tracker ||= @external_issue_tracker ||=
services.issue_trackers.active.without_defaults.first services.issue_trackers.active.without_defaults.first
end end
...@@ -526,11 +551,11 @@ class Project < ActiveRecord::Base ...@@ -526,11 +551,11 @@ class Project < ActiveRecord::Base
end end
def ci_services def ci_services
services.select { |service| service.category == :ci } services.where(category: :ci)
end end
def ci_service def ci_service
@ci_service ||= ci_services.find(&:activated?) @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end end
def jira_tracker? def jira_tracker?
...@@ -876,6 +901,10 @@ class Project < ActiveRecord::Base ...@@ -876,6 +901,10 @@ class Project < ActiveRecord::Base
jira_tracker? && jira_service.active jira_tracker? && jira_service.active
end end
def allowed_to_share_with_group?
!namespace.share_with_group_lock
end
def ci_commit(sha) def ci_commit(sha)
ci_commits.find_by(sha: sha) ci_commits.find_by(sha: sha)
end end
...@@ -907,13 +936,13 @@ class Project < ActiveRecord::Base ...@@ -907,13 +936,13 @@ class Project < ActiveRecord::Base
end end
def valid_runners_token? token def valid_runners_token? token
self.runners_token && self.runners_token == token self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
# TODO (ayufan): For now we use runners_token (backward compatibility) # TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build # In 8.4 every build will have its own individual token valid for time of build
def valid_build_token? token def valid_build_token? token
self.builds_enabled? && self.runners_token && self.runners_token == token self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
def build_coverage_enabled? def build_coverage_enabled?
......
class ProjectGroupLink < ActiveRecord::Base
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MASTER = 40
belongs_to :project
belongs_to :group
validates :project_id, presence: true
validates :group_id, presence: true
validates :group_id, uniqueness: { scope: [:project_id], message: "already shared with this group" }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
def self.access_options
Gitlab::Access.options
end
def self.default_access
DEVELOPER
end
def human_access
self.class.access_options.key(self.group_access)
end
private
def different_group
if self.group && self.project && self.project.group == self.group
errors.add(:base, "Project cannot be shared with the project it is in.")
end
end
end
...@@ -26,7 +26,7 @@ class CiService < Service ...@@ -26,7 +26,7 @@ class CiService < Service
default_value_for :category, 'ci' default_value_for :category, 'ci'
def valid_token?(token) def valid_token?(token)
self.respond_to?(:token) && self.token.present? && self.token == token self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def supported_events def supported_events
......
...@@ -160,7 +160,27 @@ class ProjectTeam ...@@ -160,7 +160,27 @@ class ProjectTeam
end end
end end
access.max if project.invited_groups.any? && project.allowed_to_share_with_group?
access << max_invited_level(user_id)
end
access.compact.max
end
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
invited_group = group_link.group
access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
# If group member has higher access level we should restrict it
# to max allowed access level
if access && access > group_link.group_access
access = group_link.group_access
end
access
end.compact.max
end end
private private
...@@ -168,6 +188,35 @@ class ProjectTeam ...@@ -168,6 +188,35 @@ class ProjectTeam
def fetch_members(level = nil) def fetch_members(level = nil)
project_members = project.project_members project_members = project.project_members
group_members = group ? group.group_members : [] group_members = group ? group.group_members : []
invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group?
project.project_group_links.each do |group_link|
invited_group = group_link.group
im = invited_group.group_members
if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
# Skip group members if we ask for masters
# but max group access is developers
next if int_level > group_link.group_access
# If we ask for developers and max
# group access is developers we need to provide
# both group master, developers as devs
if int_level == group_link.group_access
im.where("access_level >= ?)", group_link.group_access)
else
im.send(level)
end
end
invited_members << im
end
invited_members = invited_members.flatten.compact
end
if level if level
project_members = project_members.send(level) project_members = project_members.send(level)
...@@ -175,6 +224,7 @@ class ProjectTeam ...@@ -175,6 +224,7 @@ class ProjectTeam
end end
user_ids = project_members.pluck(:user_id) user_ids = project_members.pluck(:user_id)
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
user_ids.push(*group_members.pluck(:user_id)) if group user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids) User.where(id: user_ids)
......
...@@ -113,12 +113,32 @@ class Snippet < ActiveRecord::Base ...@@ -113,12 +113,32 @@ class Snippet < ActiveRecord::Base
end end
class << self class << self
# Searches for snippets with a matching title or file name.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") t = arel_table
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end end
# Searches for snippets with matching content.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def search_code(query) def search_code(query)
where('(content LIKE :query)', query: "%#{query}%") table = Snippet.arel_table
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end end
def accessible_to(user) def accessible_to(user)
......
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
# hide_project_limit :boolean default(FALSE) # hide_project_limit :boolean default(FALSE)
# unlock_token :string # unlock_token :string
# otp_grace_period_started_at :datetime # otp_grace_period_started_at :datetime
# external :boolean default(FALSE)
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
...@@ -77,6 +78,7 @@ class User < ActiveRecord::Base ...@@ -77,6 +78,7 @@ class User < ActiveRecord::Base
add_authentication_token_field :authentication_token add_authentication_token_field :authentication_token
default_value_for :admin, false default_value_for :admin, false
default_value_for :external, false
default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_group, gitlab_config.default_can_create_group
default_value_for :can_create_team, false default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false default_value_for :hide_no_ssh_key, false
...@@ -98,9 +100,6 @@ class User < ActiveRecord::Base ...@@ -98,9 +100,6 @@ class User < ActiveRecord::Base
# Virtual attribute for authenticating by either username or email # Virtual attribute for authenticating by either username or email
attr_accessor :login attr_accessor :login
# Virtual attributes to define avatar cropping
attr_accessor :avatar_crop_x, :avatar_crop_y, :avatar_crop_size
# #
# Relations # Relations
# #
...@@ -166,11 +165,6 @@ class User < ActiveRecord::Base ...@@ -166,11 +165,6 @@ class User < ActiveRecord::Base
validate :owns_public_email, if: ->(user) { user.public_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
validates :avatar_crop_x, :avatar_crop_y, :avatar_crop_size,
numericality: { only_integer: true },
presence: true,
if: ->(user) { user.avatar? && user.avatar_changed? }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create before_validation :restricted_signup_domains, on: :create
before_validation :sanitize_attrs before_validation :sanitize_attrs
...@@ -179,6 +173,7 @@ class User < ActiveRecord::Base ...@@ -179,6 +173,7 @@ class User < ActiveRecord::Base
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token before_save :ensure_authentication_token
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct after_save :ensure_namespace_correct
after_initialize :set_projects_limit after_initialize :set_projects_limit
after_create :post_create_hook after_create :post_create_hook
...@@ -226,6 +221,7 @@ class User < ActiveRecord::Base ...@@ -226,6 +221,7 @@ class User < ActiveRecord::Base
# Scopes # Scopes
scope :admins, -> { where(admin: true) } scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
...@@ -281,13 +277,29 @@ class User < ActiveRecord::Base ...@@ -281,13 +277,29 @@ class User < ActiveRecord::Base
self.with_two_factor self.with_two_factor
when 'wop' when 'wop'
self.without_projects self.without_projects
when 'external'
self.external
else else
self.active self.active
end end
end end
# Searches users matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query) def search(query)
where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%") table = arel_table
pattern = "%#{query}%"
where(
table[:name].matches(pattern).
or(table[:email].matches(pattern)).
or(table[:username].matches(pattern))
)
end end
def by_login(login) def by_login(login)
...@@ -612,6 +624,13 @@ class User < ActiveRecord::Base ...@@ -612,6 +624,13 @@ class User < ActiveRecord::Base
end end
end end
def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user.
lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
lease.try_obtain
end
def solo_owned_groups def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group| @solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self] group.owners == [self]
...@@ -811,7 +830,8 @@ class User < ActiveRecord::Base ...@@ -811,7 +830,8 @@ class User < ActiveRecord::Base
def projects_union def projects_union
Gitlab::SQL::Union.new([personal_projects.select(:id), Gitlab::SQL::Union.new([personal_projects.select(:id),
groups_projects.select(:id), groups_projects.select(:id),
projects.select(:id)]) projects.select(:id),
groups.joins(:shared_projects).select(:project_id)])
end end
def ci_projects_union def ci_projects_union
...@@ -827,4 +847,11 @@ class User < ActiveRecord::Base ...@@ -827,4 +847,11 @@ class User < ActiveRecord::Base
def send_devise_notification(notification, *args) def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later devise_mailer.send(notification, self, *args).deliver_later
end end
def ensure_external_user_rights
return unless self.external?
self.can_create_group = false
self.projects_limit = 0
end
end end
...@@ -3,7 +3,7 @@ module Ci ...@@ -3,7 +3,7 @@ module Ci
def execute(project, opts) def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref]) sha = opts[:sha] || ref_sha(project, opts[:ref])
commit = project.ci_commits.ordered.find_by(sha: sha) commit = project.ci_commits.find_by(sha: sha)
image_name = image_for_commit(commit) image_name = image_for_commit(commit)
image_path = Rails.root.join('public/ci', image_name) image_path = Rails.root.join('public/ci', image_name)
......
...@@ -33,7 +33,6 @@ class CreateCommitBuildsService ...@@ -33,7 +33,6 @@ class CreateCommitBuildsService
unless commit.skip_ci? unless commit.skip_ci?
# Create builds for commit # Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref) tag = Gitlab::Git.tag_ref?(origin_ref)
commit.update_committed!
commit.create_builds(ref, tag, user) commit.create_builds(ref, tag, user)
end end
......
...@@ -12,7 +12,7 @@ class GitPushService < BaseService ...@@ -12,7 +12,7 @@ class GitPushService < BaseService
# 1. Creates the push event # 1. Creates the push event
# 2. Updates merge requests # 2. Updates merge requests
# 3. Recognizes cross-references from commit messages # 3. Recognizes cross-references from commit messages
# 4. Executes the project's web hooks # 4. Executes the project's webhooks
# 5. Executes the project's services # 5. Executes the project's services
# 6. Checks if the project's main language has changed # 6. Checks if the project's main language has changed
# #
...@@ -49,6 +49,8 @@ class GitPushService < BaseService ...@@ -49,6 +49,8 @@ class GitPushService < BaseService
# Update merge requests that may be affected by this push. A new branch # Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change. # could cause the last commit of a merge request to change.
update_merge_requests update_merge_requests
perform_housekeeping
end end
def update_main_language def update_main_language
...@@ -73,6 +75,13 @@ class GitPushService < BaseService ...@@ -73,6 +75,13 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(@project.id) ProjectCacheWorker.perform_async(@project.id)
end end
def perform_housekeeping
housekeeping = Projects::HousekeepingService.new(@project)
housekeeping.increment!
housekeeping.execute if housekeeping.needed?
rescue Projects::HousekeepingService::LeaseTaken
end
def process_default_branch def process_default_branch
@push_commits = project.repository.commits(params[:newrev]) @push_commits = project.repository.commits(params[:newrev])
...@@ -80,7 +89,7 @@ class GitPushService < BaseService ...@@ -80,7 +89,7 @@ class GitPushService < BaseService
project.change_head(branch_name) project.change_head(branch_name)
# Set protection on the default branch if configured # Set protection on the default branch if configured
if (current_application_settings.default_branch_protection != PROTECTION_NONE) if current_application_settings.default_branch_protection != PROTECTION_NONE
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
@project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push }) @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
end end
......
...@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService ...@@ -11,7 +11,10 @@ class IssuableBaseService < BaseService
issuable, issuable.project, current_user, issuable.milestone) issuable, issuable.project, current_user, issuable.milestone)
end end
def create_labels_note(issuable, added_labels, removed_labels) def create_labels_note(issuable, old_labels)
added_labels = issuable.labels - old_labels
removed_labels = old_labels - issuable.labels
SystemNoteService.change_label( SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels) issuable, issuable.project, current_user, added_labels, removed_labels)
end end
...@@ -71,20 +74,19 @@ class IssuableBaseService < BaseService ...@@ -71,20 +74,19 @@ class IssuableBaseService < BaseService
end end
end end
def has_changes?(issuable, options = {}) def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr| attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s) issuable.previous_changes.include?(attr.to_s)
end end
old_labels = options[:old_labels] labels_changed = issuable.labels != old_labels
labels_changed = old_labels && issuable.labels != old_labels
attrs_changed || labels_changed attrs_changed || labels_changed
end end
def handle_common_system_notes(issuable, options = {}) def handle_common_system_notes(issuable, old_labels: [])
if issuable.previous_changes.include?('title') if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first) create_title_change_note(issuable, issuable.previous_changes['title'].first)
end end
...@@ -93,9 +95,6 @@ class IssuableBaseService < BaseService ...@@ -93,9 +95,6 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable) create_task_status_note(issuable)
end end
old_labels = options[:old_labels] create_labels_note(issuable, old_labels) if issuable.labels != old_labels
if old_labels && (issuable.labels != old_labels)
create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
end
end end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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