Commit 0b4981e7 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/master' into knapsack

# Conflicts:
#	.gitlab-ci.yml
parents aebfdcd8 9e256de4
...@@ -170,6 +170,7 @@ rake brakeman: *exec ...@@ -170,6 +170,7 @@ rake brakeman: *exec
rake flog: *exec rake flog: *exec
rake flay: *exec rake flay: *exec
rake db:migrate:reset: *exec rake db:migrate:reset: *exec
license-finder: *exec
bundler:audit: bundler:audit:
stage: test stage: test
......
...@@ -194,7 +194,7 @@ Style/EmptyLines: ...@@ -194,7 +194,7 @@ Style/EmptyLines:
# Keep blank lines around access modifiers. # Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier: Style/EmptyLinesAroundAccessModifier:
Enabled: false Enabled: true
# Keeps track of empty lines around block bodies. # Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody: Style/EmptyLinesAroundBlockBody:
...@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity: ...@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation # Checks for ambiguous operators in the first argument of a method invocation
# without parentheses. # without parentheses.
Lint/AmbiguousOperator: Lint/AmbiguousOperator:
Enabled: false Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method # Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses. # invocation without parentheses.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased) v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues.
- Allow enabling wiki page events from Webhook management UI - Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
- Make EmailsOnPushWorker use Sidekiq mailers queue - Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
- Fix issue todo not remove when leave project !4150 (Long Nguyen) - Fix issue todo not remove when leave project !4150 (Long Nguyen)
...@@ -22,6 +24,7 @@ v 8.9.0 (unreleased) ...@@ -22,6 +24,7 @@ v 8.9.0 (unreleased)
- Fix issues filter when ordering by milestone - Fix issues filter when ordering by milestone
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore - Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
...@@ -34,14 +37,18 @@ v 8.9.0 (unreleased) ...@@ -34,14 +37,18 @@ v 8.9.0 (unreleased)
- Cache project build count in sidebar nav - Cache project build count in sidebar nav
- Reduce number of queries needed to render issue labels in the sidebar - Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects - Improve error handling importing projects
- Remove duplicated notification settings
- Put project Files and Commits tabs under Code tab - Put project Files and Commits tabs under Code tab
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
v 8.8.4 - An indicator is now displayed at the top of the comment field for confidential issues.
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
v 8.8.4 (unreleased) v 8.8.4 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Ensure branch cleanup regardless of whether the GitHub import process succeeds
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references
- Upgrade to jQuery 2
- Remove prev/next buttons on issues and merge requests
v 8.8.3 v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
......
...@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the ...@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the
[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design [free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
(the PNG is 1:1). (the PNG is 1:1).
The current designs can be found in the [`gitlab1.atype` file]. The current designs can be found in the [`gitlab8.atype` file].
### UI development kit ### UI development kit
...@@ -530,4 +530,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -530,4 +530,4 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS 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/ [`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
...@@ -45,9 +45,10 @@ gem 'akismet', '~> 2.0' ...@@ -45,9 +45,10 @@ gem 'akismet', '~> 2.0'
gem 'devise-two-factor', '~> 3.0.0' gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7' gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0' gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# Browser detection # Browser detection
gem "browser", '~> 1.0.0' gem "browser", '~> 2.0.3'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
...@@ -110,7 +111,7 @@ gem 'org-ruby', '~> 0.9.12' ...@@ -110,7 +111,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 1.10.1' gem 'rouge', '~> 1.11'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -143,7 +144,7 @@ gem 'redis-namespace' ...@@ -143,7 +144,7 @@ gem 'redis-namespace'
gem "httparty", '~> 0.13.3' gem "httparty", '~> 0.13.3'
# Colored output to console # Colored output to console
gem "colorize", '~> 0.7.0' gem "rainbow", '~> 2.1.0'
# GitLab settings # GitLab settings
gem 'settingslogic', '~> 2.0.9' gem 'settingslogic', '~> 2.0.9'
...@@ -305,6 +306,8 @@ group :development, :test do ...@@ -305,6 +306,8 @@ group :development, :test do
gem 'bundler-audit', require: false gem 'bundler-audit', require: false
gem 'benchmark-ips', require: false gem 'benchmark-ips', require: false
gem "license_finder", require: false
end end
group :test do group :test do
......
...@@ -92,7 +92,7 @@ GEM ...@@ -92,7 +92,7 @@ GEM
sass (~> 3.0) sass (~> 3.0)
slim (>= 1.3.6, < 4.0) slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4) terminal-table (~> 1.4)
browser (1.0.1) browser (2.0.3)
builder (3.2.2) builder (3.2.2)
bullet (5.0.0) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
...@@ -369,6 +369,12 @@ GEM ...@@ -369,6 +369,12 @@ GEM
actionmailer (>= 3.2) actionmailer (>= 3.2)
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
license_finder (2.1.0)
bundler
httparty
rubyzip
thor
xml-simple
licensee (8.0.0) licensee (8.0.0)
rugged (>= 0.24b) rugged (>= 0.24b)
listen (3.0.5) listen (3.0.5)
...@@ -572,7 +578,7 @@ GEM ...@@ -572,7 +578,7 @@ GEM
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rinku (1.7.3) rinku (1.7.3)
rotp (2.1.2) rotp (2.1.2)
rouge (1.10.1) rouge (1.11.0)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -621,6 +627,7 @@ GEM ...@@ -621,6 +627,7 @@ GEM
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.0)
rufus-scheduler (3.1.10) rufus-scheduler (3.1.10)
rugged (0.24.0) rugged (0.24.0)
safe_yaml (1.0.4) safe_yaml (1.0.4)
...@@ -751,6 +758,7 @@ GEM ...@@ -751,6 +758,7 @@ GEM
simple_oauth (~> 0.1.4) simple_oauth (~> 0.1.4)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1)
uglifier (2.7.2) uglifier (2.7.2)
execjs (>= 0.3.0) execjs (>= 0.3.0)
json (>= 1.8.0) json (>= 1.8.0)
...@@ -792,6 +800,7 @@ GEM ...@@ -792,6 +800,7 @@ GEM
builder builder
expression_parser expression_parser
rinku rinku
xml-simple (1.1.5)
xpath (2.0.0) xpath (2.0.0)
nokogiri (~> 1.3) nokogiri (~> 1.3)
...@@ -818,7 +827,7 @@ DEPENDENCIES ...@@ -818,7 +827,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0) brakeman (~> 3.2.0)
browser (~> 1.0.0) browser (~> 2.0.3)
bullet bullet
bundler-audit bundler-audit
byebug byebug
...@@ -827,7 +836,6 @@ DEPENDENCIES ...@@ -827,7 +836,6 @@ DEPENDENCIES
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0) connection_pool (~> 2.0)
coveralls (~> 0.8.2) coveralls (~> 0.8.2)
creole (~> 0.5.0) creole (~> 0.5.0)
...@@ -880,6 +888,7 @@ DEPENDENCIES ...@@ -880,6 +888,7 @@ DEPENDENCIES
kaminari (~> 0.17.0) kaminari (~> 0.17.0)
knapsack knapsack
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder
licensee (~> 8.0.0) licensee (~> 8.0.0)
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.7) mail_room (~> 0.7)
...@@ -919,6 +928,7 @@ DEPENDENCIES ...@@ -919,6 +928,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rails (= 4.2.6) rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2) raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
...@@ -930,7 +940,7 @@ DEPENDENCIES ...@@ -930,7 +940,7 @@ DEPENDENCIES
request_store (~> 1.3.0) request_store (~> 1.3.0)
rerun (~> 0.11.0) rerun (~> 0.11.0)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 1.10.1) rouge (~> 1.11)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.4.0) rspec-rails (~> 3.4.0)
rspec-retry rspec-retry
...@@ -968,6 +978,7 @@ DEPENDENCIES ...@@ -968,6 +978,7 @@ DEPENDENCIES
thin (~> 1.6.1) thin (~> 1.6.1)
tinder (~> 1.10.0) tinder (~> 1.10.0)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0) underscore-rails (~> 1.8.0)
unf (~> 0.1.4) unf (~> 0.1.4)
...@@ -980,4 +991,4 @@ DEPENDENCIES ...@@ -980,4 +991,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.12.4 1.12.5
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file. # the compiled file.
# #
#= require jquery #= require jquery2
#= require jquery-ui/autocomplete #= require jquery-ui/autocomplete
#= require jquery-ui/datepicker #= require jquery-ui/datepicker
#= require jquery-ui/draggable #= require jquery-ui/draggable
...@@ -56,9 +56,11 @@ ...@@ -56,9 +56,11 @@
#= require_directory ./commit #= require_directory ./commit
#= require_directory ./extensions #= require_directory ./extensions
#= require_directory ./lib #= require_directory ./lib
#= require_directory ./u2f
#= require_directory . #= require_directory .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper #= require cropper
#= require u2f
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
......
...@@ -17,11 +17,13 @@ class Dispatcher ...@@ -17,11 +17,13 @@ class Dispatcher
switch page switch page
when 'projects:issues:index' when 'projects:issues:index'
Issuable.init() Issuable.init()
new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show' when 'projects:issues:show'
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
gl.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index' when 'dashboard:todos:index'
...@@ -52,6 +54,7 @@ class Dispatcher ...@@ -52,6 +54,7 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
gl.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
......
...@@ -21,7 +21,7 @@ class @DueDateSelect ...@@ -21,7 +21,7 @@ class @DueDateSelect
$dropdown.glDropdown( $dropdown.glDropdown(
hidden: -> hidden: ->
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
) )
addDueDate = (isDropdown) -> addDueDate = (isDropdown) ->
...@@ -42,12 +42,13 @@ class @DueDateSelect ...@@ -42,12 +42,13 @@ class @DueDateSelect
type: 'PUT' type: 'PUT'
url: issueUpdateURL url: issueUpdateURL
data: data data: data
dataType: 'json'
beforeSend: -> beforeSend: ->
$loading.fadeIn() $loading.fadeIn()
if isDropdown if isDropdown
$dropdown.trigger('loading.gl.dropdown') $dropdown.trigger('loading.gl.dropdown')
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
$valueContent.html(mediumDate) $valueContent.html(mediumDate)
$sidebarValue.html(mediumDate) $sidebarValue.html(mediumDate)
......
class @Flash class @Flash
constructor: (message, type)-> constructor: (message, type = 'alert')->
@flash = $(".flash-container") @flash = $(".flash-container")
@flash.html("") @flash.html("")
......
...@@ -11,6 +11,8 @@ class GitLabDropdownFilter ...@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent() $inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear') $clearButton = $inputContainer.find('.js-dropdown-input-clear')
@indeterminateIds = []
# Clear click # Clear click
$clearButton.on 'click', (e) => $clearButton.on 'click', (e) =>
e.preventDefault() e.preventDefault()
...@@ -35,20 +37,20 @@ class GitLabDropdownFilter ...@@ -35,20 +37,20 @@ class GitLabDropdownFilter
if keyCode is 13 if keyCode is 13
return false return false
clearTimeout timeout # Only filter asynchronously only if option remote is set
timeout = setTimeout => if @options.remote
blur_field = @shouldBlur keyCode clearTimeout timeout
search_text = @input.val() timeout = setTimeout =>
blur_field = @shouldBlur keyCode
if blur_field and @filterInputBlur if blur_field and @filterInputBlur
@input.blur() @input.blur()
if @options.remote @options.query @input.val(), (data) =>
@options.query search_text, (data) =>
@options.callback(data) @options.callback(data)
else , 250
@filter search_text else
, 250 @filter @input.val()
shouldBlur: (keyCode) -> shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0 return BLUR_KEYCODES.indexOf(keyCode) >= 0
...@@ -142,6 +144,7 @@ class GitLabDropdown ...@@ -142,6 +144,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading" LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two" PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active" ACTIVE_CLASS = "is-active"
INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1 currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field' FILTER_INPUT = '.dropdown-input .dropdown-input-field'
...@@ -182,9 +185,6 @@ class GitLabDropdown ...@@ -182,9 +185,6 @@ class GitLabDropdown
@fullData = data @fullData = data
@parseData @fullData @parseData @fullData
if @options.filterable
@filterInput.trigger 'keyup'
} }
# Init filterable # Init filterable
...@@ -298,6 +298,13 @@ class GitLabDropdown ...@@ -298,6 +298,13 @@ class GitLabDropdown
opened: => opened: =>
@addArrowKeyEvent() @addArrowKeyEvent()
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
@remote.execute() @remote.execute()
...@@ -309,12 +316,18 @@ class GitLabDropdown ...@@ -309,12 +316,18 @@ class GitLabDropdown
hidden: (e) => hidden: (e) =>
@removeArrayKeyEvent() @removeArrayKeyEvent()
$input = @dropdown.find(".dropdown-input-field")
if @options.filterable if @options.filterable
@dropdown $input
.find(".dropdown-input-field")
.blur() .blur()
.val("") .val("")
.trigger("keyup")
# Triggering 'keyup' will re-render the dropdown which is not always required
# specially if we want to keep the state of the dropdown needed for bulk-assignment
if not @options.persistWhenHide
$input.trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
...@@ -358,7 +371,7 @@ class GitLabDropdown ...@@ -358,7 +371,7 @@ class GitLabDropdown
if @options.renderRow if @options.renderRow
# Call the render function # Call the render function
html = @options.renderRow(data) html = @options.renderRow.call(@options, data, @)
else else
if not selected if not selected
value = if @options.id then @options.id(data) else data.id value = if @options.id then @options.id(data) else data.id
...@@ -443,6 +456,17 @@ class GitLabDropdown ...@@ -443,6 +456,17 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else else
selectedObject selectedObject
else if el.hasClass(INDETERMINATE_CLASS)
el.addClass ACTIVE_CLASS
el.removeClass INDETERMINATE_CLASS
if not value?
field.remove()
if not field.length and fieldName
@addInput(fieldName, value)
return selectedObject
else else
if not @options.multiSelect or el.hasClass('dropdown-clear-active') if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
...@@ -459,31 +483,42 @@ class GitLabDropdown ...@@ -459,31 +483,42 @@ class GitLabDropdown
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
if value? if value?
if !field.length and fieldName if !field.length and fieldName
# Create hidden input for form @addInput(fieldName, value)
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
else else
field.val value field.val value
return selectedObject return selectedObject
selectRowAtIndex: (index) -> addInput: (fieldName, value)->
selector = ".dropdown-content li:not(.divider):eq(#{index}) a" # Create hidden input for form
$input = $('<input>').attr('type', 'hidden')
.attr('name', fieldName)
.val(value)
if @options.inputId?
$input.attr('id', @options.inputId)
@dropdown.before $input
selectRowAtIndex: (e, index) ->
selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link # simulate a click on the first link
$(selector, @dropdown).trigger "click" $el = $(selector, @dropdown)
if $el.length
e.preventDefault()
e.stopImmediatePropagation()
$(selector, @dropdown)[0].click()
addArrowKeyEvent: -> addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40] ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field") $input = @dropdown.find(".dropdown-input-field")
selector = '.dropdown-content li:not(.divider)' selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
...@@ -511,8 +546,8 @@ class GitLabDropdown ...@@ -511,8 +546,8 @@ class GitLabDropdown
return false return false
if currentKeyCode is 13 if currentKeyCode is 13 and currentIndex isnt -1
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex @selectRowAtIndex e, currentIndex
removeArrayKeyEvent: -> removeArrayKeyEvent: ->
$('body').off 'keydown' $('body').off 'keydown'
......
class @IssuableBulkActions
constructor: (opts = {}) ->
# Set defaults
{
@container = $('.content')
@form = @getElement('.bulk-update')
@issues = @getElement('.issues-list .issue')
} = opts
@bindEvents()
getElement: (selector) ->
@container.find selector
bindEvents: ->
@form.off('submit').on('submit', @onFormSubmit.bind(@))
onFormSubmit: (e) ->
e.preventDefault()
@submit()
submit: ->
_this = @
xhr = $.ajax
url: @form.attr 'action'
method: @form.attr 'method'
dataType: 'JSON',
data: @getFormDataAsObject()
xhr.done (response, status, xhr) ->
location.reload()
xhr.fail ->
new Flash("Issue update failed")
xhr.always @onFormSubmitAlways.bind(@)
onFormSubmitAlways: ->
@form.find('[type="submit"]').enable()
getSelectedIssues: ->
@issues.has('.selected_issue:checked')
getLabelsFromSelection: ->
labels = []
@getSelectedIssues().map ->
_labels = $(@).data('labels')
if _labels
_labels.map (labelId) ->
labels.push(labelId) if labels.indexOf(labelId) is -1
labels
###*
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
###
getUnmarkedIndeterminedLabels: ->
result = []
labelsToKeep = []
for el in @getElement('.labels-filter .is-indeterminate')
labelsToKeep.push $(el).data('labelId')
for id in @getLabelsFromSelection()
# Only the ones that we are not going to keep
result.push(id) if labelsToKeep.indexOf(id) is -1
result
###*
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
###
getFormDataAsObject: ->
formData =
update:
state_event : @form.find('input[name="update[state_event]"]').val()
assignee_id : @form.find('input[name="update[assignee_id]"]').val()
milestone_id : @form.find('input[name="update[milestone_id]"]').val()
issues_ids : @form.find('input[name="update[issues_ids]"]').val()
add_label_ids : []
remove_label_ids : []
@getLabelsToApply().map (id) ->
formData.update.add_label_ids.push id
@getLabelsToRemove().map (id) ->
formData.update.remove_label_ids.push id
formData
getLabelsToApply: ->
labelIds = []
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
labelIds.push $(label).val() if label
labelIds
###*
* Just an alias of @getUnmarkedIndeterminedLabels
* @return {Array} Array of labels
###
getLabelsToRemove: ->
@getUnmarkedIndeterminedLabels()
class @LabelsSelect class @LabelsSelect
constructor: -> constructor: ->
_this = @
$('.js-label-select').each (i, dropdown) -> $('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
...@@ -196,10 +198,18 @@ class @LabelsSelect ...@@ -196,10 +198,18 @@ class @LabelsSelect
callback data callback data
renderRow: (label) -> renderRow: (label, instance) ->
removesAll = label.id is 0 or not label.id? $li = $('<li>')
$a = $('<a href="#">')
selectedClass = [] selectedClass = []
removesAll = label.id is 0 or not label.id?
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
if $form.find("input[type='hidden']\ if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\ [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length [value='#{this.id(label)}']").length
...@@ -230,13 +240,17 @@ class @LabelsSelect ...@@ -230,13 +240,17 @@ class @LabelsSelect
else else
colorEl = '' colorEl = ''
"<li> # We need to identify which items are actually labels
<a href='#' class='#{selectedClass.join(' ')}'> if label.id
#{colorEl} selectedClass.push('label-item')
#{_.escape(label.title)} $a.attr('data-label-id', label.id)
</a>
</li>" $a.addClass(selectedClass.join(' '))
filterable: true .html("#{colorEl} #{_.escape(label.title)}")
# Return generated html
$li.html($a).prop('outerHTML')
persistWhenHide: $dropdown.data('persistWhenHide')
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
...@@ -280,10 +294,19 @@ class @LabelsSelect ...@@ -280,10 +294,19 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit') else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
saveLabelData() if not $dropdown.hasClass 'js-filter-bulk-update'
saveLabelData()
if $dropdown.hasClass('js-filter-bulk-update')
# If we are persisting state we need the classes
if not @options.persistWhenHide
$dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
multiSelect: $dropdown.hasClass 'js-multiselect' multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) -> clicked: (label) ->
if $dropdown.hasClass('js-filter-bulk-update')
return
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index' isMRIndex = page is 'projects:merge_requests:index'
...@@ -298,4 +321,31 @@ class @LabelsSelect ...@@ -298,4 +321,31 @@ class @LabelsSelect
return return
else else
saveLabelData() saveLabelData()
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
) )
@bindEvents()
bindEvents: ->
$('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
onSelectCheckboxIssue: ->
return if $('.selected_issue:checked').length
# Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
# Also restore button text
$('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
getIndeterminateIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
gl.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
...@@ -83,7 +83,7 @@ class @MilestoneSelect ...@@ -83,7 +83,7 @@ class @MilestoneSelect
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
clicked: (selected) -> clicked: (selected) ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
...@@ -118,7 +118,7 @@ class @MilestoneSelect ...@@ -118,7 +118,7 @@ class @MilestoneSelect
$dropdown.trigger('loaded.gl.dropdown') $dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut() $loading.fadeOut()
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
if data.milestone? if data.milestone?
data.milestone.namespace = _this.currentProject.namespace data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path data.milestone.path = _this.currentProject.path
......
...@@ -162,13 +162,14 @@ class @Notes ...@@ -162,13 +162,14 @@ class @Notes
renderNote: (note) -> renderNote: (note) ->
unless note.valid unless note.valid
if note.award if note.award
flash = new Flash('You have already used this award emoji!', 'alert') flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content') flash.pinTo('.header-content')
return return
if note.award if note.award
awardsHandler.addAwardToEmojiBar(note.note) votesBlock = $('.js-awards-block').eq 0
awardsHandler.scrollToAwards() gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
gl.awardsHandler.scrollToAwards()
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
......
...@@ -156,11 +156,14 @@ class @SearchAutocomplete ...@@ -156,11 +156,14 @@ class @SearchAutocomplete
# No need to enable anything if user is not logged in # No need to enable anything if user is not logged in
return if !gon.current_user_id return if !gon.current_user_id
_this = @ unless @dropdown.hasClass('open')
@loadingSuggestions = false _this = @
@loadingSuggestions = false
@dropdown.addClass('open') @dropdown
@searchInput.removeClass('disabled') .addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled')
onSearchInputKeyDown: => onSearchInputKeyDown: =>
# Saves last length of the entered text # Saves last length of the entered text
...@@ -191,7 +194,7 @@ class @SearchAutocomplete ...@@ -191,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete() @disableAutocomplete()
else else
# We should display the menu only when input is not empty # We should display the menu only when input is not empty
@enableAutocomplete() @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value @wrap.toggleClass 'has-value', !!e.target.value
......
...@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation ...@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@replyWithSelectedText() @replyWithSelectedText()
return false return false
) )
Mousetrap.bind('j', =>
@prevIssue()
return false
)
Mousetrap.bind('k', =>
@nextIssue()
return false
)
Mousetrap.bind('e', => Mousetrap.bind('e', =>
@editIssue() @editIssue()
return false return false
...@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation ...@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
else else
@enabledHelp.push('.hidden-shortcut.issues') @enabledHelp.push('.hidden-shortcut.issues')
prevIssue: ->
$prevBtn = $('.prev-btn')
if not $prevBtn.hasClass('disabled')
Turbolinks.visit($prevBtn.attr('href'))
nextIssue: ->
$nextBtn = $('.next-btn')
if not $nextBtn.hasClass('disabled')
Turbolinks.visit($nextBtn.attr('href'))
replyWithSelectedText: -> replyWithSelectedText: ->
if window.getSelection if window.getSelection
selected = window.getSelection().toString() selected = window.getSelection().toString()
......
# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> authenticated -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FAuthenticate
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@challenges = u2fParams.challenges
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
authenticate: () =>
u2f.sign(@appId, @challenges, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderAuthenticated(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
"inProgress": '#js-authenticate-u2f-in-progress',
"error": '#js-authenticate-u2f-error',
"authenticated": '#js-authenticate-u2f-authenticated'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-login-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@authenticate()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderAuthenticated: (deviceResponse) =>
@renderTemplate('authenticated')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
class @U2FError
constructor: (@errorCode) ->
@httpsDisabled = (window.location.protocol isnt 'https:')
console.error("U2F Error Code: #{@errorCode}")
message: () =>
switch
when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
"U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
"This device has already been registered with us."
else
"There was a problem communicating with your device."
# Register U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> registered -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FRegister
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@registerRequests = u2fParams.register_requests
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
register: () =>
u2f.register(@appId, @registerRequests, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderRegistered(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
"inProgress": '#js-register-u2f-in-progress',
"error": '#js-register-u2f-error',
"registered": '#js-register-u2f-registered'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-setup-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@register()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderRegistered: (deviceResponse) =>
@renderTemplate('registered')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
# Helper class for U2F (universal 2nd factor) device registration and authentication.
class @U2FUtil
@isU2FSupported: ->
if @testMode
true
else
gon.u2f.browser_supports_u2f
@enableTestMode: ->
@testMode = true
<% if Rails.env.test? %>
U2FUtil.enableTestMode();
<% end %>
...@@ -149,7 +149,7 @@ class @UsersSelect ...@@ -149,7 +149,7 @@ class @UsersSelect
hidden: (e) -> hidden: (e) ->
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
clicked: (user) -> clicked: (user) ->
page = $('body').data 'page' page = $('body').data 'page'
......
...@@ -232,9 +232,8 @@ ...@@ -232,9 +232,8 @@
a { a {
padding-left: 25px; padding-left: 25px;
&.is-active { &.is-indeterminate, &.is-active {
&::before { &::before {
content: "\f00c";
position: absolute; position: absolute;
left: 5px; left: 5px;
top: 50%; top: 50%;
...@@ -246,6 +245,14 @@ ...@@ -246,6 +245,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
} }
&.is-indeterminate::before {
content: "\f068";
}
&.is-active::before {
content: "\f00c";
}
} }
} }
......
...@@ -2,18 +2,10 @@ ...@@ -2,18 +2,10 @@
* Generic mixins * Generic mixins
*/ */
@mixin box-shadow($shadow) { @mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
-o-box-shadow: $shadow;
box-shadow: $shadow; box-shadow: $shadow;
} }
@mixin border-radius($radius) { @mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
-o-border-radius: $radius;
border-radius: $radius; border-radius: $radius;
} }
......
...@@ -66,10 +66,6 @@ ...@@ -66,10 +66,6 @@
display: none; display: none;
} }
%ul.notes .note-role, .note-actions {
display: none;
}
.nav-links, .nav-links { .nav-links, .nav-links {
li a { li a {
font-size: 14px; font-size: 14px;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding; padding: $gl-padding $gl-btn-padding 11px;
border-color: $table-border-color; border-color: $table-border-color;
color: $gl-gray; color: $gl-gray;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
......
.awards { .awards {
line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
...@@ -9,8 +7,6 @@ ...@@ -9,8 +7,6 @@
.emoji-menu { .emoji-menu {
position: absolute; position: absolute;
top: 100%;
left: 0;
margin-top: 3px; margin-top: 3px;
z-index: 1000; z-index: 1000;
min-width: 160px; min-width: 160px;
...@@ -23,7 +19,12 @@ ...@@ -23,7 +19,12 @@
opacity: 0; opacity: 0;
transform: scale(.2); transform: scale(.2);
transform-origin: 0 -45px; transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44); transition: .3s cubic-bezier(.87,-.41,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
transform-origin: 100% -45px;
}
&.is-visible { &.is-visible {
pointer-events: all; pointer-events: all;
...@@ -94,6 +95,7 @@ ...@@ -94,6 +95,7 @@
.award-control { .award-control {
margin-right: 5px; margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
line-height: 20px; line-height: 20px;
...@@ -107,7 +109,8 @@ ...@@ -107,7 +109,8 @@
} }
&.is-loading { &.is-loading {
.award-control-icon { .award-control-icon-normal,
.emoji-icon {
display: none; display: none;
} }
......
...@@ -3,12 +3,7 @@ ...@@ -3,12 +3,7 @@
background: #111; background: #111;
color: #fff; color: #fff;
font-family: $monospace_font; font-family: $monospace_font;
white-space: pre; white-space: pre-wrap;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
overflow: auto; overflow: auto;
overflow-y: hidden; overflow-y: hidden;
font-size: 12px; font-size: 12px;
......
...@@ -87,6 +87,39 @@ ...@@ -87,6 +87,39 @@
} }
} }
.md-header .nav-links {
display: flex;
display: -webkit-flex;
flex-flow: row wrap;
-webkit-flex-flow: row wrap;
width: 100%;
.pull-right {
// Flexbox quirk to make sure right-aligned items stay right-aligned.
margin-left: auto;
}
}
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 13px;
@media (max-width: $screen-md-min) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
-webkit-order: 4;
margin: 6px auto;
width: 100%;
}
}
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
background-color: $white-light; background-color: $white-light;
......
...@@ -69,6 +69,10 @@ ul.notes { ...@@ -69,6 +69,10 @@ ul.notes {
.note-edit-form { .note-edit-form {
display: block; display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
} }
} }
...@@ -116,8 +120,41 @@ ul.notes { ...@@ -116,8 +120,41 @@ ul.notes {
} }
} }
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
.award-control {
font-size: 13px;
padding: 2px 5px;
}
}
.note-header { .note-header {
padding-bottom: 3px; padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
} }
} }
...@@ -179,6 +216,8 @@ ul.notes { ...@@ -179,6 +216,8 @@ ul.notes {
.discussion-header, .discussion-header,
.note-header { .note-header {
position: relative;
a { a {
color: inherit; color: inherit;
...@@ -215,6 +254,16 @@ ul.notes { ...@@ -215,6 +254,16 @@ ul.notes {
color: $notes-action-color; color: $notes-action-color;
} }
.note-actions {
position: absolute;
right: 0;
top: 0;
@media (min-width: $screen-sm-min) {
position: relative;
}
}
.discussion-actions { .discussion-actions {
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
float: none; float: none;
...@@ -228,8 +277,13 @@ ul.notes { ...@@ -228,8 +277,13 @@ ul.notes {
.note-action-button { .note-action-button {
display: inline-block; display: inline-block;
margin-left: 10px; margin-left: 0;
line-height: 24px; line-height: 20px;
@media (min-width: $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
.fa { .fa {
color: $notes-action-color; color: $notes-action-color;
......
...@@ -158,13 +158,11 @@ ...@@ -158,13 +158,11 @@
.search-holder { .search-holder {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox;
display: flex; display: flex;
} }
.search-field-holder { .search-field-holder {
-webkit-flex: 1 0 auto; -webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto; flex: 1 0 auto;
position: relative; position: relative;
margin-right: 0; margin-right: 0;
......
...@@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base ...@@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base
end end
def check_2fa_requirement def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path redirect_to profile_two_factor_auth_path
end end
end end
...@@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base ...@@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current session[:skip_tfa] && session[:skip_tfa] > Time.current
end end
def browser_supports_u2f?
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
end
def redirect_to_home_page_url? def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page # If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections # Don't redirect to the default URL to prevent endless redirections
...@@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base ...@@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path current_user.nil? && root_path == request.path
end end
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
def u2f_app_id
request.base_url
end
private private
def set_default_sort def set_default_sort
......
...@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor ...@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
private
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
end
end
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
# Remove any lingering user data from login
session.delete(:otp_user_id)
session.delete(:challenges)
sign_in(user)
else
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
render 'devise/sessions/two_factor' and return if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
challenges = sign_requests.map(&:challenge)
session[:challenges] = challenges
gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
end end
module ToggleAwardEmoji
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, only: [:toggle_award_emoji]
end
def toggle_award_emoji
name = params.require(:name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true }
end
private
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
else
awardable
end
end
def awardable
raise NotImplementedError
end
end
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement skip_before_action :check_2fa_requirement
def new def show
unless current_user.otp_secret unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32) current_user.otp_secret = User.generate_otp_secret(32)
end end
...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed? current_user.save! if current_user.changed?
if two_factor_authentication_required? if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired? if two_factor_grace_period_expired?
flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.'
else else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end end
end end
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
end end
def create def create
if current_user.validate_and_consume_otp!(params[:pin_code]) if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes! @codes = current_user.generate_otp_backup_codes!
current_user.save! current_user.save!
...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else else
@error = 'Invalid pin code' @error = 'Invalid pin code'
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
render 'show'
end
end
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
render 'new' if @u2f_registration.persisted?
session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
render :show
end end
end end
...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host def issuer_host
Gitlab.config.gitlab.host Gitlab.config.gitlab.host
end end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles)
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
...@@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController
private private
def build def build
@build ||= project.builds.unscoped.find_by!(id: params[:id]) @build ||= project.builds.find_by!(id: params[:id])
end end
def build_path(build) def build_path(build)
......
class Projects::IssuesController < Projects::ApplicationController class Projects::IssuesController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include IssuableActions include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
...@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
def show def show
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh @notes = @issue.notes.with_associations.fresh
@noteable = @issue @noteable = @issue
respond_to do |format| respond_to do |format|
...@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController
def bulk_update def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" })
respond_to do |format|
format.json do
render json: { notice: "#{result[:count]} issues updated" }
end
end
end end
protected protected
...@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :issue alias_method :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
alias_method :awardable, :issue
def authorize_read_issue! def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
...@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController
:issues_ids, :issues_ids,
:assignee_id, :assignee_id,
:milestone_id, :milestone_id,
:state_event :state_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
) )
end end
end end
...@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
include DiffHelper include DiffHelper
include IssuableActions include IssuableActions
include ToggleAwardEmoji
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
...@@ -201,7 +202,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -201,7 +202,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
.execute(@merge_request) .execute(@merge_request)
@status = :merge_when_build_succeeds @status = :merge_when_build_succeeds
else else
MergeWorker.perform_async(@merge_request.id, current_user.id, params) MergeWorker.perform_async(@merge_request.id, current_user.id, params)
...@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -270,6 +271,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :merge_request alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
def closes_issues def closes_issues
@closes_issues ||= @merge_request.closes_issues @closes_issues ||= @merge_request.closes_issues
...@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -305,7 +307,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_show_vars def define_show_vars
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)
@notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
@discussions = @notes.discussions @discussions = @notes.discussions
@noteable = @merge_request @noteable = @merge_request
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include ToggleAwardEmoji
# Authorize # Authorize
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] before_action :find_current_user_notes, only: [:index]
def index def index
current_fetched_at = Time.now.to_i current_fetched_at = Time.now.to_i
...@@ -56,35 +58,12 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -56,35 +58,12 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def award_toggle
noteable = if note_params[:noteable_type] == "issue"
project.issues.find(note_params[:noteable_id])
else
project.merge_requests.find(note_params[:noteable_id])
end
data = {
author: current_user,
is_award: true,
note: note_params[:note].delete(":")
}
note = noteable.notes.find_by(data)
if note
note.destroy
else
Notes::CreateService.new(project, current_user, note_params).execute
end
render json: { ok: true }
end
private private
def note def note
@note ||= @project.notes.find(params[:id]) @note ||= @project.notes.find(params[:id])
end end
alias_method :awardable, :note
def note_to_html(note) def note_to_html(note)
render_to_string( render_to_string(
...@@ -131,13 +110,20 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -131,13 +110,20 @@ class Projects::NotesController < Projects::ApplicationController
end end
def note_json(note) def note_json(note)
if note.valid? if note.is_a?(AwardEmoji)
{
valid: note.valid?,
award: true,
id: note.id,
name: note.name
}
elsif note.valid?
{ {
valid: true, valid: true,
id: note.id, id: note.id,
discussion_id: note.discussion_id, discussion_id: note.discussion_id,
html: note_to_html(note), html: note_to_html(note),
award: note.is_award, award: false,
note: note.note, note: note.note,
discussion_html: note_to_discussion_html(note), discussion_html: note_to_discussion_html(note),
discussion_with_diff_html: note_to_discussion_with_diff_html(note) discussion_with_diff_html: note_to_discussion_with_diff_html(note)
...@@ -145,7 +131,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -145,7 +131,7 @@ class Projects::NotesController < Projects::ApplicationController
else else
{ {
valid: false, valid: false,
award: note.is_award, award: false,
errors: note.errors errors: note.errors
} }
end end
......
...@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
emojis: AwardEmoji.urls, emojis: Gitlab::AwardEmoji.urls,
issues: autocomplete.issues, issues: autocomplete.issues,
milestones: autocomplete.milestones, milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
......
...@@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController ...@@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil, resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil) reset_password_sent_at: nil)
end end
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard" log_audit_event(current_user, with: authentication_method)
log_audit_event(current_user, with: authenticated_with)
end end
end end
...@@ -54,7 +53,7 @@ class SessionsController < Devise::SessionsController ...@@ -54,7 +53,7 @@ class SessionsController < Devise::SessionsController
end end
def user_params def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end end
def find_user def find_user
...@@ -89,27 +88,6 @@ class SessionsController < Devise::SessionsController ...@@ -89,27 +88,6 @@ class SessionsController < Devise::SessionsController
find_user.try(:two_factor_enabled?) find_user.try(:two_factor_enabled?)
end end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) and return
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor and return
end
else
if user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
end
def auto_sign_in_with_provider def auto_sign_in_with_provider
provider = Gitlab.config.omniauth.auto_sign_in_with_provider provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present? return unless provider.present?
...@@ -138,4 +116,14 @@ class SessionsController < Devise::SessionsController ...@@ -138,4 +116,14 @@ class SessionsController < Devise::SessionsController
def load_recaptcha def load_recaptcha
Gitlab::Recaptcha.load_configurations! Gitlab::Recaptcha.load_configurations!
end end
def authentication_method
if user_params[:otp_attempt]
"two-factor"
elsif user_params[:device_response]
"two-factor-via-u2f-device"
else
"standard"
end
end
end end
...@@ -12,9 +12,9 @@ class NotesFinder ...@@ -12,9 +12,9 @@ class NotesFinder
when "commit" when "commit"
project.notes.for_commit_id(target_id).non_diff_notes project.notes.for_commit_id(target_id).non_diff_notes
when "issue" when "issue"
project.issues.find(target_id).notes.nonawards.inc_author project.issues.find(target_id).notes.inc_author
when "merge_request" when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet" when "snippet", "project_snippet"
project.snippets.find(target_id).notes project.snippets.find(target_id).notes
else else
......
...@@ -66,7 +66,7 @@ module AuthHelper ...@@ -66,7 +66,7 @@ module AuthHelper
def two_factor_skippable? def two_factor_skippable?
current_application_settings.require_two_factor_authentication && current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled && !current_user.two_factor_enabled? &&
current_application_settings.two_factor_grace_period && current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired? !two_factor_grace_period_expired?
end end
......
...@@ -8,14 +8,6 @@ module IssuablesHelper ...@@ -8,14 +8,6 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end end
def issuables_count(issuable)
base_issuable_scope(issuable).maximum(:iid)
end
def next_issuable_for(issuable)
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
# current_labels may be a string from before # current_labels may be a string from before
if current_labels.is_a?(Array) if current_labels.is_a?(Array)
...@@ -45,10 +37,6 @@ module IssuablesHelper ...@@ -45,10 +37,6 @@ module IssuablesHelper
end end
end end
def prev_issuable_for(issuable)
base_issuable_scope(issuable).where('iid < ?', issuable.iid).first
end
def user_dropdown_label(user_id, default_label) def user_dropdown_label(user_id, default_label)
return default_label if user_id.nil? return default_label if user_id.nil?
return "Unassigned" if user_id == "0" return "Unassigned" if user_id == "0"
...@@ -96,5 +84,4 @@ module IssuablesHelper ...@@ -96,5 +84,4 @@ module IssuablesHelper
issuable.open? ? :opened : :closed issuable.open? ? :opened : :closed
end end
end end
end end
...@@ -145,16 +145,14 @@ module IssuesHelper ...@@ -145,16 +145,14 @@ module IssuesHelper
end end
end end
def emoji_author_list(notes, current_user) def award_user_list(awards, current_user)
list = notes.map do |note| awards.map do |award|
note.author == current_user ? "me" : note.author.name award.user == current_user ? 'me' : award.user.name
end end.join(', ')
list.join(", ")
end end
def note_active_class(notes, current_user) def award_active_class(awards, current_user)
if current_user && notes.pluck(:author_id).include?(current_user.id) if current_user && awards.find { |a| a.user_id == current_user.id }
"active" "active"
else else
"" ""
......
class AwardEmoji < ActiveRecord::Base
DOWNVOTE_NAME = "thumbsdown".freeze
UPVOTE_NAME = "thumbsup".freeze
include Participable
belongs_to :awardable, polymorphic: true
belongs_to :user
validates :awardable, :user, presence: true
validates :name, presence: true, inclusion: { in: Emoji.emojis_names }
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }
participant :user
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
def downvote?
self.name == DOWNVOTE_NAME
end
def upvote?
self.name == UPVOTE_NAME
end
end
module Awardable
extend ActiveSupport::Concern
included do
has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable
participant :award_emoji
end
end
module ClassMethods
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
def order_downvotes_desc
order_votes_desc(AwardEmoji::DOWNVOTE_NAME)
end
def order_votes_desc(emoji_name)
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
awards_table[:awardable_id].eq(awardable_table[:id]).and(
awards_table[:awardable_type].eq(self.name).and(
awards_table[:name].eq(emoji_name)
)
)
).join_sources
joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC")
end
end
def grouped_awards(with_thumbs: true)
awards = award_emoji.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_NAME] ||= []
awards[AwardEmoji::DOWNVOTE_NAME] ||= []
end
awards
end
def downvotes
award_emoji.downvotes.count
end
def upvotes
award_emoji.upvotes.count
end
def emoji_awardable?
true
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
def create_award_emoji(name, current_user)
return unless emoji_awardable?
award_emoji.create(name: name, user: current_user)
end
def remove_award_emoji(name, current_user)
award_emoji.where(name: name, user: current_user).destroy_all
end
def toggle_award_emoji(emoji_name, current_user)
if awarded_emoji?(emoji_name, current_user)
remove_award_emoji(emoji_name, current_user)
else
create_award_emoji(emoji_name, current_user)
end
end
end
...@@ -10,6 +10,7 @@ module Issuable ...@@ -10,6 +10,7 @@ module Issuable
include Mentionable include Mentionable
include Subscribable include Subscribable
include StripAttribute include StripAttribute
include Awardable
included do included do
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
...@@ -115,29 +116,6 @@ module Issuable ...@@ -115,29 +116,6 @@ module Issuable
end end
end end
def order_downvotes_desc
order_votes_desc('thumbsdown')
end
def order_upvotes_desc
order_votes_desc('thumbsup')
end
def order_votes_desc(award_emoji_name)
issuable_table = self.arel_table
note_table = Note.arel_table
join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on(
note_table[:noteable_id].eq(issuable_table[:id]).and(
note_table[:noteable_type].eq(self.name).and(
note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name))
)
)
).join_sources
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
end
def with_label(title, sort = nil) def with_label(title, sort = nil)
if title.is_a?(Array) && title.size > 1 if title.is_a?(Array) && title.size > 1
joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}")
...@@ -179,14 +157,6 @@ module Issuable ...@@ -179,14 +157,6 @@ module Issuable
opened? || reopened? opened? || reopened?
end end
def downvotes
notes.awards.where(note: "thumbsdown").count
end
def upvotes
notes.awards.where(note: "thumbsup").count
end
def user_notes_count def user_notes_count
notes.user.count notes.user.count
end end
......
...@@ -110,6 +110,10 @@ class LegacyDiffNote < Note ...@@ -110,6 +110,10 @@ class LegacyDiffNote < Note
@active @active
end end
def award_emoji_supported?
false
end
private private
def find_diff def find_diff
......
...@@ -3,6 +3,7 @@ class Note < ActiveRecord::Base ...@@ -3,6 +3,7 @@ class Note < ActiveRecord::Base
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Participable include Participable
include Mentionable include Mentionable
include Awardable
default_value_for :system, false default_value_for :system, false
...@@ -21,11 +22,8 @@ class Note < ActiveRecord::Base ...@@ -21,11 +22,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
before_validation :set_award!
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, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award }
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size } validates :attachment, file_size: { maximum: :max_attachment_size }
...@@ -43,8 +41,6 @@ class Note < ActiveRecord::Base ...@@ -43,8 +41,6 @@ class Note < ActiveRecord::Base
mount_uploader :attachment, AttachmentUploader mount_uploader :attachment, AttachmentUploader
# Scopes # Scopes
scope :awards, ->{ where(is_award: true) }
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 :system, ->{ where(system: true) } scope :system, ->{ where(system: true) }
scope :user, ->{ where(system: false) } scope :user, ->{ where(system: false) }
...@@ -109,19 +105,6 @@ class Note < ActiveRecord::Base ...@@ -109,19 +105,6 @@ class Note < ActiveRecord::Base
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
end end
end end
def grouped_awards
notes = {}
awards.select(:note).distinct.map do |note|
notes[note.note] = where(note: note.note)
end
notes["thumbsup"] ||= Note.none
notes["thumbsdown"] ||= Note.none
notes
end
end end
def cross_reference? def cross_reference?
...@@ -205,44 +188,24 @@ class Note < ActiveRecord::Base ...@@ -205,44 +188,24 @@ class Note < ActiveRecord::Base
Event.reset_event_cache_for(self) Event.reset_event_cache_for(self)
end end
def downvote?
is_award && note == "thumbsdown"
end
def upvote?
is_award && note == "thumbsup"
end
def editable? def editable?
!system? && !is_award !system?
end end
def cross_reference_not_visible_for?(user) def cross_reference_not_visible_for?(user)
cross_reference? && referenced_mentionables(user).empty? cross_reference? && referenced_mentionables(user).empty?
end end
# Checks if note is an award added as a comment def award_emoji?
# award_emoji_supported? && contains_emoji_only?
# If note is an award, this method sets is_award to true
# and changes content of the note to award name.
#
# Method is executed as a before_validation callback.
#
def set_award!
return unless awards_supported? && contains_emoji_only?
self.is_award = true
self.note = award_emoji_name
end end
private
def clear_blank_line_code! def clear_blank_line_code!
self.line_code = nil if self.line_code.blank? self.line_code = nil if self.line_code.blank?
end end
def awards_supported? def award_emoji_supported?
(for_issue? || for_merge_request?) && !diff_note? noteable.is_a?(Awardable)
end end
def contains_emoji_only? def contains_emoji_only?
...@@ -251,6 +214,6 @@ class Note < ActiveRecord::Base ...@@ -251,6 +214,6 @@ class Note < ActiveRecord::Base
def award_emoji_name def award_emoji_name
original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
AwardEmoji.normilize_emoji_name(original_name) Gitlab::AwardEmoji.normalize_emoji_name(original_name)
end end
end end
...@@ -83,7 +83,7 @@ class IrkerService < Service ...@@ -83,7 +83,7 @@ class IrkerService < Service
self.channels = recipients.split(/\s+/).map do |recipient| self.channels = recipients.split(/\s+/).map do |recipient|
format_channel(recipient) format_channel(recipient)
end end
channels.reject! &:nil? channels.reject!(&:nil?)
end end
def format_channel(recipient) def format_channel(recipient)
......
# Registration information for U2F (universal 2nd factor) devices, like Yubikeys
class U2fRegistration < ActiveRecord::Base
belongs_to :user
def self.register(user, app_id, json_response, challenges)
u2f = U2F::U2F.new(app_id)
registration = self.new
begin
response = U2F::RegisterResponse.load_from_json(json_response)
registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle,
public_key: registration_data.public_key,
counter: registration_data.counter,
user: user)
rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e
registration.errors.add(:base, e.message)
end
registration
end
def self.authenticate(user, app_id, json_response, challenges)
response = U2F::SignResponse.load_from_json(json_response)
registration = user.u2f_registrations.find_by_key_handle(response.key_handle)
u2f = U2F::U2F.new(app_id)
if registration
u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter)
registration.update(counter: response.counter)
true
end
rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error
false
end
end
...@@ -27,7 +27,6 @@ class User < ActiveRecord::Base ...@@ -27,7 +27,6 @@ class User < ActiveRecord::Base
devise :two_factor_authenticatable, devise :two_factor_authenticatable,
otp_secret_encryption_key: Gitlab::Application.config.secret_key_base otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
alias_attribute :two_factor_enabled, :otp_required_for_login
devise :two_factor_backupable, otp_number_of_backup_codes: 10 devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON serialize :otp_backup_codes, JSON
...@@ -51,6 +50,7 @@ class User < ActiveRecord::Base ...@@ -51,6 +50,7 @@ class User < ActiveRecord::Base
has_many :keys, dependent: :destroy has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy has_many :emails, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
# Groups # Groups
has_many :members, dependent: :destroy has_many :members, dependent: :destroy
...@@ -84,6 +84,7 @@ class User < ActiveRecord::Base ...@@ -84,6 +84,7 @@ class User < ActiveRecord::Base
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
has_many :todos, dependent: :destroy has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy has_many :notification_settings, dependent: :destroy
has_many :award_emoji, as: :awardable, dependent: :destroy
# #
# Validations # Validations
...@@ -174,8 +175,16 @@ class User < ActiveRecord::Base ...@@ -174,8 +175,16 @@ class User < ActiveRecord::Base
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)') }
scope :with_two_factor, -> { where(two_factor_enabled: true) }
scope :without_two_factor, -> { where(two_factor_enabled: false) } def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
end
def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
where("u2f.id IS NULL AND otp_required_for_login = ?", false)
end
# #
# Class methods # Class methods
...@@ -322,14 +331,29 @@ class User < ActiveRecord::Base ...@@ -322,14 +331,29 @@ class User < ActiveRecord::Base
end end
def disable_two_factor! def disable_two_factor!
update_attributes( transaction do
two_factor_enabled: false, update_attributes(
encrypted_otp_secret: nil, otp_required_for_login: false,
encrypted_otp_secret_iv: nil, encrypted_otp_secret: nil,
encrypted_otp_secret_salt: nil, encrypted_otp_secret_iv: nil,
otp_grace_period_started_at: nil, encrypted_otp_secret_salt: nil,
otp_backup_codes: nil otp_grace_period_started_at: nil,
) otp_backup_codes: nil
)
self.u2f_registrations.destroy_all
end
end
def two_factor_enabled?
two_factor_otp_enabled? || two_factor_u2f_enabled?
end
def two_factor_otp_enabled?
self.otp_required_for_login?
end
def two_factor_u2f_enabled?
self.u2f_registrations.exists?
end end
def namespace_uniq def namespace_uniq
......
...@@ -45,6 +45,8 @@ class IssuableBaseService < BaseService ...@@ -45,6 +45,8 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project) unless can?(current_user, ability, project)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids) params.delete(:label_ids)
params.delete(:assignee_id) params.delete(:assignee_id)
end end
...@@ -67,10 +69,34 @@ class IssuableBaseService < BaseService ...@@ -67,10 +69,34 @@ class IssuableBaseService < BaseService
end end
def filter_labels def filter_labels
return if params[:label_ids].to_a.empty? if params[:add_label_ids].present? || params[:remove_label_ids].present?
params.delete(:label_ids)
filter_labels_in_param(:add_label_ids)
filter_labels_in_param(:remove_label_ids)
else
filter_labels_in_param(:label_ids)
end
end
def filter_labels_in_param(key)
return if params[key].to_a.empty?
params[:label_ids] = params[key] = project.labels.where(id: params[key]).pluck(:id)
project.labels.where(id: params[:label_ids]).pluck(:id) end
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
issuable.label_ids |= add_label_ids if add_label_ids
issuable.label_ids -= remove_label_ids if remove_label_ids
issuable.assign_attributes(attributes.merge(updated_by: current_user))
issuable.save
end
end end
def update(issuable) def update(issuable)
...@@ -78,7 +104,7 @@ class IssuableBaseService < BaseService ...@@ -78,7 +104,7 @@ class IssuableBaseService < BaseService
filter_params filter_params
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels) handle_common_system_notes(issuable, old_labels: old_labels)
handle_changes(issuable, old_labels: old_labels) handle_changes(issuable, old_labels: old_labels)
......
...@@ -4,9 +4,9 @@ module Issues ...@@ -4,9 +4,9 @@ module Issues
issues_ids = params.delete(:issues_ids).split(",") issues_ids = params.delete(:issues_ids).split(",")
issue_params = params issue_params = params
issue_params.delete(:state_event) unless issue_params[:state_event].present? %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key|
issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? issue_params.delete(key) unless issue_params[key].present?
issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? end
issues = Issue.where(id: issues_ids) issues = Issue.where(id: issues_ids)
issues.each do |issue| issues.each do |issue|
......
...@@ -24,6 +24,7 @@ module Issues ...@@ -24,6 +24,7 @@ module Issues
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes rewrite_notes
rewrite_award_emoji
add_note_moved_from add_note_moved_from
# Old issue tasks # Old issue tasks
...@@ -72,6 +73,14 @@ module Issues ...@@ -72,6 +73,14 @@ module Issues
end end
end end
def rewrite_award_emoji
@old_issue.award_emoji.each do |award|
new_award = award.dup
new_award.awardable = @new_issue
new_award.save
end
end
def rewrite_content(content) def rewrite_content(content)
return unless content return unless content
......
...@@ -5,6 +5,13 @@ module Notes ...@@ -5,6 +5,13 @@ module Notes
note.author = current_user note.author = current_user
note.system = false note.system = false
if note.award_emoji?
noteable = note.noteable
todo_service.new_award_emoji(noteable, current_user)
return noteable.create_award_emoji(note.award_emoji_name, current_user)
end
if note.save if note.save
# Finish the harder work in the background # Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params) NewNoteWorker.perform_in(2.seconds, note.id, params)
......
...@@ -8,7 +8,7 @@ module Notes ...@@ -8,7 +8,7 @@ module Notes
def execute def execute
# Skip system notes, like status changes and cross-references and awards # Skip system notes, like status changes and cross-references and awards
unless @note.system || @note.is_award unless @note.system?
EventCreateService.new.leave_note(@note, @note.author) EventCreateService.new.leave_note(@note, @note.author)
@note.create_cross_references! @note.create_cross_references!
execute_note_hooks execute_note_hooks
......
...@@ -130,8 +130,7 @@ class NotificationService ...@@ -130,8 +130,7 @@ class NotificationService
# ignore gitlab service messages # ignore gitlab service messages
return true if note.note.start_with?('Status changed to closed') return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true return true if note.cross_reference? && note.system?
return true if note.is_award
target = note.noteable target = note.noteable
......
...@@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService ...@@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService
end end
protected protected
# True if the token's scope is a superset of required scopes, # True if the token's scope is a superset of required scopes,
# or the required scopes is empty. # or the required scopes is empty.
def sufficient_scope?(token, scopes) def sufficient_scope?(token, scopes)
......
...@@ -122,6 +122,14 @@ class TodoService ...@@ -122,6 +122,14 @@ class TodoService
handle_note(note, current_user) handle_note(note, current_user)
end end
# When an emoji is awarded we should:
#
# * mark all pending todos related to the awardable for the current user as done
#
def new_award_emoji(awardable, current_user)
mark_pending_todos_as_done(awardable, current_user)
end
# When marking pending todos as done we should: # When marking pending todos as done we should:
# #
# * mark all pending todos related to the target for the current user as done # * mark all pending todos related to the target for the current user as done
......
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= awards.count
- if current_user
:javascript
gl.awardMenuUrl = "#{emojis_path}"
.award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{ type: "button" }
= icon('smile-o', class: "award-control-icon award-control-icon-normal")
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
%span.award-control-text
Add
%div %div
.login-box .login-box
.login-heading .login-heading
%h3 Two-factor Authentication %h3 Two-Factor Authentication
.login-body .login-body
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - if @user.two_factor_otp_enabled?
= f.hidden_field :remember_me, value: params[resource_name][:remember_me] %h5 Authenticate via Two-Factor App
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. = f.hidden_field :remember_me, value: params[resource_name][:remember_me]
.prepend-top-20 = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
= f.submit "Verify code", class: "btn btn-save" %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled?
%hr
= render "u2f/authenticate"
.emoji-menu .emoji-menu
.emoji-menu-content .emoji-menu-content
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
- AwardEmoji.emoji_by_category.each do |category, emojis| - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title %h5.emoji-menu-title
= AwardEmoji::CATEGORIES[category] = Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list %ul.clearfix.emoji-menu-list
- emojis.each do |emoji| - emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item %li.pull-left.text-center.emoji-menu-list-item
......
.event-title .event-title
%span.author_name= link_to_author event %span.author_name= link_to_author event
%span.event_label{class: event.action_name} %span.event_label{class: event.action_name}
= event_action_name(event)
- if event.target - if event.target
%strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title = event.action_name
%strong
= link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do
= event.target_type.titleize.downcase
= event.target.reference_link_text
- else
= event_action_name(event)
= event_preposition(event) = event_preposition(event)
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
%td Show/hide this dialog %td Show/hide this dialog
%tr %tr
%td.shortcut %td.shortcut
- if browser.mac? - if browser.platform.mac?
.key &#8984; shift p .key &#8984; shift p
- else - else
.key ctrl shift p .key ctrl shift p
......
...@@ -35,8 +35,6 @@ ...@@ -35,8 +35,6 @@
= csrf_meta_tags = csrf_meta_tags
= include_gon
- unless browser.safari? - unless browser.safari?
%meta{name: 'referrer', content: 'origin-when-cross-origin'} %meta{name: 'referrer', content: 'origin-when-cross-origin'}
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body{class: "#{user_application_theme}", 'data-page' => body_data_page} %body{class: "#{user_application_theme}", 'data-page' => body_data_page}
= Gon::Base.render_data
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top = yield :scripts_body_top
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body.ui_charcoal.login-page.application.navless %body.ui_charcoal.login-page.application.navless
= Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
= render "layouts/broadcast" = render "layouts/broadcast"
.container.navless-container .container.navless-container
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body.ui_charcoal.login-page.application.navless %body.ui_charcoal.login-page.application.navless
= Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
= render "layouts/broadcast" = render "layouts/broadcast"
.container.navless-container .container.navless-container
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
%html{ lang: "en"} %html{ lang: "en"}
= render "layouts/head" = render "layouts/head"
%body{class: "#{user_application_theme} application navless"} %body{class: "#{user_application_theme} application navless"}
= Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
.container.navless-container .container.navless-container
= render "layouts/flash" = render "layouts/flash"
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
= icon('hdd-o fw') = icon('hdd-o fw')
%span %span
Container Registry Registry
- if project_nav_tab? :graphs - if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do = nav_link(controller: %w(graphs)) do
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%p %p
Your private token is used to access application resources without authentication. Your private token is used to access application resources without authentication.
.col-lg-9 .col-lg-9
= form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f| = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f|
%p.cgray %p.cgray
- if current_user.private_token - if current_user.private_token
= label_tag "token", "Private token", class: "label-light" = label_tag "token", "Private token", class: "label-light"
...@@ -29,21 +29,22 @@ ...@@ -29,21 +29,22 @@
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Two-factor Authentication Two-Factor Authentication
%p %p
Increase your account's security by enabling two-factor authentication (2FA). Increase your account's security by enabling Two-Factor Authentication (2FA).
.col-lg-9 .col-lg-9
%p %p
Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if !current_user.two_factor_enabled? - if current_user.two_factor_enabled?
%p = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info'
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. = link_to 'Disable', profile_two_factor_auth_path,
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. method: :delete,
.append-bottom-10 data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." },
= link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' class: 'btn btn-danger'
- else - else
= link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', .append-bottom-10
data: { confirm: 'Are you sure?' } = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success'
%hr %hr
- if button_based_providers.any? - if button_based_providers.any?
.row.prepend-top-default .row.prepend-top-default
......
- page_title 'Two-factor Authentication', 'Account'
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Two-factor Authentication (2FA)
%p
Increase your account's security by enabling two-factor authentication (2FA).
.col-lg-9
%p
Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
= raw @qr_code
.col-md-9
.account-well
%p.prepend-top-0.append-bottom-0
Can't scan the code?
%p.prepend-top-0.append-bottom-0
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
= current_user.email
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
%p.two-factor-new-manual-content
Time based: Yes
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
.alert.alert-danger
= @error
.form-group
= label_tag :pin_code, nil, class: "label-light"
= text_field_tag :pin_code, nil, class: "form-control", required: true
.prepend-top-default
= submit_tag 'Enable two-factor authentication', class: 'btn btn-success'
= link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
- page_title 'Two-Factor Authentication', 'Account'
- header_title "Two-Factor Authentication", profile_two_factor_auth_path
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Register Two-Factor Authentication App
%p
Use an app on your mobile device to enable two-factor authentication (2FA).
.col-lg-9
- if current_user.two_factor_otp_enabled?
= icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
= raw @qr_code
.col-md-9
.account-well
%p.prepend-top-0.append-bottom-0
Can't scan the code?
%p.prepend-top-0.append-bottom-0
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
= current_user.email
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')
%p.two-factor-new-manual-content
Time based: Yes
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
.alert.alert-danger
= @error
.form-group
= label_tag :pin_code, nil, class: "label-light"
= text_field_tag :pin_code, nil, class: "form-control", required: true
.prepend-top-default
= submit_tag 'Register with Two-Factor App', class: 'btn btn-success'
%hr
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
Register Universal Two-Factor (U2F) Device
%p
Use a hardware device to add the second factor of authentication.
%p
As U2F devices are only supported by a few browsers, it's recommended that you set up a
two-factor authentication app as well as a U2F device so you'll always be able to log in
using an unsupported browser.
.col-lg-9
%p
- if @registration_key_handles.present?
= icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
- if two_factor_skippable?
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
$(".flash-alert").append(button);
...@@ -7,6 +7,12 @@ ...@@ -7,6 +7,12 @@
%li %li
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview Preview
- if defined?(@issue) && @issue.confidential?
%li.confidential-issue-warning
= icon('warning')
%span This is a confidential issue. Your comment will not be visible to the public.
%li.pull-right %li.pull-right
%button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 }
Go full screen Go full screen
......
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
.issue-check .issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = issue.notes.user.nonawards.count - note_count = issue.notes.user.count
%li %li
= link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
.content-block.content-block-small .content-block.content-block-small
= render 'new_branch' = render 'new_branch'
= render 'votes/votes_block', votable: @issue = render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion %section.issuable-discussion
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
......
%li{id: dom_id(label)} %li{ id: dom_id(label), data: { id: label.id } }
= render "shared/label_row", label: label = render "shared/label_row", label: label
.pull-info-right .pull-info-right
%span.append-right-20 %span.append-right-20
= link_to_label(label, type: :merge_request) do = link_to_label(label, type: :merge_request) do
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
= icon('thumbs-down') = icon('thumbs-down')
= downvotes = downvotes
- note_count = merge_request.mr_and_commit_notes.user.nonawards.count - note_count = merge_request.mr_and_commit_notes.user.count
%li %li
= link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do
= icon('comments') = icon('comments')
......
...@@ -6,4 +6,3 @@ ...@@ -6,4 +6,3 @@
- if @merge_requests.present? - if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
%li.notes-tab %li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.nonawards.count %span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab %li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits Commits
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
.tab-content .tab-content
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
.content-block.content-block-small.oneline-block .content-block.content-block-small.oneline-block
= render 'votes/votes_block', votable: @merge_request = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row .row
%section.col-md-12 %section.col-md-12
......
...@@ -19,20 +19,24 @@ ...@@ -19,20 +19,24 @@
.note-actions .note-actions
- access = note.project.team.human_max_access(note.author.id) - access = note.project.team.human_max_access(note.author.id)
- if access - if access
%span.note-role %span.note-role.hidden-xs= access
= access
- if note_editable - if note_editable
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
= icon('smile-o')
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
= icon('trash-o') = icon('trash-o')
.note-body{class: note_editable ? 'js-task-list-container' : ''} .note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text .note-text
= preserve do = preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable - if note_editable
= render 'projects/notes/edit_form', note: note = render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) .note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.attachment.url - if note.attachment.url
.note-attachment .note-attachment
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
.wiki-holder.prepend-top-default .wiki-holder.prepend-top-default.append-bottom-default
.wiki .wiki
= preserve do = preserve do
= render_wiki_content(@page) = render_wiki_content(@page)
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
- if controller.controller_name == 'issues' - if controller.controller_name == 'issues'
.issues_bulk_update.hide .issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do
.filter-item.inline .filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul %ul
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
= hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event] = hidden_field_tag :state_event, params[:state_event]
.filter-item.inline .filter-item.inline
......
- show_create = local_assigns.fetch(:show_create, true)
- extra_options = local_assigns.fetch(:extra_options, true)
- filter_submit = local_assigns.fetch(:filter_submit, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
- if params[:label_name].present? - if params[:label_name].present?
- if params[:label_name].respond_to?('any?') - if params[:label_name].respond_to?('any?')
- params[:label_name].each do |label| - params[:label_name].each do |label|
= hidden_field_tag "label_name[]", label, id: nil = hidden_field_tag "label_name[]", label, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
%span.dropdown-toggle-text %span.dropdown-toggle-text
= h(multi_label_name(params[:label_name], "Label")) = h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
- if can? current_user, :admin_label, @project and @project - if show_create and @project and can?(current_user, :admin_label, @project)
= render partial: "shared/issuable/label_page_create" = render partial: "shared/issuable/label_page_create"
= dropdown_loading = dropdown_loading
- title = local_assigns.fetch(:title, 'Assign labels') - title = local_assigns.fetch(:title, 'Assign labels')
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels')
.dropdown-page-one .dropdown-page-one
= dropdown_title(title) = dropdown_title(title)
= dropdown_filter(filter_placeholder) = dropdown_filter(filter_placeholder)
= dropdown_content = dropdown_content
- if @project - if @project && show_footer
= dropdown_footer do = dropdown_footer do
%ul.dropdown-footer-list %ul.dropdown-footer-list
- if can? current_user, :admin_label, @project - if can?(current_user, :admin_label, @project)
%li %li
%a.dropdown-toggle-page{href: "#"} %a.dropdown-toggle-page{href: "#"}
Create new Create new
%li %li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
- if can? current_user, :admin_label, @project - if show_create && @project && can?(current_user, :admin_label, @project)
Manage labels Manage labels
- else - else
View labels View labels
= dropdown_loading = dropdown_loading
\ No newline at end of file
...@@ -2,23 +2,8 @@ ...@@ -2,23 +2,8 @@
.issuable-sidebar .issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header .block.issuable-sidebar-header
%span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
%a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'} %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager'
- else
%a.btn.btn-default.issuable-pager.disabled{href: '#'}
Prev
- if next_issuable = next_issuable_for(issuable)
= link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager'
- else
%a.btn.btn-default.issuable-pager.disabled{href: '#'}
Next
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee .block.assignee
......
...@@ -6,7 +6,11 @@ ...@@ -6,7 +6,11 @@
%ul.well-list %ul.well-list
- @query.application_backtrace.each do |location| - @query.application_backtrace.each do |location|
%li %li
= location.path %strong
- if defined?(BetterErrors)
= link_to(location.path, BetterErrors.editor[location.path, location.line])
- else
= location.path
%small.light %small.light
= t('sherlock.line') = t('sherlock.line')
= location.line = location.line
......
...@@ -11,13 +11,17 @@ ...@@ -11,13 +11,17 @@
= @query.duration.round(4) = @query.duration.round(4)
= t('sherlock.milliseconds') = t('sherlock.milliseconds')
%li %li
- frame = @query.last_application_frame
%span.light %span.light
#{t('sherlock.origin')}: #{t('sherlock.origin')}:
%strong %strong
= @query.last_application_frame.path - if defined?(BetterErrors)
= link_to(frame.path, BetterErrors.editor[frame.path, frame.line])
- else
= frame.path
%small.light %small.light
= t('sherlock.line') = t('sherlock.line')
= @query.last_application_frame.line = frame.line
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
......
#js-authenticate-u2f
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-authenticate-u2f-setup{ type: "text/template" }
%div
%p Insert your security key (if you haven't already), and press the button below.
%a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device
%script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-authenticate-u2f-error{ type: "text/template" }
%div
%p <%= error_message %>
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-authenticate-u2f-authenticated{ type: "text/template" }
%div
%p We heard back from your U2F device. Click this button to authenticate with the GitLab server.
= form_tag(new_user_session_path, method: :post) do |f|
= hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Authenticate via U2F Device", class: "btn btn-success"
:javascript
var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f);
u2fAuthenticate.start();
#js-register-u2f
%script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-register-u2f-setup{ type: "text/template" }
.row.append-bottom-10
.col-md-3
%a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device
.col-md-9
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
%script#js-register-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
%script#js-register-u2f-error{ type: "text/template" }
%div
%p
%span <%= error_message %>
%a.btn.btn-warning#js-u2f-try-again Try again?
%script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10
%p Your device was successfully set up! Click this button to register with the GitLab server.
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
= hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Register U2F Device", class: "btn btn-success"
:javascript
var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
u2fRegister.start();
.awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
%button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}}
= emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter
= notes.count
- if current_user
%div.award-menu-holder.js-award-holder
%a.btn.award-control.js-add-award{"href" => "#"}
= icon('smile-o', {class: "award-control-icon"})
= icon('spinner spin', {class: "award-control-icon award-control-icon-loading"})
%span.award-control-text
Add
- if current_user
:javascript
var getEmojisUrl = "#{emojis_path}";
var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
var noteableType = "#{votable.class.name.underscore}";
var noteableId = "#{votable.id}";
var unicodes = #{AwardEmoji.unicode.to_json};
window.awardsHandler = new AwardsHandler(
getEmojisUrl,
postEmojiUrl,
noteableType,
noteableId,
unicodes
);
---
# IGNORED GROUPS AND GEMS
- - :ignore_group
- development
- :who: Connor Shea
:why: Development gems are not distributed with the final product and are therefore exempt.
:versions: []
:when: 2016-04-17 21:27:01.054140000 Z
- - :ignore_group
- test
- :who: Connor Shea
:why: Test gems are not distributed with the final product and are therefore exempt.
:versions: []
:when: 2016-04-17 21:27:06.250326000 Z
- - :ignore
- bundler
- :who: Connor Shea
:why: Bundler is MIT licensed but will sometimes fail in CI.
:versions: []
:when: 2016-05-02 06:42:08.045090000 Z
# LICENSE WHITELIST
- - :whitelist
- MIT
- :who: Connor Shea
:why: http://choosealicense.com/licenses/mit/
:versions: []
:when: 2016-04-17 21:12:24.558441000 Z
- - :whitelist
- Apache 2.0
- :who: Connor Shea
:why: http://choosealicense.com/licenses/apache-2.0/
:versions: []
:when: 2016-05-02 05:27:43.762702000 Z
- - :whitelist
- ruby
- :who: Connor Shea
:why: https://github.com/ruby/ruby/blob/ruby_2_1/COPYING
:versions: []
:when: 2016-05-02 05:31:54.498490000 Z
- - :whitelist
- LGPL
- :who: Connor Shea
:why: http://www.gnu.org/licenses/license-list.html#LGPLv2.1
:versions: []
:when: 2016-05-02 05:32:48.645841000 Z
- - :whitelist
- ISC
- :who: Connor Shea
:why: http://www.gnu.org/licenses/license-list.html#ISC
:versions: []
:when: 2016-05-02 05:42:01.894452000 Z
- - :whitelist
- New BSD
- :who: Connor Shea
:why: https://opensource.org/licenses/BSD-3-Clause
:versions: []
:when: 2016-05-02 05:44:38.246021000 Z
- - :whitelist
- LGPL-2.1+
- :who: Connor Shea
:why: Equivalent to LGPL.
:versions: []
:when: 2016-05-02 05:52:56.303239000 Z
- - :whitelist
- BSD
- :who: Connor Shea
:why: https://opensource.org/licenses/BSD-2-Clause
:versions: []
:when: 2016-05-02 05:55:09.796363000 Z
# LICENSE BLACKLIST
- - :blacklist
- GPLv2
- :who: Connor Shea
:why: GPL-licensed libraries cannot be linked to from non-GPL projects.
:versions: []
:when: 2016-05-02 05:29:27.637336000 Z
- - :blacklist
- GPLv3
- :who: Connor Shea
:why: GPL-licensed libraries cannot be linked to from non-GPL projects.
:versions: []
:when: 2016-05-02 05:29:43.904715000 Z
# GEM LICENSES
- - :license
- raphael-rails
- MIT
- :who: Connor Shea
:why: https://github.com/mockdeep/raphael-rails/blob/master/license.txt
:versions: []
:when: 2016-04-17 21:30:07.575392000 Z
- - :license
- rouge
- MIT
- :who: Connor Shea
:why: https://github.com/jneen/rouge/blob/master/LICENSE
:versions: []
:when: 2016-04-17 21:31:29.490394000 Z
- - :license
- pyu-ruby-sasl
- MIT
- :who: Connor Shea
:why: https://github.com/pyu10055/ruby-sasl/blob/master/MIT-LICENSE
:versions: []
:when: 2016-04-17 21:41:55.266420000 Z
- - :license
- six
- MIT
- :who: Connor Shea
:why: https://github.com/randx/six/blob/master/LICENSE
:versions: []
:when: 2016-04-17 21:42:31.420186000 Z
- - :license
- rdoc
- ruby
- :who: Connor Shea
:why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc
:versions: []
:when: 2016-04-17 21:43:30.480413000 Z
- - :license
- expression_parser
- MIT
- :who: Connor Shea
:why: https://github.com/nricciar/expression_parser/blob/master/MIT-LICENSE
:versions: []
:when: 2016-04-17 21:45:41.829912000 Z
- - :license
- creole
- ruby
- :who: Connor Shea
:why: https://github.com/minad/creole#license
:versions: []
:when: 2016-04-17 21:49:10.329759000 Z
- - :license
- eventmachine
- ruby
- :who: Connor Shea
:why: https://github.com/eventmachine/eventmachine/blob/master/LICENSE
:versions: []
:when: 2016-04-17 21:49:10.329759001 Z
- - :license
- unicorn
- ruby
- :who: Connor Shea
:why: http://unicorn.bogomips.org/LICENSE.html
:versions: []
:when: 2016-05-02 05:45:28.817510000 Z
- - :license
- unicorn-worker-killer
- ruby
- :who: Connor Shea
:why: https://github.com/kzk/unicorn-worker-killer/blob/master/LICENSE
:versions: []
:when: 2016-05-02 05:45:38.323867000 Z
- - :license
- json
- ruby
- :who: Connor Shea
:why: https://github.com/flori/json/tree/master#license
:versions: []
:when: 2016-05-02 05:50:07.826564000 Z
- - :license
- unf
- BSD
- :who: Connor Shea
:why: https://github.com/knu/ruby-unf/blob/master/LICENSE
:versions: []
:when: 2016-05-02 05:51:46.886872000 Z
- - :license
- rubypants
- BSD
- :who: Connor Shea
:why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc
:versions: []
:when: 2016-05-02 05:56:50.696858000 Z
...@@ -8,3 +8,7 @@ ...@@ -8,3 +8,7 @@
# inflect.irregular 'person', 'people' # inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep ) # inflect.uncountable %w( fish sheep )
# end # end
#
ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(award_emoji)
end
---
decisions_file: './config/dependency_decisions.yml'
...@@ -343,8 +343,9 @@ Rails.application.routes.draw do ...@@ -343,8 +343,9 @@ Rails.application.routes.draw do
resources :keys resources :keys
resources :emails, only: [:index, :create, :destroy] resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy] resource :avatar, only: [:destroy]
resource :two_factor_auth, only: [:new, :create, :destroy] do resource :two_factor_auth, only: [:show, :create, :destroy] do
member do member do
post :create_u2f
post :codes post :codes
patch :skip patch :skip
end end
...@@ -652,6 +653,7 @@ Rails.application.routes.draw do ...@@ -652,6 +653,7 @@ Rails.application.routes.draw do
post :cancel_merge_when_build_succeeds post :cancel_merge_when_build_succeeds
get :ci_status get :ci_status
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji
post :remove_wip post :remove_wip
end end
...@@ -727,6 +729,7 @@ Rails.application.routes.draw do ...@@ -727,6 +729,7 @@ Rails.application.routes.draw do
resources :issues, constraints: { id: /\d+/ } do resources :issues, constraints: { id: /\d+/ } do
member do member do
post :toggle_subscription post :toggle_subscription
post :toggle_award_emoji
get :referenced_merge_requests get :referenced_merge_requests
get :related_branches get :related_branches
get :can_create_branch get :can_create_branch
...@@ -755,12 +758,9 @@ Rails.application.routes.draw do ...@@ -755,12 +758,9 @@ Rails.application.routes.draw do
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
member do member do
post :toggle_award_emoji
delete :delete_attachment delete :delete_attachment
end end
collection do
post :award_toggle
end
end end
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
......
...@@ -16,21 +16,21 @@ user = User.new(user_args) ...@@ -16,21 +16,21 @@ user = User.new(user_args)
user.skip_confirmation! user.skip_confirmation!
if user.save if user.save
puts "Administrator account created:".green puts "Administrator account created:".color(:green)
puts puts
puts "login: root".green puts "login: root".color(:green)
if user_args.key?(:password) if user_args.key?(:password)
puts "password: #{user_args[:password]}".green puts "password: #{user_args[:password]}".color(:green)
else else
puts "password: You'll be prompted to create one on your first visit.".green puts "password: You'll be prompted to create one on your first visit.".color(:green)
end end
puts puts
else else
puts "Could not create the default administrator account:".red puts "Could not create the default administrator account:".color(:red)
puts puts
user.errors.full_messages.map do |message| user.errors.full_messages.map do |message|
puts "--> #{message}".red puts "--> #{message}".color(:red)
end end
puts puts
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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