mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-23 19:46:12 +10:00
Merge remote-tracking branch 'origin/master' into ci-lfs-fetch
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly.
|
||||
|
||||
We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account.
|
||||
@@ -0,0 +1,3 @@
|
||||
Thank you for taking the time to contribute back to GitLab!
|
||||
|
||||
Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account.
|
||||
+169
-197
@@ -2,7 +2,7 @@ image: "ruby:2.1"
|
||||
|
||||
services:
|
||||
- mysql:latest
|
||||
- redis:latest
|
||||
- redis:alpine
|
||||
|
||||
cache:
|
||||
key: "ruby21"
|
||||
@@ -13,100 +13,189 @@ 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
|
||||
only:
|
||||
- master
|
||||
|
||||
# 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 +204,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
|
||||
|
||||
+7
-3
@@ -13,7 +13,8 @@ AllCops:
|
||||
# Exclude some GitLab files
|
||||
Exclude:
|
||||
- 'vendor/**/*'
|
||||
- 'db/**/*'
|
||||
- 'db/*'
|
||||
- 'db/fixtures/**/*'
|
||||
- 'tmp/**/*'
|
||||
- 'bin/**/*'
|
||||
- 'lib/backup/**/*'
|
||||
@@ -194,7 +195,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 +772,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.
|
||||
@@ -1088,6 +1089,9 @@ Rails/TimeZone:
|
||||
Rails/Validation:
|
||||
Enabled: false
|
||||
|
||||
Rails/UniqBeforePluck:
|
||||
Enabled: false
|
||||
|
||||
##################### RSpec ##################################
|
||||
|
||||
# Check that instances are not being stubbed globally.
|
||||
|
||||
@@ -1,39 +1,103 @@
|
||||
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)
|
||||
- Fix endless redirections when accessing user OAuth applications when they are disabled
|
||||
- Allow enabling wiki page events from Webhook management UI
|
||||
- Bump rouge to 1.11.0
|
||||
- Fix issue with arrow keys not working in search autocomplete dropdown
|
||||
- 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)
|
||||
- Allow customisable text on the 'nearly there' page after a user signs up
|
||||
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
|
||||
- Allow forking projects with restricted visibility level
|
||||
- Added descriptions to notification settings dropdown
|
||||
- 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
|
||||
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
|
||||
- Bump nokogiri to 1.6.8
|
||||
- Use gitlab-shell v3.0.0
|
||||
- Upgrade to jQuery 2
|
||||
- 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)
|
||||
- Links from a wiki page to other wiki pages should be rewritten as expected
|
||||
- Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
|
||||
- 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
|
||||
- Allow to use CI token to fetch LFS objects
|
||||
- Projects pending deletion will render a 404 page
|
||||
- Measure queue duration between gitlab-workhorse and Rails
|
||||
- Make Omniauth providers specs to not modify global configuration
|
||||
- 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
|
||||
- Use Knapsack only in CI environment
|
||||
- Cache project build count in sidebar nav
|
||||
- Add milestone expire date to the right sidebar
|
||||
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
|
||||
- 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.
|
||||
- Add workhorse controller and API helpers
|
||||
- An indicator is now displayed at the top of the comment field for confidential issues.
|
||||
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
|
||||
- Improve issuables APIs performance when accessing notes !4471
|
||||
- External links now open in a new tab
|
||||
- Markdown editor now correctly resets the input value on edit cancellation !4175
|
||||
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions
|
||||
- Improved UX of date pickers on issue & milestone forms
|
||||
- Cache on the database if a project has an active external issue tracker.
|
||||
- Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
|
||||
|
||||
v 8.8.5 (unreleased)
|
||||
- Ensure branch cleanup regardless of whether the GitHub import process succeeds
|
||||
- Fix todos page throwing errors when you have a project pending deletion
|
||||
- Reduce number of SQL queries when rendering user references
|
||||
- Import GitHub repositories respecting the API rate limit
|
||||
- Fix importer for GitHub comments on diff
|
||||
- Disable Webhooks before proceeding with the GitHub import
|
||||
- Fix incremental trace upload API when using multi-byte UTF-8 chars in trace
|
||||
|
||||
v 8.8.4
|
||||
- Fix LDAP-based login for users with 2FA enabled. !4493
|
||||
|
||||
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
|
||||
@@ -126,6 +190,7 @@ v 8.8.0
|
||||
- Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine)
|
||||
- Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs)
|
||||
- When creating a .gitignore file a dropdown with templates will be provided
|
||||
- Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562
|
||||
|
||||
v 8.7.7
|
||||
- Fix import by `Any Git URL` broken if the URL contains a space
|
||||
@@ -135,6 +200,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,15 @@ gem "carrierwave", '~> 0.10.0'
|
||||
# Drag and Drop UI
|
||||
gem 'dropzonejs-rails', '~> 0.7.1'
|
||||
|
||||
# for backups
|
||||
gem 'fog-aws', '~> 0.9'
|
||||
gem 'fog-azure', '~> 0.0'
|
||||
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 +112,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 +145,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 +307,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
|
||||
|
||||
+65
-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)
|
||||
@@ -71,6 +70,21 @@ GEM
|
||||
descendants_tracker (~> 0.0.4)
|
||||
ice_nine (~> 0.11.0)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
azure (0.7.5)
|
||||
addressable (~> 2.3)
|
||||
azure-core (~> 0.1)
|
||||
faraday (~> 0.9)
|
||||
faraday_middleware (~> 0.10)
|
||||
json (~> 1.8)
|
||||
mime-types (>= 1, < 3.0)
|
||||
nokogiri (~> 1.6)
|
||||
systemu (~> 2.6)
|
||||
thor (~> 0.19)
|
||||
uuid (~> 2.0)
|
||||
azure-core (0.1.2)
|
||||
faraday (~> 0.9)
|
||||
faraday_middleware (~> 0.10)
|
||||
nokogiri (~> 1.6)
|
||||
babosa (1.0.2)
|
||||
base32 (0.3.2)
|
||||
bcrypt (3.1.11)
|
||||
@@ -93,7 +107,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 +197,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 +214,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 +223,33 @@ 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-azure (0.0.2)
|
||||
azure (~> 0.6)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
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 +358,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 +378,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 +389,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 +410,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 +421,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 +495,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 +571,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 +600,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 +649,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 +760,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 +780,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 +849,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 +858,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 +875,12 @@ DEPENDENCIES
|
||||
ffaker (~> 2.0.0)
|
||||
flay
|
||||
flog
|
||||
fog (~> 1.36.0)
|
||||
fog-aws (~> 0.9)
|
||||
fog-azure (~> 0.0)
|
||||
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 +909,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 +951,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 +963,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 +1001,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 +1014,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
|
||||
@@ -1,11 +1,14 @@
|
||||
class @Activities
|
||||
constructor: ->
|
||||
Pager.init 20, true
|
||||
Pager.init 20, true, false, @updateTooltips
|
||||
$(".event-filter-link").on "click", (event) =>
|
||||
event.preventDefault()
|
||||
@toggleFilter($(event.currentTarget))
|
||||
@reloadActivities()
|
||||
|
||||
updateTooltips: ->
|
||||
gl.utils.localTimeAgo($('.js-timeago', '#activity'))
|
||||
|
||||
reloadActivities: ->
|
||||
$(".content_list").html ''
|
||||
Pager.init 20, true
|
||||
|
||||
@@ -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
|
||||
@@ -35,7 +35,6 @@
|
||||
#= require raphael
|
||||
#= require g.raphael
|
||||
#= require g.bar
|
||||
#= require Chart
|
||||
#= require branch-graph
|
||||
#= require ace/ace
|
||||
#= require ace/ext-searchbox
|
||||
@@ -56,9 +55,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()
|
||||
@@ -224,6 +225,10 @@ $ ->
|
||||
form = btn.closest("form")
|
||||
new ConfirmDangerModal(form, text)
|
||||
|
||||
|
||||
$(document).on 'click', 'button', ->
|
||||
$(this).blur()
|
||||
|
||||
$('input[type="search"]').each ->
|
||||
$this = $(this)
|
||||
$this.attr 'value', $this.val()
|
||||
@@ -262,9 +267,10 @@ $ ->
|
||||
$(document).trigger('breakpoint:change', [bootstrapBreakpoint])
|
||||
|
||||
$(window)
|
||||
.off "resize"
|
||||
.on "resize", (e) ->
|
||||
.off "resize.app"
|
||||
.on "resize.app", (e) ->
|
||||
fitSidebarForSize()
|
||||
|
||||
gl.awardsHandler = new AwardsHandler()
|
||||
checkInitialSidebarSize()
|
||||
new Aside()
|
||||
|
||||
@@ -1,201 +1,354 @@
|
||||
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() unless @frequentEmojiBlockRendered
|
||||
|
||||
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 = true, 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 = true) ->
|
||||
|
||||
@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, false, ->
|
||||
$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 gon.award_menu_url
|
||||
|
||||
|
||||
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 frequent-emojis'>")
|
||||
|
||||
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'))
|
||||
|
||||
@frequentEmojiBlockRendered = true
|
||||
|
||||
|
||||
setupSearch: ->
|
||||
$('input.emoji-search').keyup (ev) =>
|
||||
|
||||
$('input.emoji-search').on 'keyup', (ev) =>
|
||||
term = $(ev.target).val()
|
||||
|
||||
# Clean previous search results
|
||||
@@ -204,12 +357,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-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone()
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
class CiBuild
|
||||
class @CiBuild
|
||||
@interval: null
|
||||
@state: null
|
||||
|
||||
constructor: (build_url, build_status, build_state) ->
|
||||
constructor: (@build_url, @build_status, @state) ->
|
||||
clearInterval(CiBuild.interval)
|
||||
|
||||
@state = build_state
|
||||
# Init breakpoint checker
|
||||
@bp = Breakpoints.get()
|
||||
@hideSidebar()
|
||||
$('.js-build-sidebar').niceScroll()
|
||||
$(document)
|
||||
.off 'click', '.js-sidebar-build-toggle'
|
||||
.on 'click', '.js-sidebar-build-toggle', @toggleSidebar
|
||||
|
||||
@initScrollButtonAffix()
|
||||
$(window)
|
||||
.off 'resize.build'
|
||||
.on 'resize.build', @hideSidebar
|
||||
|
||||
if build_status == "running" || build_status == "pending"
|
||||
if $('#build-trace').length
|
||||
@getInitialBuildTrace()
|
||||
@initScrollButtonAffix()
|
||||
|
||||
if @build_status is "running" or @build_status is "pending"
|
||||
#
|
||||
# Bind autoscroll button to follow build output
|
||||
#
|
||||
$("#autoscroll-button").bind "click", ->
|
||||
$('#autoscroll-button').on 'click', ->
|
||||
state = $(this).data("state")
|
||||
if "enabled" is state
|
||||
$(this).data "state", "disabled"
|
||||
@@ -27,26 +39,37 @@ class CiBuild
|
||||
# Only valid for runnig build when output changes during time
|
||||
#
|
||||
CiBuild.interval = setInterval =>
|
||||
if window.location.href.split("#").first() is build_url
|
||||
last_state = @state
|
||||
$.ajax
|
||||
url: build_url + "/trace.json?state=" + encodeURIComponent(@state)
|
||||
dataType: "json"
|
||||
success: (log) =>
|
||||
return unless last_state is @state
|
||||
|
||||
if log.state and log.status is "running"
|
||||
@state = log.state
|
||||
if log.append
|
||||
$('.fa-refresh').before log.html
|
||||
else
|
||||
$('#build-trace code').html log.html
|
||||
$('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
|
||||
@checkAutoscroll()
|
||||
else if log.status isnt build_status
|
||||
Turbolinks.visit build_url
|
||||
if window.location.href.split("#").first() is @build_url
|
||||
@getBuildTrace()
|
||||
, 4000
|
||||
|
||||
getInitialBuildTrace: ->
|
||||
$.ajax
|
||||
url: @build_url
|
||||
dataType: 'json'
|
||||
success: (build_data) ->
|
||||
$('.js-build-output').html build_data.trace_html
|
||||
|
||||
if build_data.status is 'success' or build_data.status is 'failed'
|
||||
$('.js-build-refresh').remove()
|
||||
|
||||
getBuildTrace: ->
|
||||
$.ajax
|
||||
url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}"
|
||||
dataType: "json"
|
||||
success: (log) =>
|
||||
if log.state
|
||||
@state = log.state
|
||||
|
||||
if log.status is "running"
|
||||
if log.append
|
||||
$('.js-build-output').append log.html
|
||||
else
|
||||
$('.js-build-output').html log.html
|
||||
@checkAutoscroll()
|
||||
else if log.status isnt @build_status
|
||||
Turbolinks.visit @build_url
|
||||
|
||||
checkAutoscroll: ->
|
||||
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
|
||||
|
||||
@@ -61,4 +84,22 @@ class CiBuild
|
||||
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
|
||||
)
|
||||
|
||||
@CiBuild = CiBuild
|
||||
shouldHideSidebar: ->
|
||||
bootstrapBreakpoint = @bp.getBreakpointSize()
|
||||
|
||||
bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm'
|
||||
|
||||
toggleSidebar: =>
|
||||
if @shouldHideSidebar()
|
||||
$('.js-build-sidebar')
|
||||
.toggleClass 'right-sidebar-expanded right-sidebar-collapsed'
|
||||
|
||||
hideSidebar: =>
|
||||
if @shouldHideSidebar()
|
||||
$('.js-build-sidebar')
|
||||
.removeClass 'right-sidebar-expanded'
|
||||
.addClass 'right-sidebar-collapsed'
|
||||
else
|
||||
$('.js-build-sidebar')
|
||||
.removeClass 'right-sidebar-collapsed'
|
||||
.addClass 'right-sidebar-expanded'
|
||||
|
||||
@@ -17,6 +17,7 @@ class Dispatcher
|
||||
switch page
|
||||
when 'projects:issues:index'
|
||||
Issuable.init()
|
||||
new IssuableBulkActions()
|
||||
shortcut_handler = new ShortcutsNavigation()
|
||||
when 'projects:issues:show'
|
||||
new Issue()
|
||||
@@ -97,6 +98,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("")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
window.GitLab ?= {}
|
||||
GitLab.GfmAutoComplete =
|
||||
dataLoading: false
|
||||
dataLoaded: false
|
||||
|
||||
dataSource: ''
|
||||
|
||||
@@ -22,6 +23,24 @@ GitLab.GfmAutoComplete =
|
||||
Milestones:
|
||||
template: '<li>${title}</li>'
|
||||
|
||||
Loading:
|
||||
template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
|
||||
|
||||
DefaultOptions:
|
||||
sorter: (query, items, searchKey) ->
|
||||
return items if items[0].name? and items[0].name is 'loading'
|
||||
|
||||
$.fn.atwho.default.callbacks.sorter(query, items, searchKey)
|
||||
filter: (query, data, searchKey) ->
|
||||
return data if data[0] is 'loading'
|
||||
|
||||
$.fn.atwho.default.callbacks.filter(query, data, searchKey)
|
||||
beforeInsert: (value) ->
|
||||
if not GitLab.GfmAutoComplete.dataLoaded
|
||||
@at
|
||||
else
|
||||
value
|
||||
|
||||
# Add GFM auto-completion to all input fields, that accept GFM input.
|
||||
setup: (wrap) ->
|
||||
@input = $('.js-gfm-input')
|
||||
@@ -53,18 +72,37 @@ GitLab.GfmAutoComplete =
|
||||
# Emoji
|
||||
@input.atwho
|
||||
at: ':'
|
||||
displayTpl: @Emoji.template
|
||||
displayTpl: (value) =>
|
||||
if value.path?
|
||||
@Emoji.template
|
||||
else
|
||||
@Loading.template
|
||||
insertTpl: ':${name}:'
|
||||
data: ['loading']
|
||||
callbacks:
|
||||
sorter: @DefaultOptions.sorter
|
||||
filter: @DefaultOptions.filter
|
||||
beforeInsert: @DefaultOptions.beforeInsert
|
||||
|
||||
# Team Members
|
||||
@input.atwho
|
||||
at: '@'
|
||||
displayTpl: @Members.template
|
||||
displayTpl: (value) =>
|
||||
if value.username?
|
||||
@Members.template
|
||||
else
|
||||
@Loading.template
|
||||
insertTpl: '${atwho-at}${username}'
|
||||
searchKey: 'search'
|
||||
data: ['loading']
|
||||
callbacks:
|
||||
sorter: @DefaultOptions.sorter
|
||||
filter: @DefaultOptions.filter
|
||||
beforeInsert: @DefaultOptions.beforeInsert
|
||||
beforeSave: (members) ->
|
||||
$.map members, (m) ->
|
||||
return m if not m.username?
|
||||
|
||||
title = m.name
|
||||
title += " (#{m.count})" if m.count
|
||||
|
||||
@@ -76,11 +114,21 @@ GitLab.GfmAutoComplete =
|
||||
at: '#'
|
||||
alias: 'issues'
|
||||
searchKey: 'search'
|
||||
displayTpl: @Issues.template
|
||||
displayTpl: (value) =>
|
||||
if value.title?
|
||||
@Issues.template
|
||||
else
|
||||
@Loading.template
|
||||
data: ['loading']
|
||||
insertTpl: '${atwho-at}${id}'
|
||||
callbacks:
|
||||
sorter: @DefaultOptions.sorter
|
||||
filter: @DefaultOptions.filter
|
||||
beforeInsert: @DefaultOptions.beforeInsert
|
||||
beforeSave: (issues) ->
|
||||
$.map issues, (i) ->
|
||||
return i if not i.title?
|
||||
|
||||
id: i.iid
|
||||
title: sanitize(i.title)
|
||||
search: "#{i.iid} #{i.title}"
|
||||
@@ -89,11 +137,18 @@ GitLab.GfmAutoComplete =
|
||||
at: '%'
|
||||
alias: 'milestones'
|
||||
searchKey: 'search'
|
||||
displayTpl: @Milestones.template
|
||||
displayTpl: (value) =>
|
||||
if value.title?
|
||||
@Milestones.template
|
||||
else
|
||||
@Loading.template
|
||||
insertTpl: '${atwho-at}"${title}"'
|
||||
data: ['loading']
|
||||
callbacks:
|
||||
beforeSave: (milestones) ->
|
||||
$.map milestones, (m) ->
|
||||
return m if not m.title?
|
||||
|
||||
id: m.iid
|
||||
title: sanitize(m.title)
|
||||
search: "#{m.title}"
|
||||
@@ -102,11 +157,21 @@ GitLab.GfmAutoComplete =
|
||||
at: '!'
|
||||
alias: 'mergerequests'
|
||||
searchKey: 'search'
|
||||
displayTpl: @Issues.template
|
||||
displayTpl: (value) =>
|
||||
if value.title?
|
||||
@Issues.template
|
||||
else
|
||||
@Loading.template
|
||||
data: ['loading']
|
||||
insertTpl: '${atwho-at}${id}'
|
||||
callbacks:
|
||||
sorter: @DefaultOptions.sorter
|
||||
filter: @DefaultOptions.filter
|
||||
beforeInsert: @DefaultOptions.beforeInsert
|
||||
beforeSave: (merges) ->
|
||||
$.map merges, (m) ->
|
||||
return m if not m.title?
|
||||
|
||||
id: m.iid
|
||||
title: sanitize(m.title)
|
||||
search: "#{m.iid} #{m.title}"
|
||||
@@ -118,6 +183,8 @@ GitLab.GfmAutoComplete =
|
||||
$.getJSON(dataSource)
|
||||
|
||||
loadData: (data) ->
|
||||
@dataLoaded = true
|
||||
|
||||
# load members
|
||||
@input.atwho 'load', '@', data.members
|
||||
# load issues
|
||||
@@ -128,3 +195,7 @@ GitLab.GfmAutoComplete =
|
||||
@input.atwho 'load', 'mergerequests', data.mergerequests
|
||||
# load emojis
|
||||
@input.atwho 'load', ':', data.emojis
|
||||
|
||||
# This trigger at.js again
|
||||
# otherwise we would be stuck with loading until the user types
|
||||
$(':focus').trigger('keyup')
|
||||
|
||||
@@ -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
|
||||
@@ -211,6 +211,7 @@ class GitLabDropdown
|
||||
|
||||
@dropdown.on "shown.bs.dropdown", @opened
|
||||
@dropdown.on "hidden.bs.dropdown", @hidden
|
||||
$(@el).on "update.label", @updateLabel
|
||||
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
|
||||
@dropdown.on 'keyup', (e) =>
|
||||
if e.which is 27 # Escape key
|
||||
@@ -298,6 +299,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 +317,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 +372,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
|
||||
@@ -440,9 +454,20 @@ class GitLabDropdown
|
||||
|
||||
# Toggle the dropdown label
|
||||
if @options.toggleLabel
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel
|
||||
@updateLabel()
|
||||
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
|
||||
@@ -456,34 +481,45 @@ class GitLabDropdown
|
||||
|
||||
# Toggle the dropdown label
|
||||
if @options.toggleLabel
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
|
||||
@updateLabel(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 +547,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'
|
||||
@@ -544,6 +580,9 @@ class GitLabDropdown
|
||||
# Scroll the dropdown content up
|
||||
$dropdownContent.scrollTop(listItemTop - dropdownContentTop)
|
||||
|
||||
updateLabel: (selected = null, el = null) =>
|
||||
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el)
|
||||
|
||||
$.fn.glDropdown = (opts) ->
|
||||
return @.each ->
|
||||
if (!$.data @, 'glDropdown')
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
# 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 Chart
|
||||
#= require_tree .
|
||||
|
||||
@@ -6,12 +6,18 @@ issuable_created = false
|
||||
Issuable.initTemplates()
|
||||
Issuable.initSearch()
|
||||
Issuable.initChecks()
|
||||
Issuable.initLabelFilterRemove()
|
||||
|
||||
initTemplates: ->
|
||||
Issuable.labelRow = _.template(
|
||||
'<% _.each(labels, function(label){ %>
|
||||
<span class="label-row">
|
||||
<a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
|
||||
<span class="label-row btn-group" role="group" aria-label="<%= _.escape(label.title) %>" style="color: <%= label.text_color %>;">
|
||||
<a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%= label.color %>;" title="<%= _.escape(label.description) %>" data-container="body">
|
||||
<%= _.escape(label.title) %>
|
||||
</a>
|
||||
<button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%= label.color %>;" data-label="<%= _.escape(label.title) %>">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</span>
|
||||
<% }); %>'
|
||||
)
|
||||
@@ -35,6 +41,21 @@ issuable_created = false
|
||||
Issuable.filterResults $form
|
||||
, 500)
|
||||
|
||||
initLabelFilterRemove: ->
|
||||
$(document)
|
||||
.off 'click', '.js-label-filter-remove'
|
||||
.on 'click', '.js-label-filter-remove', (e) ->
|
||||
$button = $(@)
|
||||
|
||||
# Remove the label input box
|
||||
$('input[name="label_name[]"]')
|
||||
.filter -> @value is $button.data('label')
|
||||
.remove()
|
||||
|
||||
# Submit the form to get new data
|
||||
Issuable.filterResults $('.filter-form')
|
||||
$('.js-label-select').trigger('update.label')
|
||||
|
||||
toggleLabelFilters: ->
|
||||
$filteredLabels = $('.filtered-labels')
|
||||
if $filteredLabels.find('.label-row').length > 0
|
||||
|
||||
@@ -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')
|
||||
@@ -93,8 +95,11 @@ class @LabelsSelect
|
||||
$newLabelCreateButton.enable()
|
||||
|
||||
if label.message?
|
||||
errors = _.map label.message, (value, key) ->
|
||||
"#{key} #{value[0]}"
|
||||
|
||||
$newLabelError
|
||||
.text label.message
|
||||
.html errors.join("<br/>")
|
||||
.show()
|
||||
else
|
||||
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
|
||||
@@ -196,10 +201,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,17 +243,21 @@ 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
|
||||
|
||||
filterable: true
|
||||
toggleLabel: (selected, el) ->
|
||||
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
|
||||
|
||||
@@ -280,10 +297,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 +324,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,24 @@
|
||||
((w) ->
|
||||
|
||||
jQuery.timefor = (time, suffix, expiredLabel) ->
|
||||
|
||||
return '' unless time
|
||||
|
||||
suffix or= 'remaining'
|
||||
expiredLabel or= 'Past due'
|
||||
|
||||
jQuery.timeago.settings.allowFuture = yes
|
||||
|
||||
{ suffixFromNow } = jQuery.timeago.settings.strings
|
||||
jQuery.timeago.settings.strings.suffixFromNow = suffix
|
||||
|
||||
timefor = $.timeago time
|
||||
|
||||
if timefor.indexOf('ago') > -1
|
||||
timefor = expiredLabel
|
||||
|
||||
jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
|
||||
|
||||
return timefor
|
||||
|
||||
) window
|
||||
@@ -12,6 +12,13 @@
|
||||
$el.attr('title', gl.utils.formatDate($el.attr('datetime')))
|
||||
)
|
||||
|
||||
$timeagoEls.timeago() if setTimeago
|
||||
if setTimeago
|
||||
$timeagoEls.timeago()
|
||||
$timeagoEls.tooltip('destroy')
|
||||
|
||||
# Recreate with custom template
|
||||
$timeagoEls.tooltip(
|
||||
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
|
||||
)
|
||||
|
||||
) window
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
gl.emojiAliases = ->
|
||||
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
|
||||
@@ -24,11 +24,21 @@ class @MilestoneSelect
|
||||
|
||||
if issueUpdateURL
|
||||
milestoneLinkTemplate = _.template(
|
||||
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>'
|
||||
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>">
|
||||
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
|
||||
<%= _.escape(title) %>
|
||||
</span>
|
||||
</a>'
|
||||
)
|
||||
|
||||
milestoneLinkNoneTemplate = '<div class="light">None</div>'
|
||||
|
||||
collapsedSidebarLabelTemplate = _.template(
|
||||
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
|
||||
<%= _.escape(title) %>
|
||||
</span>'
|
||||
)
|
||||
|
||||
$dropdown.glDropdown(
|
||||
data: (term, callback) ->
|
||||
$.ajax(
|
||||
@@ -83,7 +93,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,12 +128,13 @@ 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
|
||||
data.milestone.remaining = $.timefor data.milestone.due_date
|
||||
$value.html(milestoneLinkTemplate(data.milestone))
|
||||
$sidebarCollapsedValue.find('span').text(data.milestone.title)
|
||||
$sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone))
|
||||
else
|
||||
$value.html(milestoneLinkNoneTemplate)
|
||||
$sidebarCollapsedValue.find('span').text('No')
|
||||
|
||||
@@ -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
|
||||
@@ -353,8 +354,7 @@ class @Notes
|
||||
Called in response to clicking the edit note link
|
||||
|
||||
Replaces the note text with the note edit form
|
||||
Adds a hidden div with the original content of the note to fill the edit note form with
|
||||
if the user cancels
|
||||
Adds a data attribute to the form with the original content of the note for cancellations
|
||||
###
|
||||
showEditForm: (e, scrollTo, myLastNote) ->
|
||||
e.preventDefault()
|
||||
@@ -370,6 +370,8 @@ class @Notes
|
||||
done = ($noteText) ->
|
||||
# Neat little trick to put the cursor at the end
|
||||
noteTextVal = $noteText.val()
|
||||
# Store the original note text in a data attribute to retrieve if a user cancels edit.
|
||||
form.find('form.edit-note').data 'original-note', noteTextVal
|
||||
$noteText.val('').val(noteTextVal);
|
||||
|
||||
new GLForm form
|
||||
@@ -392,14 +394,16 @@ class @Notes
|
||||
###
|
||||
Called in response to clicking the edit note link
|
||||
|
||||
Hides edit form
|
||||
Hides edit form and restores the original note text to the editor textarea.
|
||||
###
|
||||
cancelEdit: (e) ->
|
||||
e.preventDefault()
|
||||
note = $(this).closest(".note")
|
||||
form = note.find(".current-note-edit-form")
|
||||
note.removeClass "is-editting"
|
||||
note.find(".current-note-edit-form")
|
||||
.removeClass("current-note-edit-form")
|
||||
form.removeClass("current-note-edit-form")
|
||||
# Replace markdown textarea text with original note text.
|
||||
form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'))
|
||||
|
||||
###
|
||||
Called in response to deleting a note of any kind.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@Pager =
|
||||
init: (@limit = 0, preload, @disable = false) ->
|
||||
init: (@limit = 0, preload, @disable = false, @callback = $.noop) ->
|
||||
@loading = $('.loading').first()
|
||||
|
||||
if preload
|
||||
@@ -19,6 +19,7 @@
|
||||
@loading.hide()
|
||||
success: (data) ->
|
||||
Pager.append(data.count, data.html)
|
||||
Pager.callback()
|
||||
dataType: "json"
|
||||
|
||||
append: (count, html) ->
|
||||
|
||||
@@ -7,12 +7,17 @@ class @ProjectNew
|
||||
@toggleSettingsOnclick()
|
||||
|
||||
|
||||
toggleSettings: ->
|
||||
checked = $("#project_builds_enabled").prop("checked")
|
||||
if checked
|
||||
$('.builds-feature').show()
|
||||
else
|
||||
$('.builds-feature').hide()
|
||||
toggleSettings: =>
|
||||
@_showOrHide('#project_builds_enabled', '.builds-feature')
|
||||
@_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature')
|
||||
|
||||
toggleSettingsOnclick: ->
|
||||
$("#project_builds_enabled").on 'click', @toggleSettings
|
||||
$('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings
|
||||
|
||||
_showOrHide: (checkElement, container) ->
|
||||
$container = $(container)
|
||||
|
||||
if $(checkElement).prop('checked')
|
||||
$container.show()
|
||||
else
|
||||
$container.hide()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -19,3 +19,8 @@ class @Subscription
|
||||
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
|
||||
btn.find('span').text(action)
|
||||
@subscription_status.find('>div').toggleClass('hidden')
|
||||
|
||||
if btn.attr('data-original-title')
|
||||
btn.tooltip('hide')
|
||||
.attr('data-original-title', action)
|
||||
.tooltip('fixTitle')
|
||||
|
||||
@@ -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 %>
|
||||
@@ -122,6 +122,9 @@ class @UserTabs
|
||||
@parentEl.find(tabSelector).html(data.html)
|
||||
@loaded[action] = true
|
||||
|
||||
# Fix tooltips
|
||||
gl.utils.localTimeAgo($('.js-timeago', tabSelector))
|
||||
|
||||
loadActivities: (source) ->
|
||||
return if @loaded['activity'] is true
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class @UsersSelect
|
||||
data: (term, callback) =>
|
||||
isAuthorFilter = $('.js-author-search')
|
||||
|
||||
@users term, term is '' and isAuthorFilter, (users) =>
|
||||
@users term, (users) =>
|
||||
if term.length is 0
|
||||
showDivider = 0
|
||||
|
||||
@@ -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'
|
||||
@@ -221,7 +221,7 @@ class @UsersSelect
|
||||
multiple: $(select).hasClass('multiselect')
|
||||
minimumInputLength: 0
|
||||
query: (query) =>
|
||||
@users query.term, @projectId?, (users) =>
|
||||
@users query.term, (users) =>
|
||||
data = { results: users }
|
||||
|
||||
if query.term.length == 0
|
||||
@@ -304,7 +304,7 @@ class @UsersSelect
|
||||
|
||||
# Return users list. Filtered by query
|
||||
# Only active users retrieved
|
||||
users: (query, fromProject, callback) =>
|
||||
users: (query, callback) =>
|
||||
url = @buildUrl(@usersPath)
|
||||
|
||||
$.ajax(
|
||||
@@ -313,7 +313,7 @@ class @UsersSelect
|
||||
search: query
|
||||
per_page: 20
|
||||
active: true
|
||||
project_id: @projectId if fromProject
|
||||
project_id: @projectId
|
||||
group_id: @groupId
|
||||
current_user: @showCurrentUser
|
||||
author_id: @authorId
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
margin-bottom: -$gl-padding;
|
||||
}
|
||||
|
||||
&.content-component-block {
|
||||
padding: 11px 0;
|
||||
background-color: $white-light;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,23 @@
|
||||
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
|
||||
}
|
||||
|
||||
@mixin btn-with-margin {
|
||||
margin-left: $btn-side-margin;
|
||||
float: left;
|
||||
|
||||
&.inline {
|
||||
float: none;
|
||||
}
|
||||
|
||||
&.btn-sm {
|
||||
margin-left: $btn-sm-side-margin;
|
||||
}
|
||||
|
||||
&.btn-xs {
|
||||
margin-left: $btn-xs-side-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@include btn-default;
|
||||
@include btn-white;
|
||||
@@ -142,15 +159,9 @@
|
||||
}
|
||||
|
||||
&.btn-grouped {
|
||||
margin-right: 7px;
|
||||
float: left;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
&.btn-xs {
|
||||
margin-right: 3px;
|
||||
}
|
||||
@include btn-with-margin;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
@@ -192,11 +203,7 @@
|
||||
|
||||
.btn-group {
|
||||
&.btn-grouped {
|
||||
margin-right: 7px;
|
||||
float: left;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
@include btn-with-margin;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,10 +122,9 @@
|
||||
a {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding: 5px 10px;
|
||||
color: $dropdown-link-color;
|
||||
line-height: 34px;
|
||||
line-height: initial;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
@@ -162,6 +161,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-large {
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
.dropdown-menu-no-wrap {
|
||||
a {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -232,13 +241,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 +253,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-indeterminate::before {
|
||||
content: "\f068";
|
||||
}
|
||||
|
||||
&.is-active::before {
|
||||
content: "\f00c";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,3 +540,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;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ label {
|
||||
.form-control {
|
||||
@include box-shadow(none);
|
||||
border-radius: 3px;
|
||||
padding: $gl-vert-padding $gl-input-padding;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
&:hover {
|
||||
background-color: $color-dark;
|
||||
a {
|
||||
color: #fff;
|
||||
color: $white-light;
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
color: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-nav a {
|
||||
color: #fff;
|
||||
color: $white-light;
|
||||
background: $color;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
&:hover {
|
||||
background-color: $color-dark;
|
||||
color: #fff;
|
||||
color: $white-light;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -63,10 +63,20 @@
|
||||
color: $color-light;
|
||||
}
|
||||
|
||||
path,
|
||||
polygon {
|
||||
fill: $color-light;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: $color-light;
|
||||
background: $color-dark;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.separate-item {
|
||||
@@ -74,7 +84,7 @@
|
||||
}
|
||||
|
||||
&.active a {
|
||||
color: #fff;
|
||||
color: $white-light;
|
||||
background: $color-dark;
|
||||
|
||||
&.no-highlight {
|
||||
@@ -82,15 +92,23 @@
|
||||
}
|
||||
|
||||
i {
|
||||
color: #fff
|
||||
color: $white-light
|
||||
}
|
||||
|
||||
path,
|
||||
polygon {
|
||||
fill: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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 +120,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) {
|
||||
.header-content {
|
||||
margin-left: $sidebar_width;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
margin-left: 0;
|
||||
transition-duration: .3s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
font-family: $regular_font;
|
||||
font-size: $font-size-base;
|
||||
|
||||
&.ui-datepicker,
|
||||
&.ui-datepicker-inline {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
@@ -10,6 +11,25 @@
|
||||
.ui-datepicker-header {
|
||||
background: #fff;
|
||||
border-color: #ddd;
|
||||
|
||||
.ui-datepicker-prev,
|
||||
.ui-datepicker-next {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.ui-datepicker-prev {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.ui-datepicker-next {
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.ui-state-hover {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-datepicker-calendar td a {
|
||||
@@ -36,21 +56,18 @@
|
||||
}
|
||||
|
||||
.ui-state-highlight {
|
||||
border: 1px solid #eee;
|
||||
background: #eee;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ui-state-active {
|
||||
border: 1px solid $gl-primary;
|
||||
background: $gl-primary;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ui-state-hover,
|
||||
.ui-state-focus {
|
||||
border: 1px solid $row-hover;
|
||||
background: $row-hover;
|
||||
color: #333;
|
||||
.ui-datepicker-calendar {
|
||||
.ui-state-active,
|
||||
.ui-state-hover,
|
||||
.ui-state-focus {
|
||||
border: 1px solid $gl-primary;
|
||||
background: $gl-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,10 +137,30 @@ ul.content-list {
|
||||
padding-top: 1px;
|
||||
float: right;
|
||||
|
||||
.btn {
|
||||
padding: 10px 14px;
|
||||
> .btn,
|
||||
> .btn-group {
|
||||
margin-right: $gl-padding-top;
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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%;
|
||||
@@ -143,6 +171,7 @@
|
||||
> form {
|
||||
display: inline-block;
|
||||
margin-top: -1px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
@@ -179,7 +208,7 @@
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
padding-bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
|
||||
margin: 0 0 10px;
|
||||
display: block;
|
||||
@@ -210,16 +239,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small devices (tablets, 768px and lower) */
|
||||
@media (max-width: $screen-sm-max) {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
input {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,6 +295,19 @@
|
||||
border-bottom: none;
|
||||
height: 51px;
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 2px;
|
||||
height: 15px;
|
||||
width: auto;
|
||||
|
||||
path,
|
||||
polygon {
|
||||
fill: $layout-link-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-right {
|
||||
@include fade(left, rgba(250, 250, 250, 0.4), $background-color);
|
||||
right: 0;
|
||||
@@ -297,9 +329,17 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
||||
a, i {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
svg {
|
||||
path,
|
||||
polygon {
|
||||
fill: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -309,8 +349,8 @@
|
||||
}
|
||||
|
||||
.nav-control {
|
||||
.fade-right {
|
||||
|
||||
.fade-right {
|
||||
@media (min-width: $screen-xs-max) {
|
||||
right: 67px;
|
||||
}
|
||||
@@ -321,6 +361,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;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
background: #fff;
|
||||
border-color: $input-border;
|
||||
height: 35px;
|
||||
padding: $gl-vert-padding $gl-btn-padding;
|
||||
padding: $gl-vert-padding $gl-input-padding;
|
||||
font-size: $gl-font-size;
|
||||
line-height: 1.42857143;
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
@@ -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,55 +36,21 @@
|
||||
|
||||
.sidebar-wrapper {
|
||||
.header-logo {
|
||||
border-bottom: 1px solid transparent;
|
||||
float: left;
|
||||
height: $header-height;
|
||||
padding: 8px 26px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
padding: 7px 22px;
|
||||
padding: 15px 22px;
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
width: $sidebar_width;
|
||||
@@ -126,8 +78,8 @@
|
||||
|
||||
|
||||
.nav-sidebar {
|
||||
margin-top: 14 + $header-height;
|
||||
margin-bottom: 100px;
|
||||
margin-top: 22 + $header-height;
|
||||
margin-bottom: 116px;
|
||||
transition-duration: .3s;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
@@ -145,13 +97,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;
|
||||
|
||||
@@ -164,16 +115,13 @@
|
||||
}
|
||||
|
||||
i {
|
||||
width: 16px;
|
||||
color: $gray-light;
|
||||
margin-right: 13px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.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 +165,14 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 8px 0;
|
||||
|
||||
a {
|
||||
padding-left: ($sidebar_collapsed_width - 36) / 2;
|
||||
@@ -246,6 +183,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: $sidebar_collapsed_width;
|
||||
|
||||
@@ -261,44 +202,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 +248,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) {
|
||||
@@ -353,7 +273,9 @@
|
||||
padding-right: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
&:not(.build-sidebar) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -192,3 +192,8 @@
|
||||
.text-info:hover {
|
||||
color: $brand-info;
|
||||
}
|
||||
|
||||
// Prevent datetimes on tooltips to break into two lines
|
||||
.local-timeago {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -57,6 +57,7 @@ $code_line_height: 1.5;
|
||||
*/
|
||||
$gl-padding: 16px;
|
||||
$gl-btn-padding: 10px;
|
||||
$gl-input-padding: 10px;
|
||||
$gl-vert-padding: 6px;
|
||||
$gl-padding-top: 10px;
|
||||
|
||||
@@ -79,6 +80,9 @@ $provider-btn-not-active-color: #4688f1;
|
||||
$link-underline-blue: #4a8bee;
|
||||
$layout-link-gray: #7e7c7c;
|
||||
$todo-alert-blue: #428bca;
|
||||
$btn-side-margin: 10px;
|
||||
$btn-sm-side-margin: 7px;
|
||||
$btn-xs-side-margin: 5px;
|
||||
|
||||
/*
|
||||
* Color schema
|
||||
@@ -121,7 +125,7 @@ $border-white-normal: #d6dae2;
|
||||
$border-white-dark: #c6cacf;
|
||||
|
||||
$border-gray-light: #dcdcdc;
|
||||
$border-gray-normal: rgba(0, 0, 0, 0.10);
|
||||
$border-gray-normal: #d7d7d7;
|
||||
$border-gray-dark: #c6cacf;
|
||||
|
||||
$border-green-light: #2faa60;
|
||||
@@ -256,3 +260,6 @@ $calendar-header-color: #b8b8b8;
|
||||
$calendar-hover-bg: #ecf3fe;
|
||||
$calendar-border-color: rgba(#000, .1);
|
||||
$calendar-unselectable-bg: #faf9f9;
|
||||
|
||||
$ci-output-bg: #1d1f21;
|
||||
$ci-text-color: #c5c8c6;
|
||||
|
||||
@@ -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,20 +95,30 @@
|
||||
|
||||
.award-control {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
line-height: 20px;
|
||||
outline: 0;
|
||||
|
||||
&:hover,
|
||||
&.active,
|
||||
&:active {
|
||||
background-color: $white-dark;
|
||||
background-color: $row-hover;
|
||||
border-color: $row-hover-border;
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.btn {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
.award-control-icon {
|
||||
.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;
|
||||
@@ -58,37 +53,92 @@
|
||||
left: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-widget {
|
||||
padding: 10px;
|
||||
background: $background-color;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
.build-header {
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
|
||||
.title {
|
||||
margin-top: 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.attr-name {
|
||||
color: #777;
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gl-gray;
|
||||
|
||||
&:hover {
|
||||
color: $gl-link-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-disabled {
|
||||
background: $background-color;
|
||||
code {
|
||||
color: $code-color;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3084bb !important;
|
||||
}
|
||||
.avatar {
|
||||
float: none;
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
table.builds {
|
||||
|
||||
.build-link {
|
||||
a {
|
||||
color: $gl-dark-link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-trace {
|
||||
background: $ci-output-bg;
|
||||
color: $ci-text-color;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
|
||||
.fa-refresh {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.bash {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar.build-sidebar {
|
||||
padding-top: $gl-padding;
|
||||
padding-bottom: $gl-padding;
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.build-sidebar-header {
|
||||
padding-top: 0;
|
||||
|
||||
.gutter-toggle {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-detail-row {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.build-light-text {
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
|
||||
.build-gutter-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
margin-top: -17px;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
> h1 {
|
||||
> h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-content {
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
margin-top: 6px;
|
||||
|
||||
p {
|
||||
overflow-x: auto;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-sidebar {
|
||||
.right-sidebar {
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
@@ -74,6 +74,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.block-first {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $gl-text-color;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -50,11 +50,26 @@
|
||||
|
||||
.label-row {
|
||||
.label-name {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
@media (min-width: $screen-sm-min) {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label-description {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +83,6 @@
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.label-subscription {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-labels-error {
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 10px;
|
||||
@@ -79,62 +90,95 @@
|
||||
color: $white-light;
|
||||
}
|
||||
|
||||
@mixin labels-mobile {
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.manage-labels-list {
|
||||
.btn-action {
|
||||
color: $gl-dark-link-color;
|
||||
|
||||
.prepend-left-10, .prepend-description-left {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
vertical-align: middle;
|
||||
.fa {
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@include labels-mobile;
|
||||
&:hover {
|
||||
color: $gl-link-color;
|
||||
|
||||
&.remove-row {
|
||||
color: $gl-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prepend-description-left {
|
||||
width: 57%;
|
||||
.dropdown {
|
||||
@media (min-width: $screen-sm-min) {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prioritized-labels {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.add-priority {
|
||||
display: none;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
@include labels-mobile;
|
||||
.other-labels {
|
||||
.remove-priority {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pull-info-right {
|
||||
float: right;
|
||||
.toggle-priority {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
float: none;
|
||||
}
|
||||
button {
|
||||
border-color: transparent;
|
||||
padding: 5px 8px;
|
||||
vertical-align: top;
|
||||
font-size: 14px;
|
||||
|
||||
.action-buttons {
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
padding: 6px;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.label-subscribe-button {
|
||||
padding-left: 0;
|
||||
}
|
||||
.filtered-labels {
|
||||
.label-row {
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
color: $gl-text-color;
|
||||
.label-remove {
|
||||
border-left: 1px solid rgba(0, 0, 0, .1);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.label-options-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label-subscribe-button {
|
||||
.label-subscribe-button-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.label-subscribe-button-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.append-right-20 {
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-min) {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.label-subscribe-button-loading {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,14 @@
|
||||
}
|
||||
|
||||
&.ci-failed,
|
||||
&.ci-canceled,
|
||||
&.ci-error {
|
||||
color: $gl-danger;
|
||||
}
|
||||
|
||||
&.ci-canceled {
|
||||
color: $gl-gray;
|
||||
}
|
||||
|
||||
a.monospace {
|
||||
color: inherit;
|
||||
}
|
||||
@@ -105,6 +108,11 @@
|
||||
font-size: 17px;
|
||||
margin: 5px 0;
|
||||
color: $gl-gray-dark;
|
||||
|
||||
&.has-conflicts .fa-exclamation-triangle {
|
||||
color: $gl-warning;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
|
||||
@@ -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;
|
||||
@@ -96,17 +129,8 @@
|
||||
display: none;
|
||||
font-size: 15px;
|
||||
|
||||
.form-actions {
|
||||
padding-left: 20px;
|
||||
|
||||
.btn-save {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.note-form-option {
|
||||
float: left;
|
||||
padding: 2px 0 0 25px;
|
||||
}
|
||||
.md-area {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,18 +11,15 @@
|
||||
$magenta: #cd00cd;
|
||||
$cyan: #00cdcd;
|
||||
$white: #e5e5e5;
|
||||
$l-black: #7f7f7f;
|
||||
$l-red: #f00;
|
||||
$l-green: #0f0;
|
||||
$l-yellow: #ff0;
|
||||
$l-blue: #5c5cff;
|
||||
$l-magenta: #f0f;
|
||||
$l-cyan: #0ff;
|
||||
$l-white: #fff;
|
||||
$l-black: #373b41;
|
||||
$l-red: #c66;
|
||||
$l-green: #b5bd68;
|
||||
$l-yellow: #f0c674;
|
||||
$l-blue: #81a2be;
|
||||
$l-magenta: #b294bb;
|
||||
$l-cyan: #8abeb7;
|
||||
$l-white: $ci-text-color;
|
||||
|
||||
.term-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.term-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||
:two_factor_grace_period,
|
||||
:gravatar_enabled,
|
||||
:sign_in_text,
|
||||
:after_sign_up_text,
|
||||
:help_page_text,
|
||||
:home_page_url,
|
||||
:after_sign_out_path,
|
||||
|
||||
@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
|
||||
include Gitlab::GonHelper
|
||||
include GitlabRoutingHelper
|
||||
include PageLayoutHelper
|
||||
include WorkhorseHelper
|
||||
|
||||
before_action :authenticate_user_from_token!
|
||||
before_action :authenticate_user!
|
||||
@@ -182,8 +183,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 +343,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 +360,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
|
||||
@@ -42,46 +42,8 @@ class JwtController < ApplicationController
|
||||
end
|
||||
|
||||
def authenticate_user(login, password)
|
||||
# TODO: this is a copy and paste from grack_auth,
|
||||
# it should be refactored in the future
|
||||
|
||||
user = Gitlab::Auth.new.find(login, password)
|
||||
|
||||
# If the user authenticated successfully, we reset the auth failure count
|
||||
# from Rack::Attack for that IP. A client may attempt to authenticate
|
||||
# with a username and blank password first, and only after it receives
|
||||
# a 401 error does it present a password. Resetting the count prevents
|
||||
# false positives from occurring.
|
||||
#
|
||||
# Otherwise, we let Rack::Attack know there was a failed authentication
|
||||
# attempt from this IP. This information is stored in the Rails cache
|
||||
# (Redis) and will be used by the Rack::Attack middleware to decide
|
||||
# whether to block requests from this IP.
|
||||
config = Gitlab.config.rack_attack.git_basic_auth
|
||||
|
||||
if config.enabled
|
||||
if user
|
||||
# A successful login will reset the auth failure count from this IP
|
||||
Rack::Attack::Allow2Ban.reset(request.ip, config)
|
||||
else
|
||||
banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do
|
||||
# Unless the IP is whitelisted, return true so that Allow2Ban
|
||||
# increments the counter (stored in Rails.cache) for the IP
|
||||
if config.ip_whitelist.include?(request.ip)
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
if banned
|
||||
Rails.logger.info "IP #{request.ip} failed to login " \
|
||||
"as #{login} but has been temporarily banned from Git auth"
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
|
||||
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
|
||||
def verify_user_oauth_applications_enabled
|
||||
return if current_application_settings.user_oauth_applications?
|
||||
|
||||
redirect_to applications_profile_url
|
||||
redirect_to profile_path
|
||||
end
|
||||
|
||||
def set_index_vars
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController
|
||||
|
||||
return if cached_blob?
|
||||
|
||||
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
|
||||
headers['Content-Disposition'] = 'inline'
|
||||
headers['Content-Type'] = safe_content_type(@blob)
|
||||
head :ok # 'render nothing: true' messes up the Content-Type
|
||||
send_git_blob @repository, @blob
|
||||
else
|
||||
render_404
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -41,7 +41,7 @@ class Projects::BuildsController < Projects::ApplicationController
|
||||
def trace
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: @build.trace_with_state(params[:state]).merge!(id: @build.id, status: @build.status)
|
||||
render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
class Projects::GitHttpController < Projects::ApplicationController
|
||||
attr_reader :user
|
||||
|
||||
# Git clients will not know what authenticity token to send along
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_before_action :repository
|
||||
before_action :authenticate_user
|
||||
before_action :ensure_project_found!
|
||||
|
||||
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
|
||||
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
|
||||
def info_refs
|
||||
if upload_pack? && upload_pack_allowed?
|
||||
render_ok
|
||||
elsif receive_pack? && receive_pack_allowed?
|
||||
render_ok
|
||||
else
|
||||
render_not_found
|
||||
end
|
||||
end
|
||||
|
||||
# POST /foo/bar.git/git-upload-pack (git pull)
|
||||
def git_upload_pack
|
||||
if upload_pack? && upload_pack_allowed?
|
||||
render_ok
|
||||
else
|
||||
render_not_found
|
||||
end
|
||||
end
|
||||
|
||||
# POST /foo/bar.git/git-receive-pack" (git push)
|
||||
def git_receive_pack
|
||||
if receive_pack? && receive_pack_allowed?
|
||||
render_ok
|
||||
else
|
||||
render_not_found
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user
|
||||
return if project && project.public? && upload_pack?
|
||||
|
||||
authenticate_or_request_with_http_basic do |login, password|
|
||||
auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip)
|
||||
|
||||
if auth_result.type == :ci && upload_pack?
|
||||
@ci = true
|
||||
elsif auth_result.type == :oauth && !upload_pack?
|
||||
# Not allowed
|
||||
else
|
||||
@user = auth_result.user
|
||||
end
|
||||
|
||||
ci? || user
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_project_found!
|
||||
render_not_found if project.blank?
|
||||
end
|
||||
|
||||
def project
|
||||
return @project if defined?(@project)
|
||||
|
||||
project_id, _ = project_id_with_suffix
|
||||
if project_id.blank?
|
||||
@project = nil
|
||||
else
|
||||
@project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
|
||||
end
|
||||
end
|
||||
|
||||
# This method returns two values so that we can parse
|
||||
# params[:project_id] (untrusted input!) in exactly one place.
|
||||
def project_id_with_suffix
|
||||
id = params[:project_id] || ''
|
||||
|
||||
%w[.wiki.git .git].each do |suffix|
|
||||
if id.end_with?(suffix)
|
||||
# Be careful to only remove the suffix from the end of 'id'.
|
||||
# Accidentally removing it from the middle is how security
|
||||
# vulnerabilities happen!
|
||||
return [id.slice(0, id.length - suffix.length), suffix]
|
||||
end
|
||||
end
|
||||
|
||||
# Something is wrong with params[:project_id]; do not pass it on.
|
||||
[nil, nil]
|
||||
end
|
||||
|
||||
def upload_pack?
|
||||
git_command == 'git-upload-pack'
|
||||
end
|
||||
|
||||
def receive_pack?
|
||||
git_command == 'git-receive-pack'
|
||||
end
|
||||
|
||||
def git_command
|
||||
if action_name == 'info_refs'
|
||||
params[:service]
|
||||
else
|
||||
action_name.dasherize
|
||||
end
|
||||
end
|
||||
|
||||
def render_ok
|
||||
render json: Gitlab::Workhorse.git_http_ok(repository, user)
|
||||
end
|
||||
|
||||
def repository
|
||||
_, suffix = project_id_with_suffix
|
||||
if suffix == '.wiki.git'
|
||||
project.wiki.repository
|
||||
else
|
||||
project.repository
|
||||
end
|
||||
end
|
||||
|
||||
def render_not_found
|
||||
render text: 'Not Found', status: :not_found
|
||||
end
|
||||
|
||||
def ci?
|
||||
@ci.present?
|
||||
end
|
||||
|
||||
def upload_pack_allowed?
|
||||
return false unless Gitlab.config.gitlab_shell.upload_pack
|
||||
|
||||
if user
|
||||
Gitlab::GitAccess.new(user, project).download_access_check.allowed?
|
||||
else
|
||||
ci? || project.public?
|
||||
end
|
||||
end
|
||||
|
||||
def receive_pack_allowed?
|
||||
return false unless Gitlab.config.gitlab_shell.receive_pack
|
||||
|
||||
# Skip user authorization on upload request.
|
||||
# It will be done by the pre-receive hook in the repository.
|
||||
user.present?
|
||||
end
|
||||
end
|
||||
@@ -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,13 @@ 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
|
||||
return render_404 unless @merge_request.diff_refs
|
||||
|
||||
send_git_diff @project.repository, @merge_request.diff_refs
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,8 +124,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 +195,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 +235,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 +275,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 +311,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 +321,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 +331,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
|
||||
|
||||
@@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController
|
||||
if @blob.lfs_pointer?
|
||||
send_lfs_object
|
||||
else
|
||||
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob))
|
||||
headers['Content-Disposition'] = 'inline'
|
||||
headers['Content-Type'] = safe_content_type(@blob)
|
||||
head :ok # 'render nothing: true' messes up the Content-Type
|
||||
send_git_blob @repository, @blob
|
||||
end
|
||||
else
|
||||
render_404
|
||||
|
||||
@@ -11,8 +11,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
|
||||
end
|
||||
|
||||
def archive
|
||||
headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format]))
|
||||
head :ok
|
||||
send_git_archive @repository, ref: params[:ref], format: params[:format]
|
||||
rescue => ex
|
||||
logger.error("#{self.class.name}: #{ex}")
|
||||
return git_not_found!
|
||||
|
||||
@@ -95,7 +95,7 @@ class Projects::WikisController < Projects::ApplicationController
|
||||
ext.analyze(text, author: current_user)
|
||||
|
||||
render json: {
|
||||
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
|
||||
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
|
||||
references: {
|
||||
users: ext.users.map(&:username)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -234,7 +234,7 @@ class ProjectsController < Projects::ApplicationController
|
||||
:issues_tracker_id, :default_branch,
|
||||
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
|
||||
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
|
||||
:public_builds,
|
||||
:public_builds, :only_allow_merge_if_build_succeeds
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class SessionsController < Devise::SessionsController
|
||||
before_action :load_recaptcha
|
||||
|
||||
def new
|
||||
set_minimum_password_length
|
||||
if Gitlab.config.ldap.enabled
|
||||
@ldap_servers = Gitlab::LDAP::Config.servers
|
||||
else
|
||||
@@ -30,8 +31,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 +54,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 +89,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 +117,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
|
||||
|
||||
@@ -30,4 +30,8 @@ module AppearancesHelper
|
||||
render 'shared/logo.svg'
|
||||
end
|
||||
end
|
||||
|
||||
def navbar_icon(icon_name)
|
||||
render "shared/icons/#{icon_name}.svg"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,10 @@ module ApplicationSettingsHelper
|
||||
current_application_settings.sign_in_text
|
||||
end
|
||||
|
||||
def after_sign_up_text
|
||||
current_application_settings.after_sign_up_text
|
||||
end
|
||||
|
||||
def shared_runners_text
|
||||
current_application_settings.shared_runners_text
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user