mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-11 13:46:11 +10:00
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into git-http-controller
Conflicts: lib/gitlab/workhorse.rb
This commit is contained in:
+167
-197
@@ -2,7 +2,7 @@ image: "ruby:2.1"
|
||||
|
||||
services:
|
||||
- mysql:latest
|
||||
- redis:latest
|
||||
- redis:alpine
|
||||
|
||||
cache:
|
||||
key: "ruby21"
|
||||
@@ -13,100 +13,187 @@ variables:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
|
||||
# retry tests only in CI environment
|
||||
RSPEC_RETRY_RETRY_COUNT: "3"
|
||||
RAILS_ENV: "test"
|
||||
SIMPLECOV: "true"
|
||||
USE_DB: "true"
|
||||
USE_BUNDLE_INSTALL: "true"
|
||||
|
||||
before_script:
|
||||
- source ./scripts/prepare_build.sh
|
||||
- ruby -v
|
||||
- which ruby
|
||||
- retry gem install bundler --no-ri --no-rdoc
|
||||
- cp config/gitlab.yml.example config/gitlab.yml
|
||||
- touch log/application.log
|
||||
- touch log/test.log
|
||||
- retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
|
||||
- RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate
|
||||
- bundle --version
|
||||
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
|
||||
- retry gem install knapsack
|
||||
- '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- test
|
||||
- notifications
|
||||
- post-test
|
||||
|
||||
spec:feature:
|
||||
# Prepare and merge knapsack tests
|
||||
|
||||
.knapsack-state: &knapsack-state
|
||||
services: []
|
||||
variables:
|
||||
USE_DB: "false"
|
||||
USE_BUNDLE_INSTALL: "false"
|
||||
cache:
|
||||
key: "knapsack"
|
||||
paths:
|
||||
- knapsack/
|
||||
artifacts:
|
||||
paths:
|
||||
- knapsack/
|
||||
|
||||
knapsack:
|
||||
<<: *knapsack-state
|
||||
stage: prepare
|
||||
script:
|
||||
- mkdir -p knapsack/
|
||||
- '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
|
||||
- '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
|
||||
|
||||
update-knapsack:
|
||||
<<: *knapsack-state
|
||||
stage: post-test
|
||||
script:
|
||||
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
|
||||
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
|
||||
- rm -f knapsack/*_node_*.json
|
||||
|
||||
# Execute all testing suites
|
||||
|
||||
.rspec-knapsack: &rspec-knapsack
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
|
||||
- bundle exec rake assets:precompile 2>/dev/null
|
||||
- JOB_NAME=( $CI_BUILD_NAME )
|
||||
- export CI_NODE_INDEX=${JOB_NAME[1]}
|
||||
- export CI_NODE_TOTAL=${JOB_NAME[2]}
|
||||
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
||||
- export KNAPSACK_GENERATE_REPORT=true
|
||||
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
|
||||
- knapsack rspec
|
||||
artifacts:
|
||||
paths:
|
||||
- knapsack/
|
||||
|
||||
spec:api:
|
||||
.spinach-knapsack: &spinach-knapsack
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
|
||||
- bundle exec rake assets:precompile 2>/dev/null
|
||||
- JOB_NAME=( $CI_BUILD_NAME )
|
||||
- export CI_NODE_INDEX=${JOB_NAME[1]}
|
||||
- export CI_NODE_TOTAL=${JOB_NAME[2]}
|
||||
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
|
||||
- export KNAPSACK_GENERATE_REPORT=true
|
||||
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
|
||||
- knapsack spinach "-r rerun"
|
||||
# retry failed tests 3 times
|
||||
- retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
|
||||
artifacts:
|
||||
paths:
|
||||
- knapsack/
|
||||
|
||||
spec:models:
|
||||
rspec 0 20: *rspec-knapsack
|
||||
rspec 1 20: *rspec-knapsack
|
||||
rspec 2 20: *rspec-knapsack
|
||||
rspec 3 20: *rspec-knapsack
|
||||
rspec 4 20: *rspec-knapsack
|
||||
rspec 5 20: *rspec-knapsack
|
||||
rspec 6 20: *rspec-knapsack
|
||||
rspec 7 20: *rspec-knapsack
|
||||
rspec 8 20: *rspec-knapsack
|
||||
rspec 9 20: *rspec-knapsack
|
||||
rspec 10 20: *rspec-knapsack
|
||||
rspec 11 20: *rspec-knapsack
|
||||
rspec 12 20: *rspec-knapsack
|
||||
rspec 13 20: *rspec-knapsack
|
||||
rspec 14 20: *rspec-knapsack
|
||||
rspec 15 20: *rspec-knapsack
|
||||
rspec 16 20: *rspec-knapsack
|
||||
rspec 17 20: *rspec-knapsack
|
||||
rspec 18 20: *rspec-knapsack
|
||||
rspec 19 20: *rspec-knapsack
|
||||
|
||||
spinach 0 10: *spinach-knapsack
|
||||
spinach 1 10: *spinach-knapsack
|
||||
spinach 2 10: *spinach-knapsack
|
||||
spinach 3 10: *spinach-knapsack
|
||||
spinach 4 10: *spinach-knapsack
|
||||
spinach 5 10: *spinach-knapsack
|
||||
spinach 6 10: *spinach-knapsack
|
||||
spinach 7 10: *spinach-knapsack
|
||||
spinach 8 10: *spinach-knapsack
|
||||
spinach 9 10: *spinach-knapsack
|
||||
|
||||
# Execute all testing suites against Ruby 2.2
|
||||
|
||||
.ruby-22: &ruby-22
|
||||
image: "ruby:2.2"
|
||||
only:
|
||||
- master
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
.rspec-knapsack-ruby22: &rspec-knapsack-ruby22
|
||||
<<: *rspec-knapsack
|
||||
<<: *ruby-22
|
||||
|
||||
.spinach-knapsack-ruby22: &spinach-knapsack-ruby22
|
||||
<<: *spinach-knapsack
|
||||
<<: *ruby-22
|
||||
|
||||
rspec 0 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 1 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 2 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 3 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 4 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 5 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 6 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 7 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 8 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 9 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 10 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 11 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 12 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 13 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 14 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 15 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 16 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 17 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 18 20 ruby22: *rspec-knapsack-ruby22
|
||||
rspec 19 20 ruby22: *rspec-knapsack-ruby22
|
||||
|
||||
spinach 0 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 1 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 2 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 3 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 4 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 5 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 6 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 7 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 8 10 ruby22: *spinach-knapsack-ruby22
|
||||
spinach 9 10 ruby22: *spinach-knapsack-ruby22
|
||||
|
||||
# Other generic tests
|
||||
|
||||
.exec: &exec
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
|
||||
- bundle exec $CI_BUILD_NAME
|
||||
|
||||
spec:lib:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
|
||||
|
||||
spec:services:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
|
||||
|
||||
spec:other:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
|
||||
|
||||
spinach:project:half:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
|
||||
|
||||
spinach:project:rest:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
|
||||
|
||||
spinach:other:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
|
||||
|
||||
teaspoon:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec teaspoon
|
||||
|
||||
rubocop:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rubocop
|
||||
|
||||
scss-lint:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake scss_lint
|
||||
|
||||
brakeman:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake brakeman
|
||||
|
||||
flog:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake flog
|
||||
|
||||
flay:
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake flay
|
||||
teaspoon: *exec
|
||||
rubocop: *exec
|
||||
rake scss_lint: *exec
|
||||
rake brakeman: *exec
|
||||
rake flog: *exec
|
||||
rake flay: *exec
|
||||
rake db:migrate:reset: *exec
|
||||
license_finder: *exec
|
||||
|
||||
bundler:audit:
|
||||
stage: test
|
||||
@@ -115,127 +202,10 @@ bundler:audit:
|
||||
script:
|
||||
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
|
||||
|
||||
db-migrate-reset:
|
||||
stage: test
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:migrate:reset
|
||||
|
||||
# Ruby 2.2 jobs
|
||||
|
||||
spec:feature:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spec:api:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spec:models:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spec:lib:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spec:services:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spec:other:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spinach:project:half:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spinach:project:rest:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
spinach:other:ruby22:
|
||||
stage: test
|
||||
image: ruby:2.2
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
|
||||
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
|
||||
cache:
|
||||
key: "ruby22"
|
||||
paths:
|
||||
- vendor
|
||||
# Notify slack in the end
|
||||
|
||||
notify:slack:
|
||||
stage: notifications
|
||||
stage: post-test
|
||||
script:
|
||||
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
|
||||
when: on_failure
|
||||
|
||||
+2
-2
@@ -194,7 +194,7 @@ Style/EmptyLines:
|
||||
|
||||
# Keep blank lines around access modifiers.
|
||||
Style/EmptyLinesAroundAccessModifier:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
|
||||
# Keeps track of empty lines around block bodies.
|
||||
Style/EmptyLinesAroundBlockBody:
|
||||
@@ -771,7 +771,7 @@ Metrics/PerceivedComplexity:
|
||||
# Checks for ambiguous operators in the first argument of a method invocation
|
||||
# without parentheses.
|
||||
Lint/AmbiguousOperator:
|
||||
Enabled: false
|
||||
Enabled: true
|
||||
|
||||
# Checks for ambiguous regexp literals in the first argument of a method
|
||||
# invocation without parentheses.
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.9.0 (unreleased)
|
||||
- Bulk assign/unassign labels to issues.
|
||||
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
|
||||
- Allow enabling wiki page events from Webhook management UI
|
||||
- Bump rouge to 1.11.0
|
||||
- Make EmailsOnPushWorker use Sidekiq mailers queue
|
||||
- Fix wiki page events' webhook to point to the wiki repository
|
||||
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
|
||||
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
|
||||
- Allow forking projects with restricted visibility level
|
||||
- Improve note validation to prevent errors when creating invalid note via API
|
||||
- Reduce number of fog gem dependencies
|
||||
- Remove project notification settings associated with deleted projects
|
||||
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects
|
||||
- Redesign navigation for project pages
|
||||
- Fix groups API to list only user's accessible projects
|
||||
- Redesign account and email confirmation emails
|
||||
- Bump nokogiri to 1.6.8
|
||||
- Use gitlab-shell v3.0.0
|
||||
- Use Knapsack to evenly distribute tests across multiple nodes
|
||||
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
|
||||
- Don't allow MRs to be merged when commits were added since the last review / page load
|
||||
- Add DB index on users.state
|
||||
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
|
||||
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
|
||||
- Fix issues filter when ordering by milestone
|
||||
- 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
|
||||
- Add support for using Yubikeys (U2F) for two-factor authentication
|
||||
- Link to blank group icon doesn't throw a 404 anymore
|
||||
- Remove 'main language' feature
|
||||
- Pipelines can be canceled only when there are running builds
|
||||
- Use downcased path to container repository as this is expected path by Docker
|
||||
@@ -26,13 +37,46 @@ v 8.9.0 (unreleased)
|
||||
- Measure queue duration between gitlab-workhorse and Rails
|
||||
- Make authentication service for Container Registry to be compatible with < Docker 1.11
|
||||
- Add Application Setting to configure Container Registry token expire delay (default 5min)
|
||||
- Cache assigned issue and merge request counts in sidebar nav
|
||||
- Cache project build count in sidebar nav
|
||||
- Reduce number of queries needed to render issue labels in the sidebar
|
||||
- Improve error handling importing projects
|
||||
- Remove duplicated notification settings
|
||||
- Put project Files and Commits tabs under Code tab
|
||||
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
|
||||
- An indicator is now displayed at the top of the comment field for confidential issues.
|
||||
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
|
||||
|
||||
v 8.8.4 (unreleased)
|
||||
- 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
|
||||
- Import GitHub repositories respecting the API rate limit
|
||||
- Fix importer for GitHub comments on diff
|
||||
- Disable Webhooks before proceeding with the GitHub import
|
||||
- Added descriptions to notification settings dropdown
|
||||
|
||||
v 8.8.3
|
||||
- Fix incorrect links on pipeline page when merge request created from fork
|
||||
- Fix gitlab importer failing to import new projects due to missing credentials
|
||||
- Fix import URL migration not rescuing with the correct Error
|
||||
- In search results, only show notes on confidential issues that the user has access to
|
||||
- Fix health check access token changing due to old application settings being used
|
||||
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
|
||||
- Fixed JS error when trying to remove discussion form. !4303
|
||||
- Fixed issue with button color when no CI enabled. !4287
|
||||
- Fixed potential issue with 2 CI status polling events happening. !3869
|
||||
- Improve design of Pipeline view. !4230
|
||||
- Fix gitlab importer failing to import new projects due to missing credentials. !4301
|
||||
- Fix import URL migration not rescuing with the correct Error. !4321
|
||||
- Fix health check access token changing due to old application settings being used. !4332
|
||||
- Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363
|
||||
- Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364
|
||||
- Pass the "Remember me" value to the 2FA token form. !4369
|
||||
- Fix incorrect links on pipeline page when merge request created from fork. !4376
|
||||
- Use downcased path to container repository as this is expected path by Docker. !4420
|
||||
- Fix wiki project clone address error (chujinjin). !4429
|
||||
- Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392
|
||||
- Fix missing number on generated ordered list element. !4437
|
||||
- Prevent disclosure of notes on confidential issues in search results.
|
||||
|
||||
v 8.8.2
|
||||
- Added remove due date button. !4209
|
||||
@@ -134,6 +178,7 @@ v 8.7.6
|
||||
- Fix import from GitLab.com to a private instance failure. !4181
|
||||
- Fix external imports not finding the import data. !4106
|
||||
- Fix notification delay when changing status of an issue
|
||||
- Bump Workhorse to 0.7.5 so it can serve raw diffs
|
||||
|
||||
v 8.7.5
|
||||
- Fix relative links in wiki pages. !4050
|
||||
|
||||
+5
-3
@@ -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
|
||||
(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
|
||||
|
||||
@@ -308,7 +308,7 @@ tests are least likely to receive timely feedback. The workflow to make a merge
|
||||
request is as follows:
|
||||
|
||||
1. Fork the project into your personal space on GitLab.com
|
||||
1. Create a feature branch
|
||||
1. Create a feature branch, branch away from `master`.
|
||||
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
|
||||
1. Add your changes to the [CHANGELOG](CHANGELOG)
|
||||
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
|
||||
@@ -405,6 +405,7 @@ description area. Copy-paste it to retain the markdown format.
|
||||
entire line to follow it. This prevents linting tools from generating warnings.
|
||||
- Don't touch neighbouring lines. As an exception, automatic mass
|
||||
refactoring modifications may leave style non-compliant.
|
||||
1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
|
||||
|
||||
## Changes for Stable Releases
|
||||
|
||||
@@ -530,4 +531,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
|
||||
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
|
||||
[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
|
||||
[`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/
|
||||
[license-finder-doc]: doc/development/licensing.md
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.7.4
|
||||
0.7.5
|
||||
|
||||
@@ -38,16 +38,17 @@ gem 'rack-oauth2', '~> 1.2.1'
|
||||
gem 'jwt'
|
||||
|
||||
# Spam and anti-bot protection
|
||||
gem 'recaptcha', require: 'recaptcha/rails'
|
||||
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
|
||||
gem 'akismet', '~> 2.0'
|
||||
|
||||
# Two-factor authentication
|
||||
gem 'devise-two-factor', '~> 3.0.0'
|
||||
gem 'rqrcode-rails3', '~> 0.1.7'
|
||||
gem 'attr_encrypted', '~> 3.0.0'
|
||||
gem 'u2f', '~> 0.2.1'
|
||||
|
||||
# Browser detection
|
||||
gem "browser", '~> 1.0.0'
|
||||
gem "browser", '~> 2.0.3'
|
||||
|
||||
# Extracting information from a git repository
|
||||
# Provide access to Gitlab::Git library
|
||||
@@ -83,8 +84,14 @@ gem "carrierwave", '~> 0.10.0'
|
||||
# Drag and Drop UI
|
||||
gem 'dropzonejs-rails', '~> 0.7.1'
|
||||
|
||||
# for backups
|
||||
gem 'fog-aws', '~> 0.9'
|
||||
gem 'fog-core', '~> 1.40'
|
||||
gem 'fog-local', '~> 0.3'
|
||||
gem 'fog-google', '~> 0.3'
|
||||
gem 'fog-openstack', '~> 0.1'
|
||||
|
||||
# for aws storage
|
||||
gem "fog", "~> 1.36.0"
|
||||
gem "unf", '~> 0.1.4'
|
||||
|
||||
# Authorization
|
||||
@@ -104,7 +111,7 @@ gem 'org-ruby', '~> 0.9.12'
|
||||
gem 'creole', '~> 0.5.0'
|
||||
gem 'wikicloth', '0.8.1'
|
||||
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
|
||||
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
|
||||
@@ -137,7 +144,7 @@ gem 'redis-namespace'
|
||||
gem "httparty", '~> 0.13.3'
|
||||
|
||||
# Colored output to console
|
||||
gem "colorize", '~> 0.7.0'
|
||||
gem "rainbow", '~> 2.1.0'
|
||||
|
||||
# GitLab settings
|
||||
gem 'settingslogic', '~> 2.0.9'
|
||||
@@ -299,6 +306,9 @@ group :development, :test do
|
||||
gem 'bundler-audit', require: false
|
||||
|
||||
gem 'benchmark-ips', require: false
|
||||
|
||||
gem "license_finder", require: false
|
||||
gem 'knapsack'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
||||
+44
-108
@@ -1,7 +1,6 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (2.3.2)
|
||||
RedCloth (4.2.9)
|
||||
ace-rails-ap (4.0.2)
|
||||
actionmailer (4.2.6)
|
||||
@@ -93,7 +92,7 @@ GEM
|
||||
sass (~> 3.0)
|
||||
slim (>= 1.3.6, < 4.0)
|
||||
terminal-table (~> 1.4)
|
||||
browser (1.0.1)
|
||||
browser (2.0.3)
|
||||
builder (3.2.2)
|
||||
bullet (5.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@@ -183,7 +182,7 @@ GEM
|
||||
erubis (2.7.0)
|
||||
escape_utils (1.1.1)
|
||||
eventmachine (1.0.8)
|
||||
excon (0.45.4)
|
||||
excon (0.49.0)
|
||||
execjs (2.6.0)
|
||||
expression_parser (0.9.0)
|
||||
factory_girl (4.5.0)
|
||||
@@ -200,8 +199,6 @@ GEM
|
||||
multi_json
|
||||
ffaker (2.0.0)
|
||||
ffi (1.9.10)
|
||||
fission (0.5.0)
|
||||
CFPropertyList (~> 2.2)
|
||||
flay (2.6.1)
|
||||
ruby_parser (~> 3.0)
|
||||
sexp_processor (~> 4.0)
|
||||
@@ -211,109 +208,28 @@ GEM
|
||||
flowdock (0.7.1)
|
||||
httparty (~> 0.7)
|
||||
multi_json
|
||||
fog (1.36.0)
|
||||
fog-aliyun (>= 0.1.0)
|
||||
fog-atmos
|
||||
fog-aws (>= 0.6.0)
|
||||
fog-brightbox (~> 0.4)
|
||||
fog-core (~> 1.32)
|
||||
fog-dynect (~> 0.0.2)
|
||||
fog-ecloud (~> 0.1)
|
||||
fog-google (<= 0.1.0)
|
||||
fog-json
|
||||
fog-local
|
||||
fog-powerdns (>= 0.1.1)
|
||||
fog-profitbricks
|
||||
fog-radosgw (>= 0.0.2)
|
||||
fog-riakcs
|
||||
fog-sakuracloud (>= 0.0.4)
|
||||
fog-serverlove
|
||||
fog-softlayer
|
||||
fog-storm_on_demand
|
||||
fog-terremark
|
||||
fog-vmfusion
|
||||
fog-voxel
|
||||
fog-xenserver
|
||||
fog-xml (~> 0.1.1)
|
||||
ipaddress (~> 0.5)
|
||||
nokogiri (~> 1.5, >= 1.5.11)
|
||||
fog-aliyun (0.1.0)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
ipaddress (~> 0.8)
|
||||
xml-simple (~> 1.1)
|
||||
fog-atmos (0.1.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-aws (0.8.1)
|
||||
fog-aws (0.9.2)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
ipaddress (~> 0.8)
|
||||
fog-brightbox (0.10.1)
|
||||
fog-core (~> 1.22)
|
||||
fog-json
|
||||
inflecto (~> 0.0.2)
|
||||
fog-core (1.35.0)
|
||||
fog-core (1.40.0)
|
||||
builder
|
||||
excon (~> 0.45)
|
||||
excon (~> 0.49)
|
||||
formatador (~> 0.2)
|
||||
fog-dynect (0.0.2)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-ecloud (0.3.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-google (0.1.0)
|
||||
fog-google (0.3.2)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-json (1.0.2)
|
||||
fog-core (~> 1.0)
|
||||
multi_json (~> 1.10)
|
||||
fog-local (0.2.1)
|
||||
fog-local (0.3.0)
|
||||
fog-core (~> 1.27)
|
||||
fog-powerdns (0.1.1)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
fog-profitbricks (0.0.5)
|
||||
fog-core
|
||||
fog-xml
|
||||
nokogiri
|
||||
fog-radosgw (0.0.5)
|
||||
fog-core (>= 1.21.0)
|
||||
fog-json
|
||||
fog-xml (>= 0.0.1)
|
||||
fog-riakcs (0.1.0)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-sakuracloud (1.7.5)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-serverlove (0.1.2)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-softlayer (1.0.3)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-storm_on_demand (0.1.1)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-terremark (0.1.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-vmfusion (0.1.0)
|
||||
fission
|
||||
fog-core
|
||||
fog-voxel (0.1.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-xenserver (0.2.2)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-openstack (0.1.6)
|
||||
fog-core (>= 1.39)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
fog-xml (0.1.2)
|
||||
fog-core
|
||||
nokogiri (~> 1.5, >= 1.5.11)
|
||||
@@ -422,11 +338,10 @@ GEM
|
||||
httpclient (2.7.0.1)
|
||||
i18n (0.7.0)
|
||||
ice_nine (0.11.1)
|
||||
inflecto (0.0.2)
|
||||
influxdb (0.2.3)
|
||||
cause
|
||||
json
|
||||
ipaddress (0.8.2)
|
||||
ipaddress (0.8.3)
|
||||
jquery-atwho-rails (1.3.2)
|
||||
jquery-rails (4.1.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
@@ -443,6 +358,9 @@ GEM
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
kgio (2.10.0)
|
||||
knapsack (1.11.0)
|
||||
rake
|
||||
timecop (>= 0.1.0)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
letter_opener (1.4.1)
|
||||
@@ -451,6 +369,12 @@ GEM
|
||||
actionmailer (>= 3.2)
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
license_finder (2.1.0)
|
||||
bundler
|
||||
httparty
|
||||
rubyzip
|
||||
thor
|
||||
xml-simple
|
||||
licensee (8.0.0)
|
||||
rugged (>= 0.24b)
|
||||
listen (3.0.5)
|
||||
@@ -466,7 +390,7 @@ GEM
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.1)
|
||||
mimemagic (0.3.0)
|
||||
mini_portile2 (2.0.0)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.7.0)
|
||||
mousetrap-rails (1.4.6)
|
||||
multi_json (1.11.2)
|
||||
@@ -477,8 +401,9 @@ GEM
|
||||
net-ldap (0.12.1)
|
||||
net-ssh (3.0.1)
|
||||
newrelic_rpm (3.14.1.311)
|
||||
nokogiri (1.6.7.2)
|
||||
mini_portile2 (~> 2.0.0.rc2)
|
||||
nokogiri (1.6.8)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
pkg-config (~> 1.1.7)
|
||||
oauth (0.4.7)
|
||||
oauth2 (1.0.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
@@ -550,6 +475,7 @@ GEM
|
||||
parser (2.3.1.0)
|
||||
ast (~> 2.2)
|
||||
pg (0.18.4)
|
||||
pkg-config (1.1.7)
|
||||
poltergeist (1.9.0)
|
||||
capybara (~> 2.1)
|
||||
cliver (~> 0.3.1)
|
||||
@@ -625,7 +551,7 @@ GEM
|
||||
debugger-ruby_core_source (~> 1.3)
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
recaptcha (1.0.2)
|
||||
recaptcha (3.0.0)
|
||||
json
|
||||
redcarpet (3.3.3)
|
||||
redis (3.3.0)
|
||||
@@ -654,7 +580,7 @@ GEM
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
rinku (1.7.3)
|
||||
rotp (2.1.2)
|
||||
rouge (1.10.1)
|
||||
rouge (1.11.0)
|
||||
rqrcode (0.7.0)
|
||||
chunky_png
|
||||
rqrcode-rails3 (0.1.7)
|
||||
@@ -703,6 +629,7 @@ GEM
|
||||
sexp_processor (~> 4.1)
|
||||
rubyntlm (0.5.2)
|
||||
rubypants (0.2.0)
|
||||
rubyzip (1.2.0)
|
||||
rufus-scheduler (3.1.10)
|
||||
rugged (0.24.0)
|
||||
safe_yaml (1.0.4)
|
||||
@@ -813,6 +740,7 @@ GEM
|
||||
thor (0.19.1)
|
||||
thread_safe (0.3.5)
|
||||
tilt (2.0.2)
|
||||
timecop (0.8.1)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
tinder (1.10.1)
|
||||
eventmachine (~> 1.0)
|
||||
@@ -832,6 +760,7 @@ GEM
|
||||
simple_oauth (~> 0.1.4)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
u2f (0.2.1)
|
||||
uglifier (2.7.2)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
@@ -900,7 +829,7 @@ DEPENDENCIES
|
||||
binding_of_caller (~> 0.7.2)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
brakeman (~> 3.2.0)
|
||||
browser (~> 1.0.0)
|
||||
browser (~> 2.0.3)
|
||||
bullet
|
||||
bundler-audit
|
||||
byebug
|
||||
@@ -909,7 +838,6 @@ DEPENDENCIES
|
||||
carrierwave (~> 0.10.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
coffee-rails (~> 4.1.0)
|
||||
colorize (~> 0.7.0)
|
||||
connection_pool (~> 2.0)
|
||||
coveralls (~> 0.8.2)
|
||||
creole (~> 0.5.0)
|
||||
@@ -927,7 +855,11 @@ DEPENDENCIES
|
||||
ffaker (~> 2.0.0)
|
||||
flay
|
||||
flog
|
||||
fog (~> 1.36.0)
|
||||
fog-aws (~> 0.9)
|
||||
fog-core (~> 1.40)
|
||||
fog-google (~> 0.3)
|
||||
fog-local (~> 0.3)
|
||||
fog-openstack (~> 0.1)
|
||||
font-awesome-rails (~> 4.2)
|
||||
foreman
|
||||
fuubar (~> 2.0.0)
|
||||
@@ -956,7 +888,9 @@ DEPENDENCIES
|
||||
jquery-ui-rails (~> 5.0.0)
|
||||
jwt
|
||||
kaminari (~> 0.17.0)
|
||||
knapsack
|
||||
letter_opener_web (~> 1.3.0)
|
||||
license_finder
|
||||
licensee (~> 8.0.0)
|
||||
loofah (~> 2.0.3)
|
||||
mail_room (~> 0.7)
|
||||
@@ -996,10 +930,11 @@ DEPENDENCIES
|
||||
rack-oauth2 (~> 1.2.1)
|
||||
rails (= 4.2.6)
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rainbow (~> 2.1.0)
|
||||
raphael-rails (~> 2.1.2)
|
||||
rblineprof
|
||||
rdoc (~> 3.6)
|
||||
recaptcha
|
||||
recaptcha (~> 3.0)
|
||||
redcarpet (~> 3.3.3)
|
||||
redis (~> 3.2)
|
||||
redis-namespace
|
||||
@@ -1007,7 +942,7 @@ DEPENDENCIES
|
||||
request_store (~> 1.3.0)
|
||||
rerun (~> 0.11.0)
|
||||
responders (~> 2.0)
|
||||
rouge (~> 1.10.1)
|
||||
rouge (~> 1.11)
|
||||
rqrcode-rails3 (~> 0.1.7)
|
||||
rspec-rails (~> 3.4.0)
|
||||
rspec-retry
|
||||
@@ -1045,6 +980,7 @@ DEPENDENCIES
|
||||
thin (~> 1.6.1)
|
||||
tinder (~> 1.10.0)
|
||||
turbolinks (~> 2.5.0)
|
||||
u2f (~> 0.2.1)
|
||||
uglifier (~> 2.7.2)
|
||||
underscore-rails (~> 1.8.0)
|
||||
unf (~> 0.1.4)
|
||||
@@ -1057,4 +993,4 @@ DEPENDENCIES
|
||||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
1.12.4
|
||||
1.12.5
|
||||
|
||||
@@ -8,3 +8,5 @@ relative_url_conf = File.expand_path('../config/initializers/relative_url', __FI
|
||||
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
|
||||
|
||||
Gitlab::Application.load_tasks
|
||||
|
||||
Knapsack.load_tasks if defined?(Knapsack)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
class @LabelManager
|
||||
errorMessage: 'Unable to update label prioritization at this time'
|
||||
|
||||
constructor: (opts = {}) ->
|
||||
# Defaults
|
||||
{
|
||||
@togglePriorityButton = $('.js-toggle-priority')
|
||||
@prioritizedLabels = $('.js-prioritized-labels')
|
||||
@otherLabels = $('.js-other-labels')
|
||||
} = opts
|
||||
|
||||
@prioritizedLabels.sortable(
|
||||
items: 'li'
|
||||
placeholder: 'list-placeholder'
|
||||
axis: 'y'
|
||||
update: @onPrioritySortUpdate.bind(@)
|
||||
)
|
||||
|
||||
@bindEvents()
|
||||
|
||||
bindEvents: ->
|
||||
@togglePriorityButton.on 'click', @, @onTogglePriorityClick
|
||||
|
||||
onTogglePriorityClick: (e) ->
|
||||
e.preventDefault()
|
||||
_this = e.data
|
||||
$btn = $(e.currentTarget)
|
||||
$label = $("##{$btn.data('domId')}")
|
||||
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
|
||||
_this.toggleLabelPriority($label, action)
|
||||
|
||||
toggleLabelPriority: ($label, action, persistState = true) ->
|
||||
_this = @
|
||||
url = $label.find('.js-toggle-priority').data 'url'
|
||||
|
||||
$target = @prioritizedLabels
|
||||
$from = @otherLabels
|
||||
|
||||
# Optimistic update
|
||||
if action is 'remove'
|
||||
$target = @otherLabels
|
||||
$from = @prioritizedLabels
|
||||
|
||||
if $from.find('li').length is 1
|
||||
$from.find('.empty-message').show()
|
||||
|
||||
if not $target.find('li').length
|
||||
$target.find('.empty-message').hide()
|
||||
|
||||
$label.detach().appendTo($target)
|
||||
|
||||
# Return if we are not persisting state
|
||||
return unless persistState
|
||||
|
||||
if action is 'remove'
|
||||
xhr = $.ajax url: url, type: 'DELETE'
|
||||
else
|
||||
xhr = @savePrioritySort($label, action)
|
||||
|
||||
xhr.fail @rollbackLabelPosition.bind(@, $label, action)
|
||||
|
||||
onPrioritySortUpdate: ->
|
||||
xhr = @savePrioritySort()
|
||||
|
||||
xhr.fail ->
|
||||
new Flash(@errorMessage, 'alert')
|
||||
|
||||
savePrioritySort: () ->
|
||||
$.post
|
||||
url: @prioritizedLabels.data('url')
|
||||
data:
|
||||
label_ids: @getSortedLabelsIds()
|
||||
|
||||
rollbackLabelPosition: ($label, originalAction)->
|
||||
action = if originalAction is 'remove' then 'add' else 'remove'
|
||||
@toggleLabelPriority($label, action, false)
|
||||
|
||||
new Flash(@errorMessage, 'alert')
|
||||
|
||||
getSortedLabelsIds: ->
|
||||
sortedIds = []
|
||||
@prioritizedLabels.find('li').each ->
|
||||
sortedIds.push $(@).data 'id'
|
||||
sortedIds
|
||||
@@ -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
|
||||
# the compiled file.
|
||||
#
|
||||
#= require jquery
|
||||
#= require jquery2
|
||||
#= require jquery-ui/autocomplete
|
||||
#= require jquery-ui/datepicker
|
||||
#= require jquery-ui/draggable
|
||||
@@ -56,9 +56,11 @@
|
||||
#= require_directory ./commit
|
||||
#= require_directory ./extensions
|
||||
#= require_directory ./lib
|
||||
#= require_directory ./u2f
|
||||
#= require_directory .
|
||||
#= require fuzzaldrin-plus
|
||||
#= require cropper
|
||||
#= require u2f
|
||||
|
||||
window.slugify = (text) ->
|
||||
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
|
||||
|
||||
@@ -1,201 +1,352 @@
|
||||
class @AwardsHandler
|
||||
constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) ->
|
||||
$('.js-add-award').on 'click', (event) =>
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
@showEmojiMenu()
|
||||
constructor: ->
|
||||
|
||||
$('html').on 'click', (event) ->
|
||||
if !$(event.target).closest('.emoji-menu').length
|
||||
@aliases = gl.emojiAliases()
|
||||
|
||||
$(document)
|
||||
.off 'click', '.js-add-award'
|
||||
.on 'click', '.js-add-award', (e) =>
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
@showEmojiMenu $(e.currentTarget)
|
||||
|
||||
$('html').on 'click', (e) ->
|
||||
$target = $ e.target
|
||||
|
||||
unless $target.closest('.emoji-menu-content').length
|
||||
$('.js-awards-block.current').removeClass 'current'
|
||||
|
||||
unless $target.closest('.emoji-menu').length
|
||||
if $('.emoji-menu').is(':visible')
|
||||
$('.js-add-award.is-active').removeClass 'is-active'
|
||||
$('.emoji-menu').removeClass 'is-visible'
|
||||
|
||||
$('.awards')
|
||||
.off 'click'
|
||||
.on 'click', '.js-emoji-btn', @handleClick
|
||||
$(document)
|
||||
.off 'click', '.js-emoji-btn'
|
||||
.on 'click', '.js-emoji-btn', (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
@renderFrequentlyUsedBlock()
|
||||
$target = $ e.currentTarget
|
||||
emoji = $target.find('.icon').data 'emoji'
|
||||
|
||||
handleClick: (e) ->
|
||||
e.preventDefault()
|
||||
emoji = $(this)
|
||||
.find('.icon')
|
||||
.data 'emoji'
|
||||
$target.closest('.js-awards-block').addClass 'current'
|
||||
@addAward @getVotesBlock(), @getAwardUrl(), emoji
|
||||
|
||||
if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown'
|
||||
awardsHandler.addAward 'thumbsdown'
|
||||
|
||||
else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup'
|
||||
awardsHandler.addAward 'thumbsup'
|
||||
showEmojiMenu: ($addBtn) ->
|
||||
|
||||
awardsHandler.addAward emoji
|
||||
$menu = $ '.emoji-menu'
|
||||
|
||||
$(this).trigger 'blur'
|
||||
if $addBtn.hasClass 'js-note-emoji'
|
||||
$addBtn.parents('.note').find('.js-awards-block').addClass 'current'
|
||||
else
|
||||
$addBtn.closest('.js-awards-block').addClass 'current'
|
||||
|
||||
didUserClickEmoji: (that, emoji) ->
|
||||
if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title')
|
||||
$(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1
|
||||
if $menu.length
|
||||
$holder = $addBtn.closest('.js-award-holder')
|
||||
|
||||
showEmojiMenu: ->
|
||||
if $('.emoji-menu').length
|
||||
if $('.emoji-menu').is '.is-visible'
|
||||
$('.emoji-menu').removeClass 'is-visible'
|
||||
if $menu.is '.is-visible'
|
||||
$addBtn.removeClass 'is-active'
|
||||
$menu.removeClass 'is-visible'
|
||||
$('#emoji_search').blur()
|
||||
else
|
||||
$('.emoji-menu').addClass 'is-visible'
|
||||
$addBtn.addClass 'is-active'
|
||||
@positionMenu($menu, $addBtn)
|
||||
|
||||
$menu.addClass 'is-visible'
|
||||
$('#emoji_search').focus()
|
||||
else
|
||||
$('.js-add-award').addClass 'is-loading'
|
||||
$.get @getEmojisUrl, (response) =>
|
||||
$('.js-add-award').removeClass 'is-loading'
|
||||
$('.js-award-holder').append response
|
||||
$addBtn.addClass 'is-loading is-active'
|
||||
url = @getAwardMenuUrl()
|
||||
|
||||
@createEmojiMenu url, =>
|
||||
$addBtn.removeClass 'is-loading'
|
||||
$menu = $('.emoji-menu')
|
||||
@positionMenu($menu, $addBtn)
|
||||
@renderFrequentlyUsedBlock()
|
||||
|
||||
setTimeout =>
|
||||
$('.emoji-menu').addClass 'is-visible'
|
||||
$menu.addClass 'is-visible'
|
||||
$('#emoji_search').focus()
|
||||
@setupSearch()
|
||||
, 200
|
||||
|
||||
addAward: (emoji) ->
|
||||
@postEmoji emoji, =>
|
||||
@addAwardToEmojiBar(emoji)
|
||||
|
||||
createEmojiMenu: (awardMenuUrl, callback) ->
|
||||
|
||||
$.get awardMenuUrl, (response) ->
|
||||
$('body').append response
|
||||
callback()
|
||||
|
||||
|
||||
positionMenu: ($menu, $addBtn) ->
|
||||
|
||||
position = $addBtn.data('position')
|
||||
|
||||
# The menu could potentially be off-screen or in a hidden overflow element
|
||||
# So we position the element absolute in the body
|
||||
css =
|
||||
top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
|
||||
|
||||
if position? and position is 'right'
|
||||
css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
|
||||
$menu.addClass 'is-aligned-right'
|
||||
else
|
||||
css.left = "#{$addBtn.offset().left}px"
|
||||
$menu.removeClass 'is-aligned-right'
|
||||
|
||||
$menu.css(css)
|
||||
|
||||
|
||||
addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes, callback) ->
|
||||
|
||||
emoji = @normilizeEmojiName emoji
|
||||
|
||||
@postEmoji awardUrl, emoji, =>
|
||||
@addAwardToEmojiBar votesBlock, emoji, checkMutuality
|
||||
callback?()
|
||||
|
||||
$('.emoji-menu').removeClass 'is-visible'
|
||||
|
||||
addAwardToEmojiBar: (emoji) ->
|
||||
@addEmojiToFrequentlyUsedList(emoji)
|
||||
|
||||
if @exist(emoji)
|
||||
if @isActive(emoji)
|
||||
@decrementCounter(emoji)
|
||||
addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = yes) ->
|
||||
|
||||
@checkMutuality votesBlock, emoji if checkForMutuality
|
||||
@addEmojiToFrequentlyUsedList emoji
|
||||
|
||||
emoji = @normilizeEmojiName emoji
|
||||
$emojiButton = @findEmojiIcon(votesBlock, emoji).parent()
|
||||
|
||||
if $emojiButton.length > 0
|
||||
if @isActive $emojiButton
|
||||
@decrementCounter $emojiButton, emoji
|
||||
else
|
||||
counter = @findEmojiIcon(emoji).siblings('.js-counter')
|
||||
counter.text(parseInt(counter.text()) + 1)
|
||||
counter.parent().addClass('active')
|
||||
@addMeToAuthorList(emoji)
|
||||
counter = $emojiButton.find '.js-counter'
|
||||
counter.text parseInt(counter.text()) + 1
|
||||
$emojiButton.addClass 'active'
|
||||
@addMeToUserList votesBlock, emoji
|
||||
@animateEmoji $emojiButton
|
||||
else
|
||||
@createEmoji(emoji)
|
||||
votesBlock.removeClass 'hidden'
|
||||
@createEmoji votesBlock, emoji
|
||||
|
||||
exist: (emoji) ->
|
||||
@findEmojiIcon(emoji).length > 0
|
||||
|
||||
isActive: (emoji) ->
|
||||
@findEmojiIcon(emoji).parent().hasClass('active')
|
||||
getVotesBlock: ->
|
||||
|
||||
decrementCounter: (emoji) ->
|
||||
counter = @findEmojiIcon(emoji).siblings('.js-counter')
|
||||
emojiIcon = counter.parent()
|
||||
if parseInt(counter.text()) > 1
|
||||
counter.text(parseInt(counter.text()) - 1)
|
||||
emojiIcon.removeClass('active')
|
||||
@removeMeFromAuthorList(emoji)
|
||||
else if emoji == 'thumbsup' || emoji == 'thumbsdown'
|
||||
emojiIcon.tooltip('destroy')
|
||||
counter.text(0)
|
||||
emojiIcon.removeClass('active')
|
||||
@removeMeFromAuthorList(emoji)
|
||||
currentBlock = $ '.js-awards-block.current'
|
||||
return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0
|
||||
|
||||
|
||||
getAwardUrl: -> return @getVotesBlock().data 'award-url'
|
||||
|
||||
|
||||
checkMutuality: (votesBlock, emoji) ->
|
||||
|
||||
awardUrl = @getAwardUrl()
|
||||
|
||||
if emoji in [ 'thumbsup', 'thumbsdown' ]
|
||||
mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
|
||||
$emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent()
|
||||
isAlreadyVoted = $emojiButton.hasClass 'active'
|
||||
|
||||
if isAlreadyVoted
|
||||
@showEmojiLoader $emojiButton
|
||||
@addAward votesBlock, awardUrl, mutualVote, no, ->
|
||||
$emojiButton.removeClass 'is-loading'
|
||||
|
||||
|
||||
showEmojiLoader: ($emojiButton) ->
|
||||
|
||||
$loader = $emojiButton.find '.fa-spinner'
|
||||
|
||||
unless $loader.length
|
||||
$emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>'
|
||||
|
||||
$emojiButton.addClass 'is-loading'
|
||||
|
||||
|
||||
isActive: ($emojiButton) -> $emojiButton.hasClass 'active'
|
||||
|
||||
|
||||
decrementCounter: ($emojiButton, emoji) ->
|
||||
|
||||
counter = $ '.js-counter', $emojiButton
|
||||
counterNumber = parseInt counter.text(), 10
|
||||
|
||||
if counterNumber > 1
|
||||
counter.text counterNumber - 1
|
||||
@removeMeFromUserList $emojiButton, emoji
|
||||
else if emoji is 'thumbsup' or emoji is 'thumbsdown'
|
||||
$emojiButton.tooltip 'destroy'
|
||||
counter.text '0'
|
||||
@removeMeFromUserList $emojiButton, emoji
|
||||
@removeEmoji $emojiButton if $emojiButton.parents('.note').length
|
||||
else
|
||||
emojiIcon.tooltip('destroy')
|
||||
emojiIcon.remove()
|
||||
@removeEmoji $emojiButton
|
||||
|
||||
$emojiButton.removeClass 'active'
|
||||
|
||||
|
||||
removeEmoji: ($emojiButton) ->
|
||||
|
||||
$emojiButton.tooltip('destroy')
|
||||
$emojiButton.remove()
|
||||
|
||||
$votesBlock = @getVotesBlock()
|
||||
|
||||
if $votesBlock.find('.js-emoji-btn').length is 0
|
||||
$votesBlock.addClass 'hidden'
|
||||
|
||||
|
||||
getAwardTooltip: ($awardBlock) ->
|
||||
|
||||
return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or ''
|
||||
|
||||
|
||||
removeMeFromUserList: ($emojiButton, emoji) ->
|
||||
|
||||
awardBlock = $emojiButton
|
||||
originalTitle = @getAwardTooltip awardBlock
|
||||
|
||||
authors = originalTitle.split ', '
|
||||
authors.splice authors.indexOf('me'), 1
|
||||
|
||||
newAuthors = authors.join ', '
|
||||
|
||||
removeMeFromAuthorList: (emoji) ->
|
||||
awardBlock = @findEmojiIcon(emoji).parent()
|
||||
authors = awardBlock
|
||||
.attr('data-original-title')
|
||||
.split(', ')
|
||||
authors.splice(authors.indexOf('me'),1)
|
||||
awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.attr('data-original-title', authors.join(', '))
|
||||
@resetTooltip(awardBlock)
|
||||
.closest '.js-emoji-btn'
|
||||
.removeData 'original-title'
|
||||
.attr 'data-original-title', newAuthors
|
||||
|
||||
@resetTooltip awardBlock
|
||||
|
||||
|
||||
addMeToUserList: (votesBlock, emoji) ->
|
||||
|
||||
awardBlock = @findEmojiIcon(votesBlock, emoji).parent()
|
||||
origTitle = @getAwardTooltip awardBlock
|
||||
users = []
|
||||
|
||||
addMeToAuthorList: (emoji) ->
|
||||
awardBlock = @findEmojiIcon(emoji).parent()
|
||||
origTitle = awardBlock.attr('data-original-title').trim()
|
||||
authors = []
|
||||
if origTitle
|
||||
authors = origTitle.split(', ')
|
||||
authors.push('me')
|
||||
awardBlock.attr('data-original-title', authors.join(', '))
|
||||
@resetTooltip(awardBlock)
|
||||
users = origTitle.trim().split ', '
|
||||
|
||||
users.push 'me'
|
||||
awardBlock.attr 'title', users.join ', '
|
||||
|
||||
@resetTooltip awardBlock
|
||||
|
||||
|
||||
resetTooltip: (award) ->
|
||||
award.tooltip('destroy')
|
||||
|
||||
# "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
|
||||
setTimeout (->
|
||||
award.tooltip()
|
||||
), 200
|
||||
award.tooltip 'destroy'
|
||||
|
||||
# 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
|
||||
cb = -> award.tooltip()
|
||||
setTimeout cb, 200
|
||||
|
||||
|
||||
createEmoji: (emoji) ->
|
||||
emojiCssClass = @resolveNameToCssClass(emoji)
|
||||
createEmoji_: (votesBlock, emoji) ->
|
||||
|
||||
nodes = []
|
||||
nodes.push(
|
||||
"<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>",
|
||||
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
|
||||
"<span class='award-control-text js-counter'>1</span>",
|
||||
"</button>"
|
||||
)
|
||||
emojiCssClass = @resolveNameToCssClass emoji
|
||||
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
|
||||
<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
|
||||
<span class='award-control-text js-counter'>1</span>
|
||||
</button>"
|
||||
|
||||
$(nodes.join("\n"))
|
||||
.insertBefore('.js-award-holder')
|
||||
.find('.emoji-icon')
|
||||
.data('emoji', emoji)
|
||||
$emojiButton = $ buttonHtml
|
||||
$emojiButton
|
||||
.insertBefore votesBlock.find '.js-award-holder'
|
||||
.find '.emoji-icon'
|
||||
.data 'emoji', emoji
|
||||
|
||||
@animateEmoji $emojiButton
|
||||
$('.award-control').tooltip()
|
||||
votesBlock.removeClass 'current'
|
||||
|
||||
|
||||
animateEmoji: ($emoji) ->
|
||||
|
||||
className = 'pulse animated'
|
||||
|
||||
$emoji.addClass className
|
||||
setTimeout (-> $emoji.removeClass className), 321
|
||||
|
||||
|
||||
createEmoji: (votesBlock, emoji) ->
|
||||
|
||||
if $('.emoji-menu').length
|
||||
return @createEmoji_ votesBlock, emoji
|
||||
|
||||
@createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji
|
||||
|
||||
|
||||
getAwardMenuUrl: -> return gl.awardMenuUrl
|
||||
|
||||
|
||||
resolveNameToCssClass: (emoji) ->
|
||||
emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']")
|
||||
|
||||
emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']"
|
||||
|
||||
if emojiIcon.length > 0
|
||||
unicodeName = emojiIcon.data('unicode-name')
|
||||
unicodeName = emojiIcon.data 'unicode-name'
|
||||
else
|
||||
# Find by alias
|
||||
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name')
|
||||
unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name'
|
||||
|
||||
"emoji-#{unicodeName}"
|
||||
return "emoji-#{unicodeName}"
|
||||
|
||||
postEmoji: (emoji, callback) ->
|
||||
$.post @postEmojiUrl, { note: {
|
||||
note: ":#{emoji}:"
|
||||
noteable_type: @noteableType
|
||||
noteable_id: @noteableId
|
||||
}},(data) ->
|
||||
if data.ok
|
||||
callback.call()
|
||||
|
||||
findEmojiIcon: (emoji) ->
|
||||
$(".awards > .js-emoji-btn [data-emoji='#{emoji}']")
|
||||
postEmoji: (awardUrl, emoji, callback) ->
|
||||
|
||||
$.post awardUrl, { name: emoji }, (data) ->
|
||||
callback() if data.ok
|
||||
|
||||
|
||||
findEmojiIcon: (votesBlock, emoji) ->
|
||||
|
||||
return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']"
|
||||
|
||||
|
||||
scrollToAwards: ->
|
||||
$('body, html').animate({
|
||||
scrollTop: $('.awards').offset().top - 80
|
||||
}, 200)
|
||||
|
||||
options = scrollTop: $('.awards').offset().top - 110
|
||||
$('body, html').animate options, 200
|
||||
|
||||
|
||||
normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji
|
||||
|
||||
|
||||
addEmojiToFrequentlyUsedList: (emoji) ->
|
||||
|
||||
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
|
||||
frequentlyUsedEmojis.push(emoji)
|
||||
$.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 })
|
||||
frequentlyUsedEmojis.push emoji
|
||||
$.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }
|
||||
|
||||
|
||||
getFrequentlyUsedEmojis: ->
|
||||
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',')
|
||||
_.compact(_.uniq(frequentlyUsedEmojis))
|
||||
|
||||
frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',')
|
||||
return _.compact _.uniq frequentlyUsedEmojis
|
||||
|
||||
|
||||
renderFrequentlyUsedBlock: ->
|
||||
if $.cookie('frequently_used_emojis')
|
||||
|
||||
if $.cookie 'frequently_used_emojis'
|
||||
frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
|
||||
|
||||
ul = $('<ul>')
|
||||
ul = $("<ul class='clearfix emoji-menu-list'>")
|
||||
|
||||
for emoji in frequentlyUsedEmojis
|
||||
do (emoji) ->
|
||||
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
|
||||
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
|
||||
|
||||
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
|
||||
|
||||
|
||||
setupSearch: ->
|
||||
$('input.emoji-search').keyup (ev) =>
|
||||
|
||||
$('input.emoji-search').on 'keyup', (ev) =>
|
||||
term = $(ev.target).val()
|
||||
|
||||
# Clean previous search results
|
||||
@@ -204,12 +355,14 @@ class @AwardsHandler
|
||||
if term
|
||||
# Generate a search result block
|
||||
h5 = $('<h5>').text('Search results').addClass('emoji-search')
|
||||
foundEmojis = @searchEmojis(term).show()
|
||||
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis)
|
||||
found_emojis = @searchEmojis(term).show()
|
||||
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide()
|
||||
$('.emoji-menu-content').append(h5).append(ul)
|
||||
else
|
||||
$('.emoji-menu-content').children().show()
|
||||
|
||||
searchEmojis: (term)->
|
||||
$(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone()
|
||||
|
||||
searchEmojis: (term) ->
|
||||
|
||||
$(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone()
|
||||
|
||||
@@ -17,11 +17,13 @@ class Dispatcher
|
||||
switch page
|
||||
when 'projects:issues:index'
|
||||
Issuable.init()
|
||||
new IssuableBulkActions()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:issues:show'
|
||||
new Issue()
|
||||
shortcut_handler = new ShortcutsIssuable()
|
||||
new ZenMode()
|
||||
gl.awardsHandler = new AwardsHandler()
|
||||
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
|
||||
new Milestone()
|
||||
when 'dashboard:todos:index'
|
||||
@@ -52,6 +54,7 @@ class Dispatcher
|
||||
new Diff()
|
||||
shortcut_handler = new ShortcutsIssuable(true)
|
||||
new ZenMode()
|
||||
gl.awardsHandler = new AwardsHandler()
|
||||
when "projects:merge_requests:diffs"
|
||||
new Diff()
|
||||
new ZenMode()
|
||||
@@ -97,6 +100,8 @@ class Dispatcher
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:labels:new', 'projects:labels:edit'
|
||||
new Labels()
|
||||
when 'projects:labels:index'
|
||||
new LabelManager() if $('.prioritized-labels').length
|
||||
when 'projects:network:show'
|
||||
# Ensure we don't create a particular shortcut handler here. This is
|
||||
# already created, where the network graph is created.
|
||||
|
||||
@@ -21,7 +21,7 @@ class @DueDateSelect
|
||||
$dropdown.glDropdown(
|
||||
hidden: ->
|
||||
$selectbox.hide()
|
||||
$value.removeAttr('style')
|
||||
$value.css('display', '')
|
||||
)
|
||||
|
||||
addDueDate = (isDropdown) ->
|
||||
@@ -42,12 +42,13 @@ class @DueDateSelect
|
||||
type: 'PUT'
|
||||
url: issueUpdateURL
|
||||
data: data
|
||||
dataType: 'json'
|
||||
beforeSend: ->
|
||||
$loading.fadeIn()
|
||||
if isDropdown
|
||||
$dropdown.trigger('loading.gl.dropdown')
|
||||
$selectbox.hide()
|
||||
$value.removeAttr('style')
|
||||
$value.css('display', '')
|
||||
|
||||
$valueContent.html(mediumDate)
|
||||
$sidebarValue.html(mediumDate)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class @Flash
|
||||
constructor: (message, type)->
|
||||
constructor: (message, type = 'alert')->
|
||||
@flash = $(".flash-container")
|
||||
@flash.html("")
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ class GitLabDropdownFilter
|
||||
$inputContainer = @input.parent()
|
||||
$clearButton = $inputContainer.find('.js-dropdown-input-clear')
|
||||
|
||||
@indeterminateIds = []
|
||||
|
||||
# Clear click
|
||||
$clearButton.on 'click', (e) =>
|
||||
e.preventDefault()
|
||||
@@ -35,20 +37,20 @@ class GitLabDropdownFilter
|
||||
if keyCode is 13
|
||||
return false
|
||||
|
||||
clearTimeout timeout
|
||||
timeout = setTimeout =>
|
||||
blur_field = @shouldBlur keyCode
|
||||
search_text = @input.val()
|
||||
# Only filter asynchronously only if option remote is set
|
||||
if @options.remote
|
||||
clearTimeout timeout
|
||||
timeout = setTimeout =>
|
||||
blur_field = @shouldBlur keyCode
|
||||
|
||||
if blur_field and @filterInputBlur
|
||||
@input.blur()
|
||||
if blur_field and @filterInputBlur
|
||||
@input.blur()
|
||||
|
||||
if @options.remote
|
||||
@options.query search_text, (data) =>
|
||||
@options.query @input.val(), (data) =>
|
||||
@options.callback(data)
|
||||
else
|
||||
@filter search_text
|
||||
, 250
|
||||
, 250
|
||||
else
|
||||
@filter @input.val()
|
||||
|
||||
shouldBlur: (keyCode) ->
|
||||
return BLUR_KEYCODES.indexOf(keyCode) >= 0
|
||||
@@ -142,6 +144,7 @@ class GitLabDropdown
|
||||
LOADING_CLASS = "is-loading"
|
||||
PAGE_TWO_CLASS = "is-page-two"
|
||||
ACTIVE_CLASS = "is-active"
|
||||
INDETERMINATE_CLASS = "is-indeterminate"
|
||||
currentIndex = -1
|
||||
|
||||
FILTER_INPUT = '.dropdown-input .dropdown-input-field'
|
||||
@@ -182,9 +185,6 @@ class GitLabDropdown
|
||||
@fullData = data
|
||||
|
||||
@parseData @fullData
|
||||
|
||||
if @options.filterable
|
||||
@filterInput.trigger 'keyup'
|
||||
}
|
||||
|
||||
# Init filterable
|
||||
@@ -298,6 +298,13 @@ class GitLabDropdown
|
||||
opened: =>
|
||||
@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()
|
||||
if @remote && contentHtml is ""
|
||||
@remote.execute()
|
||||
@@ -309,12 +316,18 @@ class GitLabDropdown
|
||||
|
||||
hidden: (e) =>
|
||||
@removeArrayKeyEvent()
|
||||
|
||||
$input = @dropdown.find(".dropdown-input-field")
|
||||
|
||||
if @options.filterable
|
||||
@dropdown
|
||||
.find(".dropdown-input-field")
|
||||
$input
|
||||
.blur()
|
||||
.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
|
||||
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
|
||||
@@ -358,7 +371,7 @@ class GitLabDropdown
|
||||
|
||||
if @options.renderRow
|
||||
# Call the render function
|
||||
html = @options.renderRow(data)
|
||||
html = @options.renderRow.call(@options, data, @)
|
||||
else
|
||||
if not selected
|
||||
value = if @options.id then @options.id(data) else data.id
|
||||
@@ -443,6 +456,17 @@ class GitLabDropdown
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
|
||||
else
|
||||
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
|
||||
if not @options.multiSelect or el.hasClass('dropdown-clear-active')
|
||||
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
|
||||
@@ -459,31 +483,42 @@ class GitLabDropdown
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
|
||||
if value?
|
||||
if !field.length and fieldName
|
||||
# Create hidden input for form
|
||||
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
|
||||
if @options.inputId?
|
||||
input = $(input)
|
||||
.attr('id', @options.inputId)
|
||||
@dropdown.before input
|
||||
@addInput(fieldName, value)
|
||||
else
|
||||
field.val value
|
||||
|
||||
return selectedObject
|
||||
|
||||
selectRowAtIndex: (index) ->
|
||||
selector = ".dropdown-content li:not(.divider):eq(#{index}) a"
|
||||
addInput: (fieldName, value)->
|
||||
# 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
|
||||
selector = ".dropdown-page-one #{selector}"
|
||||
|
||||
# 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: ->
|
||||
ARROW_KEY_CODES = [38, 40]
|
||||
$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
|
||||
selector = ".dropdown-page-one #{selector}"
|
||||
|
||||
@@ -511,8 +546,8 @@ class GitLabDropdown
|
||||
|
||||
return false
|
||||
|
||||
if currentKeyCode is 13
|
||||
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex
|
||||
if currentKeyCode is 13 and currentIndex isnt -1
|
||||
@selectRowAtIndex e, currentIndex
|
||||
|
||||
removeArrayKeyEvent: ->
|
||||
$('body').off 'keydown'
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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()
|
||||
@@ -1,5 +1,7 @@
|
||||
class @LabelsSelect
|
||||
constructor: ->
|
||||
_this = @
|
||||
|
||||
$('.js-label-select').each (i, dropdown) ->
|
||||
$dropdown = $(dropdown)
|
||||
projectId = $dropdown.data('project-id')
|
||||
@@ -196,10 +198,18 @@ class @LabelsSelect
|
||||
|
||||
callback data
|
||||
|
||||
renderRow: (label) ->
|
||||
removesAll = label.id is 0 or not label.id?
|
||||
renderRow: (label, instance) ->
|
||||
$li = $('<li>')
|
||||
$a = $('<a href="#">')
|
||||
|
||||
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']\
|
||||
[name='#{$dropdown.data('fieldName')}']\
|
||||
[value='#{this.id(label)}']").length
|
||||
@@ -230,13 +240,17 @@ class @LabelsSelect
|
||||
else
|
||||
colorEl = ''
|
||||
|
||||
"<li>
|
||||
<a href='#' class='#{selectedClass.join(' ')}'>
|
||||
#{colorEl}
|
||||
#{_.escape(label.title)}
|
||||
</a>
|
||||
</li>"
|
||||
filterable: true
|
||||
# We need to identify which items are actually labels
|
||||
if label.id
|
||||
selectedClass.push('label-item')
|
||||
$a.attr('data-label-id', label.id)
|
||||
|
||||
$a.addClass(selectedClass.join(' '))
|
||||
.html("#{colorEl} #{_.escape(label.title)}")
|
||||
|
||||
# Return generated html
|
||||
$li.html($a).prop('outerHTML')
|
||||
persistWhenHide: $dropdown.data('persistWhenHide')
|
||||
search:
|
||||
fields: ['title']
|
||||
selectable: true
|
||||
@@ -280,10 +294,19 @@ class @LabelsSelect
|
||||
else if $dropdown.hasClass('js-filter-submit')
|
||||
$dropdown.closest('form').submit()
|
||||
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'
|
||||
clicked: (label) ->
|
||||
if $dropdown.hasClass('js-filter-bulk-update')
|
||||
return
|
||||
|
||||
page = $('body').data 'page'
|
||||
isIssueIndex = page is 'projects:issues:index'
|
||||
isMRIndex = page is 'projects:merge_requests:index'
|
||||
@@ -298,4 +321,31 @@ class @LabelsSelect
|
||||
return
|
||||
else
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
gl.emojiAliases = ->
|
||||
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
|
||||
@@ -83,7 +83,7 @@ class @MilestoneSelect
|
||||
$selectbox.hide()
|
||||
|
||||
# display:block overrides the hide-collapse rule
|
||||
$value.removeAttr('style')
|
||||
$value.css('display', '')
|
||||
clicked: (selected) ->
|
||||
page = $('body').data 'page'
|
||||
isIssueIndex = page is 'projects:issues:index'
|
||||
@@ -118,7 +118,7 @@ class @MilestoneSelect
|
||||
$dropdown.trigger('loaded.gl.dropdown')
|
||||
$loading.fadeOut()
|
||||
$selectbox.hide()
|
||||
$value.removeAttr('style')
|
||||
$value.css('display', '')
|
||||
if data.milestone?
|
||||
data.milestone.namespace = _this.currentProject.namespace
|
||||
data.milestone.path = _this.currentProject.path
|
||||
|
||||
@@ -162,13 +162,14 @@ class @Notes
|
||||
renderNote: (note) ->
|
||||
unless note.valid
|
||||
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')
|
||||
return
|
||||
|
||||
if note.award
|
||||
awardsHandler.addAwardToEmojiBar(note.note)
|
||||
awardsHandler.scrollToAwards()
|
||||
votesBlock = $('.js-awards-block').eq 0
|
||||
gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
|
||||
gl.awardsHandler.scrollToAwards()
|
||||
|
||||
# render note if it not present in loaded list
|
||||
# or skip if rendered
|
||||
|
||||
@@ -20,8 +20,7 @@ class @SearchAutocomplete
|
||||
@dropdown = @wrap.find('.dropdown')
|
||||
@dropdownContent = @dropdown.find('.dropdown-content')
|
||||
|
||||
@locationBadgeEl = @getElement('.search-location-badge')
|
||||
@locationText = @getElement('.location-text')
|
||||
@locationBadgeEl = @getElement('.location-badge')
|
||||
@scopeInputEl = @getElement('#scope')
|
||||
@searchInput = @getElement('.search-input')
|
||||
@projectInputEl = @getElement('#search_project_id')
|
||||
@@ -133,7 +132,7 @@ class @SearchAutocomplete
|
||||
scope: @scopeInputEl.val()
|
||||
|
||||
# Location badge
|
||||
_location: @locationText.text()
|
||||
_location: @locationBadgeEl.text()
|
||||
}
|
||||
|
||||
bindEvents: ->
|
||||
@@ -143,23 +142,28 @@ class @SearchAutocomplete
|
||||
@searchInput.on 'click', @onSearchInputClick
|
||||
@searchInput.on 'focus', @onSearchInputFocus
|
||||
@clearInput.on 'click', @onClearInputClick
|
||||
@locationBadgeEl.on 'click', =>
|
||||
@searchInput.focus()
|
||||
|
||||
onDocumentClick: (e) =>
|
||||
# If clicking outside the search box
|
||||
# And search input is not focused
|
||||
# And we are not clicking inside a suggestion
|
||||
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
|
||||
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length
|
||||
@onSearchInputBlur()
|
||||
|
||||
enableAutocomplete: ->
|
||||
# No need to enable anything if user is not logged in
|
||||
return if !gon.current_user_id
|
||||
|
||||
_this = @
|
||||
@loadingSuggestions = false
|
||||
unless @dropdown.hasClass('open')
|
||||
_this = @
|
||||
@loadingSuggestions = false
|
||||
|
||||
@dropdown.addClass('open')
|
||||
@searchInput.removeClass('disabled')
|
||||
@dropdown
|
||||
.addClass('open')
|
||||
.trigger('shown.bs.dropdown')
|
||||
@searchInput.removeClass('disabled')
|
||||
|
||||
onSearchInputKeyDown: =>
|
||||
# Saves last length of the entered text
|
||||
@@ -190,7 +194,7 @@ class @SearchAutocomplete
|
||||
@disableAutocomplete()
|
||||
else
|
||||
# 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
|
||||
|
||||
@@ -221,10 +225,8 @@ class @SearchAutocomplete
|
||||
category = if item.category? then "#{item.category}: " else ''
|
||||
value = if item.value? then item.value else ''
|
||||
|
||||
html = "<span class='location-badge'>
|
||||
<i class='location-text'>#{category}#{value}</i>
|
||||
</span>"
|
||||
@locationBadgeEl.html(html)
|
||||
badgeText = "#{category}#{value}"
|
||||
@locationBadgeEl.text(badgeText).show()
|
||||
@wrap.addClass('has-location-badge')
|
||||
|
||||
restoreOriginalState: ->
|
||||
@@ -233,9 +235,8 @@ class @SearchAutocomplete
|
||||
for input in inputs
|
||||
@getElement("##{input}").val(@originalState[input])
|
||||
|
||||
|
||||
if @originalState._location is ''
|
||||
@locationBadgeEl.empty()
|
||||
@locationBadgeEl.hide()
|
||||
else
|
||||
@addLocationBadge(
|
||||
value: @originalState._location
|
||||
@@ -244,7 +245,7 @@ class @SearchAutocomplete
|
||||
@dropdown.removeClass 'open'
|
||||
|
||||
badgePresent: ->
|
||||
@locationBadgeEl.children().length
|
||||
@locationBadgeEl.length
|
||||
|
||||
resetSearchState: ->
|
||||
inputs = Object.keys @originalState
|
||||
@@ -257,7 +258,7 @@ class @SearchAutocomplete
|
||||
@getElement("##{input}").val('')
|
||||
|
||||
removeLocationBadge: ->
|
||||
@locationBadgeEl.empty()
|
||||
@locationBadgeEl.hide()
|
||||
|
||||
# Reset state
|
||||
@resetSearchState()
|
||||
|
||||
@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
|
||||
@replyWithSelectedText()
|
||||
return false
|
||||
)
|
||||
Mousetrap.bind('j', =>
|
||||
@prevIssue()
|
||||
return false
|
||||
)
|
||||
Mousetrap.bind('k', =>
|
||||
@nextIssue()
|
||||
return false
|
||||
)
|
||||
Mousetrap.bind('e', =>
|
||||
@editIssue()
|
||||
return false
|
||||
@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
|
||||
else
|
||||
@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: ->
|
||||
if window.getSelection
|
||||
selected = window.getSelection().toString()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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')
|
||||
@@ -0,0 +1,13 @@
|
||||
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."
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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')
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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
|
||||
hidden: (e) ->
|
||||
$selectbox.hide()
|
||||
# display:block overrides the hide-collapse rule
|
||||
$value.removeAttr('style')
|
||||
$value.css('display', '')
|
||||
|
||||
clicked: (user) ->
|
||||
page = $('body').data 'page'
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
margin-bottom: -$gl-padding;
|
||||
}
|
||||
|
||||
&.content-component-block {
|
||||
padding: 11px 0;
|
||||
background-color: $white-light;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
@@ -122,10 +122,8 @@
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding: 5px 10px;
|
||||
color: $dropdown-link-color;
|
||||
line-height: 34px;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
@@ -162,6 +160,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-large {
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
.dropdown-menu-no-wrap {
|
||||
a {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -232,13 +240,11 @@
|
||||
a {
|
||||
padding-left: 25px;
|
||||
|
||||
&.is-active {
|
||||
&.is-indeterminate, &.is-active {
|
||||
&::before {
|
||||
content: "\f00c";
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
margin-top: -7px;
|
||||
top: 8px;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
@@ -246,6 +252,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-indeterminate::before {
|
||||
content: "\f068";
|
||||
}
|
||||
|
||||
&.is-active::before {
|
||||
content: "\f00c";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,3 +539,14 @@
|
||||
background-color: $calendar-unselectable-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-inner-title {
|
||||
display: block;
|
||||
color: $gl-title-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dropdown-menu-inner-content {
|
||||
display: block;
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
|
||||
@@ -89,8 +89,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$theme-blue: #2980b9;
|
||||
$theme-charcoal: #3d454d;
|
||||
$theme-charcoal-dark: #383f45;
|
||||
$theme-charcoal-text: #b9bbbe;
|
||||
|
||||
$theme-blue: #2980b9;
|
||||
$theme-graphite: #666;
|
||||
$theme-gray: #373737;
|
||||
$theme-green: #019875;
|
||||
@@ -102,7 +105,7 @@ body {
|
||||
}
|
||||
|
||||
&.ui_charcoal {
|
||||
@include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41);
|
||||
@include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark);
|
||||
}
|
||||
|
||||
&.ui_graphite {
|
||||
|
||||
@@ -79,6 +79,10 @@ header {
|
||||
|
||||
&.header-collapsed {
|
||||
padding: 0 16px;
|
||||
|
||||
.side-nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav-toggle {
|
||||
@@ -86,6 +90,7 @@ header {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
margin: 6px 0;
|
||||
font-size: 18px;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background-color: $background-color;
|
||||
@@ -97,10 +102,6 @@ header {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,31 +172,21 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
@mixin collapsed-header {
|
||||
margin-left: $sidebar_collapsed_width;
|
||||
}
|
||||
|
||||
.header-collapsed {
|
||||
margin-left: $sidebar_collapsed_width;
|
||||
margin-left: 0;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
@include collapsed-header;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
margin-left: 0;
|
||||
.header-content {
|
||||
padding-left: 30px;
|
||||
transition-duration: .3s;
|
||||
}
|
||||
}
|
||||
|
||||
.header-expanded {
|
||||
margin-left: $sidebar_collapsed_width;
|
||||
margin-left: 0;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
margin-left: $sidebar_width;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
margin-left: 0;
|
||||
.header-content {
|
||||
padding-left: $sidebar_width;
|
||||
transition-duration: .3s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,18 @@ ul.content-list {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// When dragging a list item
|
||||
&.ui-sortable-helper {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.list-placeholder {
|
||||
background-color: $gray-light;
|
||||
border: dotted 1px $gray-dark;
|
||||
margin: 1px 0;
|
||||
min-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,10 @@
|
||||
* Generic mixins
|
||||
*/
|
||||
@mixin box-shadow($shadow) {
|
||||
-webkit-box-shadow: $shadow;
|
||||
-moz-box-shadow: $shadow;
|
||||
-ms-box-shadow: $shadow;
|
||||
-o-box-shadow: $shadow;
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
|
||||
@mixin border-radius($radius) {
|
||||
-webkit-border-radius: $radius;
|
||||
-moz-border-radius: $radius;
|
||||
-ms-border-radius: $radius;
|
||||
-o-border-radius: $radius;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
%ul.notes .note-role, .note-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-links, .nav-links {
|
||||
li a {
|
||||
font-size: 14px;
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 14px;
|
||||
padding-top: $gl-padding;
|
||||
padding: $gl-btn-padding;
|
||||
padding-bottom: 11px;
|
||||
margin-bottom: -1px;
|
||||
font-size: 15px;
|
||||
@@ -67,6 +66,27 @@
|
||||
color: #78a;
|
||||
}
|
||||
}
|
||||
|
||||
&.sub-nav {
|
||||
background-color: $background-color;
|
||||
|
||||
.container-fluid {
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
a {
|
||||
margin: 0;
|
||||
padding: 11px 10px 9px;
|
||||
}
|
||||
|
||||
&.active a {
|
||||
border-bottom: none;
|
||||
color: $link-underline-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-area {
|
||||
@@ -81,6 +101,10 @@
|
||||
width: 50%;
|
||||
line-height: 28px;
|
||||
|
||||
&.wiki-page {
|
||||
padding: 16px 10px 11px;
|
||||
}
|
||||
|
||||
/* Small devices (phones, tablets, 768px and lower) */
|
||||
@media (max-width: $screen-sm-min) {
|
||||
width: 100%;
|
||||
@@ -104,6 +128,10 @@
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
|
||||
li a {
|
||||
padding: 16px 10px 11px;
|
||||
}
|
||||
|
||||
/* Small devices (phones, tablets, 768px and lower) */
|
||||
@media (max-width: $screen-sm-max) {
|
||||
width: 100%;
|
||||
@@ -309,8 +337,8 @@
|
||||
}
|
||||
|
||||
.nav-control {
|
||||
.fade-right {
|
||||
|
||||
.fade-right {
|
||||
@media (min-width: $screen-xs-max) {
|
||||
right: 67px;
|
||||
}
|
||||
@@ -321,6 +349,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.scrolling-tabs-container {
|
||||
position: relative;
|
||||
|
||||
.nav-links {
|
||||
@include scrolling-links();
|
||||
|
||||
.fade-right {
|
||||
@include fade(left, rgba(255, 255, 255, 0.4), $background-color);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fade-left {
|
||||
@include fade(right, rgba(255, 255, 255, 0.4), $background-color);
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-block {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
#logo {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: 58px;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.page-with-sidebar {
|
||||
padding-top: $header-height;
|
||||
transition-duration: .3s;
|
||||
@@ -20,12 +12,6 @@
|
||||
height: 100%;
|
||||
transition-duration: .3s;
|
||||
}
|
||||
|
||||
.gitlab-text-container-link {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
@@ -50,47 +36,8 @@
|
||||
|
||||
.sidebar-wrapper {
|
||||
.header-logo {
|
||||
border-bottom: 1px solid transparent;
|
||||
float: left;
|
||||
height: $header-height;
|
||||
width: $sidebar_width;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
overflow: hidden;
|
||||
transition-duration: .3s;
|
||||
|
||||
a {
|
||||
float: left;
|
||||
height: $header-height;
|
||||
width: 100%;
|
||||
padding-left: 22px;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
transition-duration: .3s;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
#tanuki-logo, img {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.gitlab-text-container {
|
||||
width: 230px;
|
||||
|
||||
h3 {
|
||||
width: 158px;
|
||||
float: left;
|
||||
margin: 0;
|
||||
margin-left: 50px;
|
||||
font-size: 19px;
|
||||
line-height: 50px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
padding: 8px 26px;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
@@ -98,7 +45,7 @@
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
padding: 7px 22px;
|
||||
padding: 15px 22px;
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
width: $sidebar_width;
|
||||
@@ -126,8 +73,7 @@
|
||||
|
||||
|
||||
.nav-sidebar {
|
||||
margin-top: 14 + $header-height;
|
||||
margin-bottom: 100px;
|
||||
margin: 22px 0;
|
||||
transition-duration: .3s;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
@@ -145,13 +91,12 @@
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 7px 15px;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
font-size: $gl-font-size;
|
||||
line-height: 24px;
|
||||
color: $gray;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding-left: 23px;
|
||||
font-weight: normal;
|
||||
outline: none;
|
||||
|
||||
@@ -166,14 +111,12 @@
|
||||
i {
|
||||
width: 16px;
|
||||
color: $gray-light;
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
.count {
|
||||
float: right;
|
||||
background: #eee;
|
||||
padding: 0 8px;
|
||||
@include border-radius(6px);
|
||||
.nav-link-text {
|
||||
margin-top: 3px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
&.back-link i {
|
||||
@@ -217,25 +160,13 @@
|
||||
}
|
||||
|
||||
.page-sidebar-collapsed {
|
||||
padding-left: $sidebar_collapsed_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
padding-left: 0;
|
||||
}
|
||||
padding-left: 0;
|
||||
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
width: 0;
|
||||
}
|
||||
width: 0;
|
||||
|
||||
.header-logo {
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
width: 0;
|
||||
}
|
||||
width: 0;
|
||||
|
||||
a {
|
||||
padding-left: ($sidebar_collapsed_width - 36) / 2;
|
||||
@@ -246,6 +177,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@@ -261,44 +196,23 @@
|
||||
}
|
||||
|
||||
.collapse-nav a {
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
width: 0;
|
||||
}
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
padding-left: ($sidebar_collapsed_width - 36) / 2;
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
width: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
width: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-nav {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
padding-right: 0;;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-sidebar-expanded {
|
||||
padding-left: $sidebar_collapsed_width;
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
padding-left: $sidebar_width;
|
||||
}
|
||||
padding-left: $sidebar_width;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
padding-left: 0;
|
||||
@@ -328,7 +242,7 @@
|
||||
}
|
||||
|
||||
@media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
|
||||
padding-right: 62px;
|
||||
padding-right: 90px;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
padding: 0;
|
||||
|
||||
.timeline-entry {
|
||||
padding: $gl-padding $gl-btn-padding;
|
||||
padding: $gl-padding $gl-btn-padding 11px;
|
||||
border-color: $table-border-color;
|
||||
color: $gl-gray;
|
||||
border-bottom: 1px solid $border-white-light;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Layout
|
||||
*/
|
||||
$sidebar_collapsed_width: 62px;
|
||||
$sidebar_width: 220px;
|
||||
$sidebar_width: 90px;
|
||||
$gutter_collapsed_width: 62px;
|
||||
$gutter_width: 290px;
|
||||
$gutter_inner_width: 258px;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
@import "framework/variables";
|
||||
|
||||
// This file is largely copied from `highlight/white.scss`, but modified to
|
||||
// avoid all descendant selectors (`table td`). This is because the CSS inlining
|
||||
// we use performs dramatically worse on descendant selectors than the
|
||||
// alternatives.
|
||||
// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632>
|
||||
//
|
||||
// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of
|
||||
// preference): plain class selectors, type (element name) selectors, or
|
||||
// explicit child selectors.
|
||||
|
||||
table.code {
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
@@ -11,33 +21,162 @@ table.code {
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
|
||||
td {
|
||||
> tr > td {
|
||||
line-height: $code_line_height;
|
||||
font-family: monospace;
|
||||
font-size: $code_font_size;
|
||||
}
|
||||
|
||||
td.diff-line-num {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: $background-color;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
padding: 0 5px;
|
||||
border-right: 1px solid $border-color;
|
||||
text-align: right;
|
||||
min-width: 35px;
|
||||
max-width: 50px;
|
||||
width: 35px;
|
||||
}
|
||||
&.diff-line-num {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
padding: 0 5px;
|
||||
border-right: 1px solid;
|
||||
text-align: right;
|
||||
min-width: 35px;
|
||||
max-width: 50px;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
td.line_content {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0 0.5em;
|
||||
border: none;
|
||||
white-space: pre;
|
||||
&.line_content {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0 0.5em;
|
||||
border: none;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import "highlight/white";
|
||||
.line-numbers, .diff-line-num {
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.diff-line-num, .diff-line-num a {
|
||||
color: $black-transparent;
|
||||
}
|
||||
|
||||
pre.code, .diff-line-num {
|
||||
border-color: $table-border-gray;
|
||||
}
|
||||
|
||||
.code.white, pre.code, .line_content {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.diff-line-num {
|
||||
&.old {
|
||||
background-color: $line-number-old;
|
||||
border-color: $line-removed-dark;
|
||||
}
|
||||
|
||||
&.new {
|
||||
background-color: $line-number-new;
|
||||
border-color: $line-added-dark;
|
||||
}
|
||||
|
||||
&.hll:not(.empty-cell) {
|
||||
background-color: $line-number-select;
|
||||
border-color: $line-select-yellow-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.line_content {
|
||||
&.old {
|
||||
background-color: $line-removed;
|
||||
|
||||
> .line > span.idiff, > .line > span > span.idiff {
|
||||
background-color: $line-removed-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.new {
|
||||
background-color: $line-added;
|
||||
|
||||
> .line > span.idiff, > .line > span > span.idiff {
|
||||
background-color: $line-added-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.match {
|
||||
color: $black-transparent;
|
||||
background-color: $match-line;
|
||||
}
|
||||
|
||||
&.hll:not(.empty-cell) {
|
||||
background-color: $line-select-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
pre > .hll {
|
||||
background-color: #f8eec7 !important;
|
||||
}
|
||||
|
||||
span.highlight_word {
|
||||
background-color: #fafe3d !important;
|
||||
}
|
||||
|
||||
.hll { background-color: #f8f8f8 }
|
||||
.c { color: #998; font-style: italic; }
|
||||
.err { color: #a61717; background-color: #e3d2d2; }
|
||||
.k { font-weight: bold; }
|
||||
.o { font-weight: bold; }
|
||||
.cm { color: #998; font-style: italic; }
|
||||
.cp { color: #999; font-weight: bold; }
|
||||
.c1 { color: #998; font-style: italic; }
|
||||
.cs { color: #999; font-weight: bold; font-style: italic; }
|
||||
.gd { color: #000; background-color: #fdd; }
|
||||
.gd .x { color: #000; background-color: #faa; }
|
||||
.ge { font-style: italic; }
|
||||
.gr { color: #a00; }
|
||||
.gh { color: #999; }
|
||||
.gi { color: #000; background-color: #dfd; }
|
||||
.gi .x { color: #000; background-color: #afa; }
|
||||
.go { color: #888; }
|
||||
.gp { color: #555; }
|
||||
.gs { font-weight: bold; }
|
||||
.gu { color: #800080; font-weight: bold; }
|
||||
.gt { color: #a00; }
|
||||
.kc { font-weight: bold; }
|
||||
.kd { font-weight: bold; }
|
||||
.kn { font-weight: bold; }
|
||||
.kp { font-weight: bold; }
|
||||
.kr { font-weight: bold; }
|
||||
.kt { color: #458; font-weight: bold; }
|
||||
.m { color: #099; }
|
||||
.s { color: #d14; }
|
||||
.n { color: #333; }
|
||||
.na { color: teal; }
|
||||
.nb { color: #0086b3; }
|
||||
.nc { color: #458; font-weight: bold; }
|
||||
.no { color: teal; }
|
||||
.ni { color: purple; }
|
||||
.ne { color: #900; font-weight: bold; }
|
||||
.nf { color: #900; font-weight: bold; }
|
||||
.nn { color: #555; }
|
||||
.nt { color: navy; }
|
||||
.nv { color: teal; }
|
||||
.ow { font-weight: bold; }
|
||||
.w { color: #bbb; }
|
||||
.mf { color: #099; }
|
||||
.mh { color: #099; }
|
||||
.mi { color: #099; }
|
||||
.mo { color: #099; }
|
||||
.sb { color: #d14; }
|
||||
.sc { color: #d14; }
|
||||
.sd { color: #d14; }
|
||||
.s2 { color: #d14; }
|
||||
.se { color: #d14; }
|
||||
.sh { color: #d14; }
|
||||
.si { color: #d14; }
|
||||
.sx { color: #d14; }
|
||||
.sr { color: #009926; }
|
||||
.s1 { color: #d14; }
|
||||
.ss { color: #990073; }
|
||||
.bp { color: #999; }
|
||||
.vc { color: teal; }
|
||||
.vg { color: teal; }
|
||||
.vi { color: teal; }
|
||||
.il { color: #099; }
|
||||
.gc { color: #999; background-color: #eaf2f5; }
|
||||
|
||||
@@ -6,19 +6,19 @@ p.details {
|
||||
font-style: italic;
|
||||
color: #777
|
||||
}
|
||||
.footer p {
|
||||
.footer > p {
|
||||
font-size: small;
|
||||
color: #777
|
||||
}
|
||||
pre.commit-message {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.file-stats a {
|
||||
.file-stats > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.file-stats .new-file {
|
||||
color: #090;
|
||||
}
|
||||
.file-stats .deleted-file {
|
||||
color: #b00;
|
||||
> .new-file {
|
||||
color: #090;
|
||||
}
|
||||
> .deleted-file {
|
||||
color: #b00;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.awards {
|
||||
line-height: 34px;
|
||||
|
||||
.emoji-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -9,8 +7,6 @@
|
||||
|
||||
.emoji-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 3px;
|
||||
z-index: 1000;
|
||||
min-width: 160px;
|
||||
@@ -23,7 +19,12 @@
|
||||
opacity: 0;
|
||||
transform: scale(.2);
|
||||
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 {
|
||||
pointer-events: all;
|
||||
@@ -94,6 +95,7 @@
|
||||
|
||||
.award-control {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
line-height: 20px;
|
||||
@@ -107,7 +109,8 @@
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
.award-control-icon {
|
||||
.award-control-icon-normal,
|
||||
.emoji-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
background: #111;
|
||||
color: #fff;
|
||||
font-family: $monospace_font;
|
||||
white-space: pre;
|
||||
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+ */
|
||||
white-space: pre-wrap;
|
||||
overflow: auto;
|
||||
overflow-y: hidden;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
margin-top: 6px;
|
||||
|
||||
p {
|
||||
overflow-x: auto;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
.label-row {
|
||||
.label-name {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
width: 170px;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
@@ -138,3 +138,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prioritized-labels {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.add-priority {
|
||||
display: none;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.other-labels {
|
||||
.remove-priority {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-priority {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
button {
|
||||
border-color: transparent;
|
||||
padding: 5px 8px;
|
||||
vertical-align: top;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,14 @@
|
||||
}
|
||||
|
||||
&.ci-failed,
|
||||
&.ci-canceled,
|
||||
&.ci-error {
|
||||
color: $gl-danger;
|
||||
}
|
||||
|
||||
&.ci-canceled {
|
||||
color: $gl-gray;
|
||||
}
|
||||
|
||||
a.monospace {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
padding: $gl-padding-top $gl-padding;
|
||||
background-color: $white-light;
|
||||
|
||||
@@ -69,6 +69,10 @@ ul.notes {
|
||||
|
||||
.note-edit-form {
|
||||
display: block;
|
||||
|
||||
&.current-note-edit-form + .note-awards {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
|
||||
.discussion-header,
|
||||
.note-header {
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
@@ -215,6 +254,16 @@ ul.notes {
|
||||
color: $notes-action-color;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-actions {
|
||||
@media (max-width: $screen-md-max) {
|
||||
float: none;
|
||||
@@ -228,8 +277,13 @@ ul.notes {
|
||||
|
||||
.note-action-button {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
line-height: 24px;
|
||||
margin-left: 0;
|
||||
line-height: 20px;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
margin-left: 10px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.fa {
|
||||
color: $notes-action-color;
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
|
||||
.container-fluid {
|
||||
position: relative;
|
||||
|
||||
@media (min-width: $screen-md-max) {
|
||||
.row {
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cover-controls {
|
||||
@@ -57,7 +66,6 @@
|
||||
max-width: 86px;
|
||||
min-width: 86px;
|
||||
padding-right: 0;
|
||||
margin: 11px 0;
|
||||
|
||||
@media (max-width: $screen-md-max) {
|
||||
padding-left: 0;
|
||||
@@ -489,9 +497,11 @@ pre.light-well {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-show-activity {
|
||||
.activity-filter-block {
|
||||
margin-top: -1px;
|
||||
|
||||
.activity-filter-block {
|
||||
.controls {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding-right: 20px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
@@ -47,6 +48,7 @@
|
||||
display: inline-block;
|
||||
background-color: $location-badge-bg;
|
||||
vertical-align: top;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
@@ -55,7 +57,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-location-badge, .search-input-wrap {
|
||||
.search-input-wrap {
|
||||
// Fallback if flexbox is not supported
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -156,13 +158,11 @@
|
||||
.search-holder {
|
||||
@media (min-width: $screen-sm-min) {
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-field-holder {
|
||||
-webkit-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
position: relative;
|
||||
margin-right: 0;
|
||||
|
||||
@@ -182,8 +182,8 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def check_2fa_requirement
|
||||
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor?
|
||||
redirect_to new_profile_two_factor_auth_path
|
||||
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
|
||||
redirect_to profile_two_factor_auth_path
|
||||
end
|
||||
end
|
||||
|
||||
@@ -342,6 +342,10 @@ class ApplicationController < ActionController::Base
|
||||
session[:skip_tfa] && session[:skip_tfa] > Time.current
|
||||
end
|
||||
|
||||
def browser_supports_u2f?
|
||||
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
|
||||
end
|
||||
|
||||
def redirect_to_home_page_url?
|
||||
# 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
|
||||
@@ -355,6 +359,13 @@ class ApplicationController < ActionController::Base
|
||||
current_user.nil? && root_path == request.path
|
||||
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
|
||||
|
||||
def set_default_sort
|
||||
|
||||
@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
|
||||
# Returns nil
|
||||
def prompt_for_two_factor(user)
|
||||
session[:otp_user_id] = user.id
|
||||
setup_u2f_authentication(user)
|
||||
render 'devise/sessions/two_factor'
|
||||
end
|
||||
|
||||
render 'devise/sessions/two_factor' and return
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
skip_before_action :check_2fa_requirement
|
||||
|
||||
def new
|
||||
def show
|
||||
unless current_user.otp_secret
|
||||
current_user.otp_secret = User.generate_otp_secret(32)
|
||||
end
|
||||
@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
|
||||
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?
|
||||
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
|
||||
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
|
||||
|
||||
@qr_code = build_qr_code
|
||||
setup_u2f_registration
|
||||
end
|
||||
|
||||
def create
|
||||
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!
|
||||
current_user.save!
|
||||
|
||||
@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
else
|
||||
@error = 'Invalid pin code'
|
||||
@qr_code = build_qr_code
|
||||
setup_u2f_registration
|
||||
render 'show'
|
||||
end
|
||||
end
|
||||
|
||||
render 'new'
|
||||
# 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])
|
||||
|
||||
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
|
||||
|
||||
@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
||||
def issuer_host
|
||||
Gitlab.config.gitlab.host
|
||||
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
|
||||
|
||||
@@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
|
||||
private
|
||||
|
||||
def build
|
||||
@build ||= project.builds.unscoped.find_by!(id: params[:build_id])
|
||||
@build ||= project.builds.find_by!(id: params[:build_id])
|
||||
end
|
||||
|
||||
def artifacts_file
|
||||
|
||||
@@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController
|
||||
redirect_to namespace_project_branches_path(@project.namespace,
|
||||
@project), status: 303
|
||||
end
|
||||
format.js { render status: status[:return_code] }
|
||||
format.js { render nothing: true, status: status[:return_code] }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ class Projects::BuildsController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
@builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC')
|
||||
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
|
||||
@builds = @builds.where("id not in (?)", @build.id)
|
||||
@commit = @build.commit
|
||||
@pipeline = @build.pipeline
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
@@ -81,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController
|
||||
private
|
||||
|
||||
def build
|
||||
@build ||= project.builds.unscoped.find_by!(id: params[:id])
|
||||
@build ||= project.builds.find_by!(id: params[:id])
|
||||
end
|
||||
|
||||
def build_path(build)
|
||||
|
||||
@@ -99,12 +99,12 @@ class Projects::CommitController < Projects::ApplicationController
|
||||
@commit ||= @project.commit(params[:id])
|
||||
end
|
||||
|
||||
def ci_commits
|
||||
@ci_commits ||= project.ci_commits.where(sha: commit.sha)
|
||||
def pipelines
|
||||
@pipelines ||= project.pipelines.where(sha: commit.sha)
|
||||
end
|
||||
|
||||
def ci_builds
|
||||
@ci_builds ||= Ci::Build.where(commit: ci_commits)
|
||||
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
|
||||
end
|
||||
|
||||
def define_show_vars
|
||||
@@ -117,8 +117,8 @@ class Projects::CommitController < Projects::ApplicationController
|
||||
@diff_refs = [commit.parent || commit, commit]
|
||||
@notes_count = commit.notes.count
|
||||
|
||||
@statuses = CommitStatus.where(commit: ci_commits)
|
||||
@builds = Ci::Build.where(commit: ci_commits)
|
||||
@statuses = CommitStatus.where(pipeline: pipelines)
|
||||
@builds = Ci::Build.where(pipeline: pipelines)
|
||||
end
|
||||
|
||||
def assign_change_commit_vars(mr_source_branch)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Projects::IssuesController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
include IssuableActions
|
||||
include ToggleAwardEmoji
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
|
||||
@@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||
|
||||
def show
|
||||
@note = @project.notes.new(noteable: @issue)
|
||||
@notes = @issue.notes.nonawards.with_associations.fresh
|
||||
@notes = @issue.notes.with_associations.fresh
|
||||
@noteable = @issue
|
||||
|
||||
respond_to do |format|
|
||||
@@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||
|
||||
def bulk_update
|
||||
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
|
||||
|
||||
protected
|
||||
@@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||
end
|
||||
alias_method :subscribable_resource, :issue
|
||||
alias_method :issuable, :issue
|
||||
alias_method :awardable, :issue
|
||||
|
||||
def authorize_read_issue!
|
||||
return render_404 unless can?(current_user, :read_issue, @issue)
|
||||
@@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||
:issues_ids,
|
||||
:assignee_id,
|
||||
:milestone_id,
|
||||
:state_event
|
||||
:state_event,
|
||||
label_ids: [],
|
||||
add_label_ids: [],
|
||||
remove_label_ids: []
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,13 +5,14 @@ class Projects::LabelsController < Projects::ApplicationController
|
||||
before_action :label, only: [:edit, :update, :destroy]
|
||||
before_action :authorize_read_label!
|
||||
before_action :authorize_admin_labels!, only: [
|
||||
:new, :create, :edit, :update, :generate, :destroy
|
||||
:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
|
||||
]
|
||||
|
||||
respond_to :js, :html
|
||||
|
||||
def index
|
||||
@labels = @project.labels.page(params[:page])
|
||||
@labels = @project.labels.unprioritized.page(params[:page])
|
||||
@prioritized_labels = @project.labels.prioritized
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
@@ -71,6 +72,30 @@ class Projects::LabelsController < Projects::ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def remove_priority
|
||||
respond_to do |format|
|
||||
if label.update_attribute(:priority, nil)
|
||||
format.json { render json: label }
|
||||
else
|
||||
message = label.errors.full_messages.uniq.join('. ')
|
||||
format.json { render json: { message: message }, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_priorities
|
||||
Label.transaction do
|
||||
params[:label_ids].each_with_index do |label_id, index|
|
||||
label = @project.labels.find_by_id(label_id)
|
||||
label.update_attribute(:priority, index) if label
|
||||
end
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: { message: 'success' } }
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def module_enabled
|
||||
|
||||
@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
include ToggleSubscriptionAction
|
||||
include DiffHelper
|
||||
include IssuableActions
|
||||
include ToggleAwardEmoji
|
||||
|
||||
before_action :module_enabled
|
||||
before_action :merge_request, only: [
|
||||
@@ -57,9 +58,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render json: @merge_request }
|
||||
format.diff { render text: @merge_request.to_diff }
|
||||
format.patch { render text: @merge_request.to_patch }
|
||||
format.json { render json: @merge_request }
|
||||
format.patch { render text: @merge_request.to_patch }
|
||||
format.diff do
|
||||
headers.store(*Gitlab::Workhorse.send_git_diff(@project.repository,
|
||||
@merge_request.diff_base_commit.id,
|
||||
@merge_request.last_commit.id))
|
||||
headers['Content-Disposition'] = 'inline'
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,8 +127,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
@diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
|
||||
@diff_notes_disabled = true
|
||||
|
||||
@ci_commit = @merge_request.ci_commit
|
||||
@statuses = @ci_commit.statuses if @ci_commit
|
||||
@pipeline = @merge_request.pipeline
|
||||
@statuses = @pipeline.statuses if @pipeline
|
||||
|
||||
@note_counts = Note.where(commit_id: @commits.map(&:id)).
|
||||
group(:commit_id).count
|
||||
@@ -190,13 +198,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
if params[:sha] != @merge_request.source_sha
|
||||
@status = :sha_mismatch
|
||||
return
|
||||
end
|
||||
|
||||
TodoService.new.merge_merge_request(merge_request, current_user)
|
||||
|
||||
@merge_request.update(merge_error: nil)
|
||||
|
||||
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.pipeline && @merge_request.pipeline.active?
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
|
||||
.execute(@merge_request)
|
||||
.execute(@merge_request)
|
||||
@status = :merge_when_build_succeeds
|
||||
else
|
||||
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
|
||||
@@ -225,10 +238,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def ci_status
|
||||
ci_commit = @merge_request.ci_commit
|
||||
if ci_commit
|
||||
status = ci_commit.status
|
||||
coverage = ci_commit.try(:coverage)
|
||||
pipeline = @merge_request.pipeline
|
||||
if pipeline
|
||||
status = pipeline.status
|
||||
coverage = pipeline.try(:coverage)
|
||||
|
||||
status ||= "preparing"
|
||||
else
|
||||
@@ -265,6 +278,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
end
|
||||
alias_method :subscribable_resource, :merge_request
|
||||
alias_method :issuable, :merge_request
|
||||
alias_method :awardable, :merge_request
|
||||
|
||||
def closes_issues
|
||||
@closes_issues ||= @merge_request.closes_issues
|
||||
@@ -300,7 +314,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
def define_show_vars
|
||||
# Build a note object for comment form
|
||||
@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
|
||||
@noteable = @merge_request
|
||||
|
||||
@@ -310,8 +324,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
|
||||
@merge_request_diff = @merge_request.merge_request_diff
|
||||
|
||||
@ci_commit = @merge_request.ci_commit
|
||||
@statuses = @ci_commit.statuses if @ci_commit
|
||||
@pipeline = @merge_request.pipeline
|
||||
@statuses = @pipeline.statuses if @pipeline
|
||||
|
||||
if @merge_request.locked_long_ago?
|
||||
@merge_request.unlock_mr
|
||||
@@ -320,8 +334,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def define_widget_vars
|
||||
@ci_commit = @merge_request.ci_commit
|
||||
@ci_commits = [@ci_commit].compact
|
||||
@pipeline = @merge_request.pipeline
|
||||
@pipelines = [@pipeline].compact
|
||||
closes_issues
|
||||
end
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
class Projects::NotesController < Projects::ApplicationController
|
||||
include ToggleAwardEmoji
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_read_note!
|
||||
before_action :authorize_create_note!, only: [:create]
|
||||
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
|
||||
current_fetched_at = Time.now.to_i
|
||||
@@ -56,35 +58,12 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
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
|
||||
|
||||
def note
|
||||
@note ||= @project.notes.find(params[:id])
|
||||
end
|
||||
alias_method :awardable, :note
|
||||
|
||||
def note_to_html(note)
|
||||
render_to_string(
|
||||
@@ -131,13 +110,20 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
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,
|
||||
id: note.id,
|
||||
discussion_id: note.discussion_id,
|
||||
html: note_to_html(note),
|
||||
award: note.is_award,
|
||||
award: false,
|
||||
note: note.note,
|
||||
discussion_html: note_to_discussion_html(note),
|
||||
discussion_with_diff_html: note_to_discussion_with_diff_html(note)
|
||||
@@ -145,7 +131,7 @@ class Projects::NotesController < Projects::ApplicationController
|
||||
else
|
||||
{
|
||||
valid: false,
|
||||
award: note.is_award,
|
||||
award: false,
|
||||
errors: note.errors
|
||||
}
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||
|
||||
def index
|
||||
@scope = params[:scope]
|
||||
all_pipelines = project.ci_commits
|
||||
all_pipelines = project.pipelines
|
||||
@pipelines_count = all_pipelines.count
|
||||
@running_or_pending_count = all_pipelines.running_or_pending.count
|
||||
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
|
||||
@@ -15,7 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def new
|
||||
@pipeline = project.ci_commits.new(ref: @project.default_branch)
|
||||
@pipeline = project.pipelines.new(ref: @project.default_branch)
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -50,7 +50,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def pipeline
|
||||
@pipeline ||= project.ci_commits.find_by!(id: params[:id])
|
||||
@pipeline ||= project.pipelines.find_by!(id: params[:id])
|
||||
end
|
||||
|
||||
def commit
|
||||
|
||||
@@ -139,7 +139,7 @@ class ProjectsController < Projects::ApplicationController
|
||||
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
|
||||
|
||||
@suggestions = {
|
||||
emojis: AwardEmoji.urls,
|
||||
emojis: Gitlab::AwardEmoji.urls,
|
||||
issues: autocomplete.issues,
|
||||
milestones: autocomplete.milestones,
|
||||
mergerequests: autocomplete.merge_requests,
|
||||
|
||||
@@ -30,8 +30,7 @@ class SessionsController < Devise::SessionsController
|
||||
resource.update_attributes(reset_password_token: nil,
|
||||
reset_password_sent_at: nil)
|
||||
end
|
||||
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard"
|
||||
log_audit_event(current_user, with: authenticated_with)
|
||||
log_audit_event(current_user, with: authentication_method)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,7 +53,7 @@ class SessionsController < Devise::SessionsController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def find_user
|
||||
@@ -89,27 +88,6 @@ class SessionsController < Devise::SessionsController
|
||||
find_user.try(:two_factor_enabled?)
|
||||
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
|
||||
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
|
||||
return unless provider.present?
|
||||
@@ -138,4 +116,14 @@ class SessionsController < Devise::SessionsController
|
||||
def load_recaptcha
|
||||
Gitlab::Recaptcha.load_configurations!
|
||||
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
|
||||
|
||||
@@ -224,7 +224,7 @@ class IssuableFinder
|
||||
def sort(items)
|
||||
# Ensure we always have an explicit sort order (instead of inheriting
|
||||
# multiple orders when combining ActiveRecord::Relation objects).
|
||||
params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
|
||||
params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
|
||||
end
|
||||
|
||||
def by_assignee(items)
|
||||
@@ -318,7 +318,11 @@ class IssuableFinder
|
||||
end
|
||||
|
||||
def label_names
|
||||
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
|
||||
if labels?
|
||||
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_related?
|
||||
|
||||
@@ -12,9 +12,9 @@ class NotesFinder
|
||||
when "commit"
|
||||
project.notes.for_commit_id(target_id).non_diff_notes
|
||||
when "issue"
|
||||
project.issues.find(target_id).notes.nonawards.inc_author
|
||||
project.issues.find(target_id).notes.inc_author
|
||||
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"
|
||||
project.snippets.find(target_id).notes
|
||||
else
|
||||
|
||||
@@ -30,7 +30,7 @@ class TodosFinder
|
||||
items = by_state(items)
|
||||
items = by_type(items)
|
||||
|
||||
items
|
||||
items.reorder(id: :desc)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -78,6 +78,16 @@ class TodosFinder
|
||||
@project
|
||||
end
|
||||
|
||||
def projects
|
||||
return @projects if defined?(@projects)
|
||||
|
||||
if project?
|
||||
@projects = project
|
||||
else
|
||||
@projects = ProjectsFinder.new.execute(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def type?
|
||||
type.present? && ['Issue', 'MergeRequest'].include?(type)
|
||||
end
|
||||
@@ -105,6 +115,8 @@ class TodosFinder
|
||||
def by_project(items)
|
||||
if project?
|
||||
items = items.where(project: project)
|
||||
elsif projects
|
||||
items = items.merge(projects).joins(:project)
|
||||
end
|
||||
|
||||
items
|
||||
|
||||
@@ -66,7 +66,7 @@ module AuthHelper
|
||||
|
||||
def two_factor_skippable?
|
||||
current_application_settings.require_two_factor_authentication &&
|
||||
!current_user.two_factor_enabled &&
|
||||
!current_user.two_factor_enabled? &&
|
||||
current_application_settings.two_factor_grace_period &&
|
||||
!two_factor_grace_period_expired?
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ module ButtonHelper
|
||||
|
||||
content_tag :a, protocol,
|
||||
class: klass,
|
||||
href: @project.http_url_to_repo,
|
||||
href: project.http_url_to_repo,
|
||||
data: {
|
||||
html: true,
|
||||
placement: 'right',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module CiStatusHelper
|
||||
def ci_status_path(ci_commit)
|
||||
project = ci_commit.project
|
||||
builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
|
||||
def ci_status_path(pipeline)
|
||||
project = pipeline.project
|
||||
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
|
||||
end
|
||||
|
||||
def ci_status_with_icon(status, target = nil)
|
||||
|
||||
@@ -31,7 +31,7 @@ module GroupsHelper
|
||||
if group && group.avatar.present?
|
||||
group.avatar.url
|
||||
else
|
||||
'no_group_avatar.png'
|
||||
image_path('no_group_avatar.png')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -8,14 +8,6 @@ module IssuablesHelper
|
||||
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
|
||||
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)
|
||||
# current_labels may be a string from before
|
||||
if current_labels.is_a?(Array)
|
||||
@@ -45,10 +37,6 @@ module IssuablesHelper
|
||||
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)
|
||||
return default_label if user_id.nil?
|
||||
return "Unassigned" if user_id == "0"
|
||||
@@ -96,5 +84,4 @@ module IssuablesHelper
|
||||
issuable.open? ? :opened : :closed
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -145,16 +145,14 @@ module IssuesHelper
|
||||
end
|
||||
end
|
||||
|
||||
def emoji_author_list(notes, current_user)
|
||||
list = notes.map do |note|
|
||||
note.author == current_user ? "me" : note.author.name
|
||||
end
|
||||
|
||||
list.join(", ")
|
||||
def award_user_list(awards, current_user)
|
||||
awards.map do |award|
|
||||
award.user == current_user ? 'me' : award.user.name
|
||||
end.join(', ')
|
||||
end
|
||||
|
||||
def note_active_class(notes, current_user)
|
||||
if current_user && notes.pluck(:author_id).include?(current_user.id)
|
||||
def award_active_class(awards, current_user)
|
||||
if current_user && awards.find { |a| a.user_id == current_user.id }
|
||||
"active"
|
||||
else
|
||||
""
|
||||
|
||||
@@ -31,6 +31,21 @@ module NotificationsHelper
|
||||
end
|
||||
end
|
||||
|
||||
def notification_description(level)
|
||||
case level.to_sym
|
||||
when :participating
|
||||
'You will only receive notifications from related resources'
|
||||
when :mention
|
||||
'You will receive notifications only for comments in which you were @mentioned'
|
||||
when :watch
|
||||
'You will receive notifications for any activity'
|
||||
when :disabled
|
||||
'You will not get any notifications via email'
|
||||
when :global
|
||||
'Use your global notification setting'
|
||||
end
|
||||
end
|
||||
|
||||
def notification_list_item(level, setting)
|
||||
title = notification_title(level)
|
||||
|
||||
@@ -39,9 +54,10 @@ module NotificationsHelper
|
||||
notification_title: title
|
||||
}
|
||||
|
||||
content_tag(:li, class: ('active' if setting.level == level)) do
|
||||
link_to '#', class: 'update-notification', data: data do
|
||||
notification_icon(level, title)
|
||||
content_tag(:li, role: "menuitem") do
|
||||
link_to '#', class: "update-notification #{('is-active' if setting.level == level)}", data: data do
|
||||
link_output = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
|
||||
link_output << content_tag(:span, notification_description(level), class: 'dropdown-menu-inner-content')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,7 +14,8 @@ module SortingHelper
|
||||
sort_value_recently_signin => sort_title_recently_signin,
|
||||
sort_value_oldest_signin => sort_title_oldest_signin,
|
||||
sort_value_downvotes => sort_title_downvotes,
|
||||
sort_value_upvotes => sort_title_upvotes
|
||||
sort_value_upvotes => sort_title_upvotes,
|
||||
sort_value_priority => sort_title_priority
|
||||
}
|
||||
end
|
||||
|
||||
@@ -28,6 +29,10 @@ module SortingHelper
|
||||
}
|
||||
end
|
||||
|
||||
def sort_title_priority
|
||||
'Priority'
|
||||
end
|
||||
|
||||
def sort_title_oldest_updated
|
||||
'Oldest updated'
|
||||
end
|
||||
@@ -84,6 +89,10 @@ module SortingHelper
|
||||
'Most popular'
|
||||
end
|
||||
|
||||
def sort_value_priority
|
||||
'priority'
|
||||
end
|
||||
|
||||
def sort_value_oldest_updated
|
||||
'updated_asc'
|
||||
end
|
||||
|
||||
@@ -17,7 +17,9 @@ module TodosHelper
|
||||
|
||||
def todo_target_link(todo)
|
||||
target = todo.target_type.titleize.downcase
|
||||
link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title }
|
||||
link_to "#{target} #{todo.target_reference}", todo_target_path(todo),
|
||||
class: 'has-tooltip',
|
||||
title: todo.target.title
|
||||
end
|
||||
|
||||
def todo_target_path(todo)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
+13
-12
@@ -45,8 +45,8 @@ module Ci
|
||||
new_build.options = build.options
|
||||
new_build.commands = build.commands
|
||||
new_build.tag_list = build.tag_list
|
||||
new_build.gl_project_id = build.gl_project_id
|
||||
new_build.commit_id = build.commit_id
|
||||
new_build.project = build.project
|
||||
new_build.pipeline = build.pipeline
|
||||
new_build.name = build.name
|
||||
new_build.allow_failure = build.allow_failure
|
||||
new_build.stage = build.stage
|
||||
@@ -66,7 +66,7 @@ module Ci
|
||||
# We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
|
||||
around_transition any => [:success, :failed, :canceled] do |build, block|
|
||||
block.call
|
||||
build.commit.create_next_builds(build) if build.commit
|
||||
build.pipeline.create_next_builds(build) if build.pipeline
|
||||
end
|
||||
|
||||
after_transition any => [:success, :failed, :canceled] do |build|
|
||||
@@ -80,7 +80,7 @@ module Ci
|
||||
end
|
||||
|
||||
def retried?
|
||||
!self.commit.statuses.latest.include?(self)
|
||||
!self.pipeline.statuses.latest.include?(self)
|
||||
end
|
||||
|
||||
def retry
|
||||
@@ -89,7 +89,7 @@ module Ci
|
||||
|
||||
def depends_on_builds
|
||||
# Get builds of the same type
|
||||
latest_builds = self.commit.builds.latest
|
||||
latest_builds = self.pipeline.builds.latest
|
||||
|
||||
# Return builds from previous stages
|
||||
latest_builds.where('stage_idx < ?', stage_idx)
|
||||
@@ -114,16 +114,16 @@ module Ci
|
||||
|
||||
def merge_request
|
||||
merge_requests = MergeRequest.includes(:merge_request_diff)
|
||||
.where(source_branch: ref, source_project_id: commit.gl_project_id)
|
||||
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
|
||||
.reorder(iid: :asc)
|
||||
|
||||
merge_requests.find do |merge_request|
|
||||
merge_request.commits.any? { |ci| ci.id == commit.sha }
|
||||
merge_request.commits.any? { |ci| ci.id == pipeline.sha }
|
||||
end
|
||||
end
|
||||
|
||||
def project_id
|
||||
commit.project.id
|
||||
pipeline.project_id
|
||||
end
|
||||
|
||||
def project_name
|
||||
@@ -313,6 +313,7 @@ module Ci
|
||||
build_data = Gitlab::BuildDataBuilder.build(self)
|
||||
project.execute_hooks(build_data.dup, :build_hooks)
|
||||
project.execute_services(build_data.dup, :build_hooks)
|
||||
project.running_or_pending_build_count(force: true)
|
||||
end
|
||||
|
||||
def artifacts?
|
||||
@@ -359,8 +360,8 @@ module Ci
|
||||
end
|
||||
|
||||
def global_yaml_variables
|
||||
if commit.config_processor
|
||||
commit.config_processor.global_variables.map do |key, value|
|
||||
if pipeline.config_processor
|
||||
pipeline.config_processor.global_variables.map do |key, value|
|
||||
{ key: key, value: value, public: true }
|
||||
end
|
||||
else
|
||||
@@ -369,8 +370,8 @@ module Ci
|
||||
end
|
||||
|
||||
def job_yaml_variables
|
||||
if commit.config_processor
|
||||
commit.config_processor.job_variables(name).map do |key, value|
|
||||
if pipeline.config_processor
|
||||
pipeline.config_processor.job_variables(name).map do |key, value|
|
||||
{ key: key, value: value, public: true }
|
||||
end
|
||||
else
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
module Ci
|
||||
class Commit < ActiveRecord::Base
|
||||
class Pipeline < ActiveRecord::Base
|
||||
extend Ci::Model
|
||||
include Statuseable
|
||||
|
||||
self.table_name = 'ci_commits'
|
||||
|
||||
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
|
||||
has_many :statuses, class_name: 'CommitStatus'
|
||||
has_many :builds, class_name: 'Ci::Build'
|
||||
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
|
||||
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
|
||||
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
|
||||
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
|
||||
|
||||
validates_presence_of :sha
|
||||
validates_presence_of :status
|
||||
@@ -21,7 +23,7 @@ module Ci
|
||||
|
||||
def self.stages
|
||||
# We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
|
||||
CommitStatus.where(commit: pluck(:id)).stages
|
||||
CommitStatus.where(pipeline: pluck(:id)).stages
|
||||
end
|
||||
|
||||
def project_id
|
||||
@@ -47,7 +49,7 @@ module Ci
|
||||
end
|
||||
|
||||
def short_sha
|
||||
Ci::Commit.truncate_sha(sha)
|
||||
Ci::Pipeline.truncate_sha(sha)
|
||||
end
|
||||
|
||||
def commit_data
|
||||
@@ -3,7 +3,7 @@ module Ci
|
||||
extend Ci::Model
|
||||
|
||||
belongs_to :trigger, class_name: 'Ci::Trigger'
|
||||
belongs_to :commit, class_name: 'Ci::Commit'
|
||||
belongs_to :commit, class_name: 'Ci::Pipeline', foreign_key: :commit_id
|
||||
has_many :builds, class_name: 'Ci::Build'
|
||||
|
||||
serialize :variables
|
||||
|
||||
@@ -214,13 +214,13 @@ class Commit
|
||||
@raw.short_id(7)
|
||||
end
|
||||
|
||||
def ci_commits
|
||||
@ci_commits ||= project.ci_commits.where(sha: sha)
|
||||
def pipelines
|
||||
@pipeline ||= project.pipelines.where(sha: sha)
|
||||
end
|
||||
|
||||
def status
|
||||
return @status if defined?(@status)
|
||||
@status ||= ci_commits.status
|
||||
@status ||= pipelines.status
|
||||
end
|
||||
|
||||
def revert_branch_name
|
||||
|
||||
@@ -4,10 +4,10 @@ class CommitStatus < ActiveRecord::Base
|
||||
self.table_name = 'ci_builds'
|
||||
|
||||
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
|
||||
belongs_to :commit, class_name: 'Ci::Commit', touch: true
|
||||
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
|
||||
belongs_to :user
|
||||
|
||||
validates :commit, presence: true
|
||||
validates :pipeline, presence: true
|
||||
|
||||
validates_presence_of :name
|
||||
|
||||
@@ -44,18 +44,18 @@ class CommitStatus < ActiveRecord::Base
|
||||
end
|
||||
|
||||
after_transition [:pending, :running] => :success do |commit_status|
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
|
||||
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
|
||||
end
|
||||
|
||||
after_transition any => :failed do |commit_status|
|
||||
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status)
|
||||
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
|
||||
end
|
||||
end
|
||||
|
||||
delegate :sha, :short_sha, to: :commit
|
||||
delegate :sha, :short_sha, to: :pipeline
|
||||
|
||||
def before_sha
|
||||
commit.before_sha || Gitlab::Git::BLANK_SHA
|
||||
pipeline.before_sha || Gitlab::Git::BLANK_SHA
|
||||
end
|
||||
|
||||
def self.stages
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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
|
||||
include Mentionable
|
||||
include Subscribable
|
||||
include StripAttribute
|
||||
include Awardable
|
||||
|
||||
included do
|
||||
belongs_to :author, class_name: "User"
|
||||
@@ -68,6 +69,14 @@ module Issuable
|
||||
strip_attributes :title
|
||||
|
||||
acts_as_paranoid
|
||||
|
||||
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
|
||||
|
||||
def update_assignee_cache_counts
|
||||
# make sure we flush the cache for both the old *and* new assignee
|
||||
User.find(assignee_id_was).update_cache_counts if assignee_id_was
|
||||
assignee.update_cache_counts if assignee
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
@@ -96,38 +105,22 @@ module Issuable
|
||||
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
|
||||
end
|
||||
|
||||
def sort(method)
|
||||
def sort(method, excluded_labels: [])
|
||||
case method.to_s
|
||||
when 'milestone_due_asc' then order_milestone_due_asc
|
||||
when 'milestone_due_desc' then order_milestone_due_desc
|
||||
when 'downvotes_desc' then order_downvotes_desc
|
||||
when 'upvotes_desc' then order_upvotes_desc
|
||||
when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
|
||||
else
|
||||
order_by(method)
|
||||
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")
|
||||
def order_labels_priority(excluded_labels: [])
|
||||
select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
|
||||
group(arel_table[:id]).
|
||||
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
|
||||
end
|
||||
|
||||
def with_label(title, sort = nil)
|
||||
@@ -153,6 +146,20 @@ module Issuable
|
||||
|
||||
grouping_columns
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def highest_label_priority(excluded_labels)
|
||||
query = Label.select(Label.arel_table[:priority].minimum).
|
||||
joins(:label_links).
|
||||
where(label_links: { target_type: name }).
|
||||
where("label_links.target_id = #{table_name}.id").
|
||||
reorder(nil)
|
||||
|
||||
query.where.not(title: excluded_labels) if excluded_labels.present?
|
||||
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
def today?
|
||||
@@ -163,10 +170,6 @@ module Issuable
|
||||
today? && created_at == updated_at
|
||||
end
|
||||
|
||||
def is_assigned?
|
||||
!!assignee_id
|
||||
end
|
||||
|
||||
def is_being_reassigned?
|
||||
assignee_id_changed?
|
||||
end
|
||||
@@ -175,14 +178,6 @@ module Issuable
|
||||
opened? || reopened?
|
||||
end
|
||||
|
||||
def downvotes
|
||||
notes.awards.where(note: "thumbsdown").count
|
||||
end
|
||||
|
||||
def upvotes
|
||||
notes.awards.where(note: "thumbsup").count
|
||||
end
|
||||
|
||||
def user_notes_count
|
||||
notes.user.count
|
||||
end
|
||||
@@ -205,6 +200,10 @@ module Issuable
|
||||
hook_data
|
||||
end
|
||||
|
||||
def labels_array
|
||||
labels.to_a
|
||||
end
|
||||
|
||||
def label_names
|
||||
labels.order('title ASC').pluck(:title)
|
||||
end
|
||||
|
||||
+2
-2
@@ -75,10 +75,10 @@ class Issue < ActiveRecord::Base
|
||||
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
|
||||
end
|
||||
|
||||
def self.sort(method)
|
||||
def self.sort(method, excluded_labels: [])
|
||||
case method.to_s
|
||||
when 'due_date_asc' then order_due_date_asc
|
||||
when 'due_date_desc' then order_due_date_desc
|
||||
when 'due_date_desc' then order_due_date_desc
|
||||
else
|
||||
super
|
||||
end
|
||||
|
||||
@@ -26,10 +26,20 @@ class Label < ActiveRecord::Base
|
||||
format: { with: /\A[^&\?,]+\z/ },
|
||||
uniqueness: { scope: :project_id }
|
||||
|
||||
before_save :nullify_priority
|
||||
|
||||
default_scope { order(title: :asc) }
|
||||
|
||||
scope :templates, -> { where(template: true) }
|
||||
|
||||
def self.prioritized
|
||||
where.not(priority: nil).reorder(:priority, :title)
|
||||
end
|
||||
|
||||
def self.unprioritized
|
||||
where(priority: nil)
|
||||
end
|
||||
|
||||
alias_attribute :name, :title
|
||||
|
||||
def self.reference_prefix
|
||||
@@ -118,4 +128,8 @@ class Label < ActiveRecord::Base
|
||||
id
|
||||
end
|
||||
end
|
||||
|
||||
def nullify_priority
|
||||
self.priority = nil if priority.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -110,6 +110,10 @@ class LegacyDiffNote < Note
|
||||
@active
|
||||
end
|
||||
|
||||
def award_emoji_supported?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_diff
|
||||
|
||||
@@ -313,13 +313,6 @@ class MergeRequest < ActiveRecord::Base
|
||||
)
|
||||
end
|
||||
|
||||
# Returns the raw diff for this merge request
|
||||
#
|
||||
# see "git diff"
|
||||
def to_diff
|
||||
target_project.repository.diff_text(diff_base_commit.sha, source_sha)
|
||||
end
|
||||
|
||||
# Returns the commit as a series of email patches.
|
||||
#
|
||||
# see "git format-patch"
|
||||
@@ -579,8 +572,8 @@ class MergeRequest < ActiveRecord::Base
|
||||
diverged_commits_count > 0
|
||||
end
|
||||
|
||||
def ci_commit
|
||||
@ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project
|
||||
def pipeline
|
||||
@pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project
|
||||
end
|
||||
|
||||
def diff_refs
|
||||
|
||||
+8
-45
@@ -3,6 +3,7 @@ class Note < ActiveRecord::Base
|
||||
include Gitlab::CurrentSettings
|
||||
include Participable
|
||||
include Mentionable
|
||||
include Awardable
|
||||
|
||||
default_value_for :system, false
|
||||
|
||||
@@ -21,11 +22,8 @@ class Note < ActiveRecord::Base
|
||||
delegate :name, :email, to: :author, prefix: true
|
||||
delegate :title, to: :noteable, allow_nil: true
|
||||
|
||||
before_validation :set_award!
|
||||
|
||||
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
|
||||
validates :attachment, file_size: { maximum: :max_attachment_size }
|
||||
|
||||
@@ -43,8 +41,6 @@ class Note < ActiveRecord::Base
|
||||
mount_uploader :attachment, AttachmentUploader
|
||||
|
||||
# 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 :system, ->{ where(system: true) }
|
||||
scope :user, ->{ where(system: false) }
|
||||
@@ -109,19 +105,6 @@ class Note < ActiveRecord::Base
|
||||
found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
|
||||
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
|
||||
|
||||
def cross_reference?
|
||||
@@ -205,44 +188,24 @@ class Note < ActiveRecord::Base
|
||||
Event.reset_event_cache_for(self)
|
||||
end
|
||||
|
||||
def downvote?
|
||||
is_award && note == "thumbsdown"
|
||||
end
|
||||
|
||||
def upvote?
|
||||
is_award && note == "thumbsup"
|
||||
end
|
||||
|
||||
def editable?
|
||||
!system? && !is_award
|
||||
!system?
|
||||
end
|
||||
|
||||
def cross_reference_not_visible_for?(user)
|
||||
cross_reference? && referenced_mentionables(user).empty?
|
||||
end
|
||||
|
||||
# Checks if note is an award added as a comment
|
||||
#
|
||||
# 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
|
||||
def award_emoji?
|
||||
award_emoji_supported? && contains_emoji_only?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_blank_line_code!
|
||||
self.line_code = nil if self.line_code.blank?
|
||||
end
|
||||
|
||||
def awards_supported?
|
||||
(for_issue? || for_merge_request?) && !diff_note?
|
||||
def award_emoji_supported?
|
||||
noteable.is_a?(Awardable)
|
||||
end
|
||||
|
||||
def contains_emoji_only?
|
||||
@@ -251,6 +214,6 @@ class Note < ActiveRecord::Base
|
||||
|
||||
def award_emoji_name
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class NotificationSetting < ActiveRecord::Base
|
||||
enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 }
|
||||
enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0 }
|
||||
|
||||
default_value_for :level, NotificationSetting.levels[:global]
|
||||
|
||||
|
||||
+23
-5
@@ -119,7 +119,7 @@ class Project < ActiveRecord::Base
|
||||
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
|
||||
|
||||
has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id
|
||||
has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
|
||||
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
|
||||
has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
|
||||
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
|
||||
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
|
||||
@@ -930,12 +930,12 @@ class Project < ActiveRecord::Base
|
||||
!namespace.share_with_group_lock
|
||||
end
|
||||
|
||||
def ci_commit(sha, ref)
|
||||
ci_commits.order(id: :desc).find_by(sha: sha, ref: ref)
|
||||
def pipeline(sha, ref)
|
||||
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
|
||||
end
|
||||
|
||||
def ensure_ci_commit(sha, ref)
|
||||
ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref)
|
||||
def ensure_pipeline(sha, ref)
|
||||
pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref)
|
||||
end
|
||||
|
||||
def enable_ci
|
||||
@@ -1011,4 +1011,22 @@ class Project < ActiveRecord::Base
|
||||
|
||||
update_attribute(:pending_delete, true)
|
||||
end
|
||||
|
||||
def running_or_pending_build_count(force: false)
|
||||
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
|
||||
builds.running_or_pending.count(:all)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_import_as_failed(error_message)
|
||||
original_errors = errors.dup
|
||||
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
|
||||
|
||||
import_fail
|
||||
update_column(:import_error, sanitized_message)
|
||||
rescue ActiveRecord::ActiveRecordError => e
|
||||
Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
|
||||
ensure
|
||||
@errors = original_errors
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,7 +83,7 @@ class IrkerService < Service
|
||||
self.channels = recipients.split(/\s+/).map do |recipient|
|
||||
format_channel(recipient)
|
||||
end
|
||||
channels.reject! &:nil?
|
||||
channels.reject!(&:nil?)
|
||||
end
|
||||
|
||||
def format_channel(recipient)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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
|
||||
+52
-11
@@ -27,7 +27,6 @@ class User < ActiveRecord::Base
|
||||
|
||||
devise :two_factor_authenticatable,
|
||||
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
|
||||
serialize :otp_backup_codes, JSON
|
||||
@@ -51,6 +50,7 @@ class User < ActiveRecord::Base
|
||||
has_many :keys, dependent: :destroy
|
||||
has_many :emails, dependent: :destroy
|
||||
has_many :identities, dependent: :destroy, autosave: true
|
||||
has_many :u2f_registrations, dependent: :destroy
|
||||
|
||||
# Groups
|
||||
has_many :members, dependent: :destroy
|
||||
@@ -84,6 +84,7 @@ class User < ActiveRecord::Base
|
||||
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
|
||||
has_many :todos, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
has_many :award_emoji, as: :awardable, dependent: :destroy
|
||||
|
||||
#
|
||||
# Validations
|
||||
@@ -174,8 +175,16 @@ class User < ActiveRecord::Base
|
||||
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 :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
|
||||
@@ -322,14 +331,29 @@ class User < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
update_attributes(
|
||||
two_factor_enabled: false,
|
||||
encrypted_otp_secret: nil,
|
||||
encrypted_otp_secret_iv: nil,
|
||||
encrypted_otp_secret_salt: nil,
|
||||
otp_grace_period_started_at: nil,
|
||||
otp_backup_codes: nil
|
||||
)
|
||||
transaction do
|
||||
update_attributes(
|
||||
otp_required_for_login: false,
|
||||
encrypted_otp_secret: nil,
|
||||
encrypted_otp_secret_iv: nil,
|
||||
encrypted_otp_secret_salt: 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
|
||||
|
||||
def namespace_uniq
|
||||
@@ -776,6 +800,23 @@ class User < ActiveRecord::Base
|
||||
notification_settings.find_or_initialize_by(source: source)
|
||||
end
|
||||
|
||||
def assigned_open_merge_request_count(force: false)
|
||||
Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
|
||||
assigned_merge_requests.opened.count
|
||||
end
|
||||
end
|
||||
|
||||
def assigned_open_issues_count(force: false)
|
||||
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
|
||||
assigned_issues.opened.count
|
||||
end
|
||||
end
|
||||
|
||||
def update_cache_counts
|
||||
assigned_open_merge_request_count(force: true)
|
||||
assigned_open_issues_count(force: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def projects_union
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module Ci
|
||||
class CreateBuildsService
|
||||
def initialize(commit)
|
||||
@commit = commit
|
||||
def initialize(pipeline)
|
||||
@pipeline = pipeline
|
||||
end
|
||||
|
||||
def execute(stage, user, status, trigger_request = nil)
|
||||
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request)
|
||||
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
|
||||
|
||||
# check when to create next build
|
||||
builds_attrs = builds_attrs.select do |build_attrs|
|
||||
@@ -21,8 +21,8 @@ module Ci
|
||||
|
||||
builds_attrs.map do |build_attrs|
|
||||
# don't create the same build twice
|
||||
unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag,
|
||||
trigger_request: trigger_request, name: build_attrs[:name])
|
||||
unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag,
|
||||
trigger_request: trigger_request, name: build_attrs[:name])
|
||||
build_attrs.slice!(:name,
|
||||
:commands,
|
||||
:tag_list,
|
||||
@@ -31,13 +31,13 @@ module Ci
|
||||
:stage,
|
||||
:stage_idx)
|
||||
|
||||
build_attrs.merge!(ref: @commit.ref,
|
||||
tag: @commit.tag,
|
||||
build_attrs.merge!(ref: @pipeline.ref,
|
||||
tag: @pipeline.tag,
|
||||
trigger_request: trigger_request,
|
||||
user: user,
|
||||
project: @commit.project)
|
||||
project: @pipeline.project)
|
||||
|
||||
@commit.builds.create!(build_attrs)
|
||||
@pipeline.builds.create!(build_attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -45,7 +45,7 @@ module Ci
|
||||
private
|
||||
|
||||
def config_processor
|
||||
@config_processor ||= @commit.config_processor
|
||||
@config_processor ||= @pipeline.config_processor
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Ci
|
||||
class CreatePipelineService < BaseService
|
||||
def execute
|
||||
pipeline = project.ci_commits.new(params)
|
||||
pipeline = project.pipelines.new(params)
|
||||
|
||||
unless ref_names.include?(params[:ref])
|
||||
pipeline.errors.add(:base, 'Reference not found')
|
||||
@@ -19,7 +19,7 @@ module Ci
|
||||
end
|
||||
|
||||
begin
|
||||
Ci::Commit.transaction do
|
||||
Ci::Pipeline.transaction do
|
||||
pipeline.sha = commit.id
|
||||
|
||||
unless pipeline.config_processor
|
||||
|
||||
@@ -7,14 +7,14 @@ module Ci
|
||||
# check if ref is tag
|
||||
tag = project.repository.find_tag(ref).present?
|
||||
|
||||
ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag)
|
||||
pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag)
|
||||
|
||||
trigger_request = trigger.trigger_requests.create!(
|
||||
variables: variables,
|
||||
commit: ci_commit,
|
||||
commit: pipeline,
|
||||
)
|
||||
|
||||
if ci_commit.create_builds(nil, trigger_request)
|
||||
if pipeline.create_builds(nil, trigger_request)
|
||||
trigger_request
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,9 +3,9 @@ module Ci
|
||||
def execute(project, opts)
|
||||
sha = opts[:sha] || ref_sha(project, opts[:ref])
|
||||
|
||||
ci_commits = project.ci_commits.where(sha: sha)
|
||||
ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref]
|
||||
image_name = image_for_status(ci_commits.status)
|
||||
pipelines = project.pipelines.where(sha: sha)
|
||||
pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
|
||||
image_name = image_for_status(pipelines.status)
|
||||
|
||||
image_path = Rails.root.join('public/ci', image_name)
|
||||
OpenStruct.new(path: image_path, name: image_name)
|
||||
|
||||
@@ -18,23 +18,23 @@ class CreateCommitBuildsService
|
||||
return false
|
||||
end
|
||||
|
||||
commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
|
||||
pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
|
||||
|
||||
# Skip creating ci_commit when no gitlab-ci.yml is found
|
||||
unless commit.ci_yaml_file
|
||||
# Skip creating pipeline when no gitlab-ci.yml is found
|
||||
unless pipeline.ci_yaml_file
|
||||
return false
|
||||
end
|
||||
|
||||
# Create a new ci_commit
|
||||
commit.save!
|
||||
# Create a new pipeline
|
||||
pipeline.save!
|
||||
|
||||
# Skip creating builds for commits that have [ci skip]
|
||||
unless commit.skip_ci?
|
||||
unless pipeline.skip_ci?
|
||||
# Create builds for commit
|
||||
commit.create_builds(user)
|
||||
pipeline.create_builds(user)
|
||||
end
|
||||
|
||||
commit.touch
|
||||
commit
|
||||
pipeline.touch
|
||||
pipeline
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user