Commit 95a10f45 authored by Kamil Trzcinski's avatar Kamil Trzcinski

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

parents 17c6bec7 b4e84809
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.
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.
This diff is collapsed.
...@@ -13,7 +13,8 @@ AllCops: ...@@ -13,7 +13,8 @@ AllCops:
# Exclude some GitLab files # Exclude some GitLab files
Exclude: Exclude:
- 'vendor/**/*' - 'vendor/**/*'
- 'db/**/*' - 'db/*'
- 'db/fixtures/**/*'
- 'tmp/**/*' - 'tmp/**/*'
- 'bin/**/*' - 'bin/**/*'
- 'lib/backup/**/*' - 'lib/backup/**/*'
...@@ -194,7 +195,7 @@ Style/EmptyLines: ...@@ -194,7 +195,7 @@ Style/EmptyLines:
# Keep blank lines around access modifiers. # Keep blank lines around access modifiers.
Style/EmptyLinesAroundAccessModifier: Style/EmptyLinesAroundAccessModifier:
Enabled: false Enabled: true
# Keeps track of empty lines around block bodies. # Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody: Style/EmptyLinesAroundBlockBody:
...@@ -771,7 +772,7 @@ Metrics/PerceivedComplexity: ...@@ -771,7 +772,7 @@ Metrics/PerceivedComplexity:
# Checks for ambiguous operators in the first argument of a method invocation # Checks for ambiguous operators in the first argument of a method invocation
# without parentheses. # without parentheses.
Lint/AmbiguousOperator: Lint/AmbiguousOperator:
Enabled: false Enabled: true
# Checks for ambiguous regexp literals in the first argument of a method # Checks for ambiguous regexp literals in the first argument of a method
# invocation without parentheses. # invocation without parentheses.
...@@ -1088,6 +1089,9 @@ Rails/TimeZone: ...@@ -1088,6 +1089,9 @@ Rails/TimeZone:
Rails/Validation: Rails/Validation:
Enabled: false Enabled: false
Rails/UniqBeforePluck:
Enabled: false
##################### RSpec ################################## ##################### RSpec ##################################
# Check that instances are not being stubbed globally. # Check that instances are not being stubbed globally.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased) v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues.
- 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 - 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 - Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
- Fix issue todo not remove when leave project !4150 (Long Nguyen) - Fix issue todo not remove when leave project !4150 (Long Nguyen)
- 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 - 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 - 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 - Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects - Fix 404 page when viewing TODOs that contain milestones or labels in different projects
- Redesign navigation for project pages - Redesign navigation for project pages
- Fix groups API to list only user's accessible projects - Fix groups API to list only user's accessible projects
- Redesign account and email confirmation emails - 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 - 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 DB index on users.state
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - 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) - 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 - Fix issues filter when ordering by milestone
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
- Use downcased path to container repository as this is expected path by Docker - Use downcased path to container repository as this is expected path by Docker
- Allow to use CI token to fetch LFS objects - Allow to use CI token to fetch LFS objects
- Projects pending deletion will render a 404 page - Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails - 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 - 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) - 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 v 8.8.3
- Fix incorrect links on pipeline page when merge request created from fork - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
- Fix gitlab importer failing to import new projects due to missing credentials - Fixed JS error when trying to remove discussion form. !4303
- Fix import URL migration not rescuing with the correct Error - Fixed issue with button color when no CI enabled. !4287
- In search results, only show notes on confidential issues that the user has access to - Fixed potential issue with 2 CI status polling events happening. !3869
- Fix health check access token changing due to old application settings being used - 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 v 8.8.2
- Added remove due date button. !4209 - Added remove due date button. !4209
...@@ -126,6 +190,7 @@ v 8.8.0 ...@@ -126,6 +190,7 @@ v 8.8.0
- Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) - 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) - 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 - 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 v 8.7.7
- Fix import by `Any Git URL` broken if the URL contains a space - Fix import by `Any Git URL` broken if the URL contains a space
...@@ -135,6 +200,7 @@ v 8.7.6 ...@@ -135,6 +200,7 @@ v 8.7.6
- Fix import from GitLab.com to a private instance failure. !4181 - Fix import from GitLab.com to a private instance failure. !4181
- Fix external imports not finding the import data. !4106 - Fix external imports not finding the import data. !4106
- Fix notification delay when changing status of an issue - 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 v 8.7.5
- Fix relative links in wiki pages. !4050 - Fix relative links in wiki pages. !4050
......
...@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the ...@@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the
[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design [free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
(the PNG is 1:1). (the PNG is 1:1).
The current designs can be found in the [`gitlab1.atype` file]. The current designs can be found in the [`gitlab8.atype` file].
### UI development kit ### UI development kit
...@@ -308,7 +308,7 @@ tests are least likely to receive timely feedback. The workflow to make a merge ...@@ -308,7 +308,7 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows: request is as follows:
1. Fork the project into your personal space on GitLab.com 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. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG) 1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] 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. ...@@ -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. entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass - Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant. 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 ## Changes for Stable Releases
...@@ -530,4 +531,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -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" [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ [`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
[license-finder-doc]: doc/development/licensing.md
...@@ -38,16 +38,17 @@ gem 'rack-oauth2', '~> 1.2.1' ...@@ -38,16 +38,17 @@ gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt' gem 'jwt'
# Spam and anti-bot protection # Spam and anti-bot protection
gem 'recaptcha', require: 'recaptcha/rails' gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0' gem 'akismet', '~> 2.0'
# Two-factor authentication # Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0' gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7' gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0' gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# Browser detection # Browser detection
gem "browser", '~> 1.0.0' gem "browser", '~> 2.0.3'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
...@@ -83,8 +84,15 @@ gem "carrierwave", '~> 0.10.0' ...@@ -83,8 +84,15 @@ gem "carrierwave", '~> 0.10.0'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' 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 # for aws storage
gem "fog", "~> 1.36.0"
gem "unf", '~> 0.1.4' gem "unf", '~> 0.1.4'
# Authorization # Authorization
...@@ -104,7 +112,7 @@ gem 'org-ruby', '~> 0.9.12' ...@@ -104,7 +112,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'rouge', '~> 1.10.1' gem 'rouge', '~> 1.11'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -137,7 +145,7 @@ gem 'redis-namespace' ...@@ -137,7 +145,7 @@ gem 'redis-namespace'
gem "httparty", '~> 0.13.3' gem "httparty", '~> 0.13.3'
# Colored output to console # Colored output to console
gem "colorize", '~> 0.7.0' gem "rainbow", '~> 2.1.0'
# GitLab settings # GitLab settings
gem 'settingslogic', '~> 2.0.9' gem 'settingslogic', '~> 2.0.9'
...@@ -299,6 +307,9 @@ group :development, :test do ...@@ -299,6 +307,9 @@ group :development, :test do
gem 'bundler-audit', require: false gem 'bundler-audit', require: false
gem 'benchmark-ips', require: false gem 'benchmark-ips', require: false
gem "license_finder", require: false
gem 'knapsack'
end end
group :test do group :test do
......
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (2.3.2)
RedCloth (4.2.9) RedCloth (4.2.9)
ace-rails-ap (4.0.2) ace-rails-ap (4.0.2)
actionmailer (4.2.6) actionmailer (4.2.6)
...@@ -71,6 +70,21 @@ GEM ...@@ -71,6 +70,21 @@ GEM
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1) 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) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
bcrypt (3.1.11) bcrypt (3.1.11)
...@@ -93,7 +107,7 @@ GEM ...@@ -93,7 +107,7 @@ GEM
sass (~> 3.0) sass (~> 3.0)
slim (>= 1.3.6, < 4.0) slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4) terminal-table (~> 1.4)
browser (1.0.1) browser (2.0.3)
builder (3.2.2) builder (3.2.2)
bullet (5.0.0) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
...@@ -183,7 +197,7 @@ GEM ...@@ -183,7 +197,7 @@ GEM
erubis (2.7.0) erubis (2.7.0)
escape_utils (1.1.1) escape_utils (1.1.1)
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.45.4) excon (0.49.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
factory_girl (4.5.0) factory_girl (4.5.0)
...@@ -200,8 +214,6 @@ GEM ...@@ -200,8 +214,6 @@ GEM
multi_json multi_json
ffaker (2.0.0) ffaker (2.0.0)
ffi (1.9.10) ffi (1.9.10)
fission (0.5.0)
CFPropertyList (~> 2.2)
flay (2.6.1) flay (2.6.1)
ruby_parser (~> 3.0) ruby_parser (~> 3.0)
sexp_processor (~> 4.0) sexp_processor (~> 4.0)
...@@ -211,109 +223,33 @@ GEM ...@@ -211,109 +223,33 @@ GEM
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
fog (1.36.0) fog-aws (0.9.2)
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-core (~> 1.27)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
xml-simple (~> 1.1) fog-azure (0.0.2)
fog-atmos (0.1.0) azure (~> 0.6)
fog-core
fog-xml
fog-aws (0.8.1)
fog-core (~> 1.27) fog-core (~> 1.27)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) fog-core (1.40.0)
fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
fog-core (1.35.0)
builder builder
excon (~> 0.45) excon (~> 0.49)
formatador (~> 0.2) formatador (~> 0.2)
fog-dynect (0.0.2) fog-google (0.3.2)
fog-core
fog-json
fog-xml
fog-ecloud (0.3.0)
fog-core
fog-xml
fog-google (0.1.0)
fog-core fog-core
fog-json fog-json
fog-xml fog-xml
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
fog-local (0.2.1) fog-local (0.3.0)
fog-core (~> 1.27) fog-core (~> 1.27)
fog-powerdns (0.1.1) fog-openstack (0.1.6)
fog-core (~> 1.27) fog-core (>= 1.39)
fog-json (~> 1.0) fog-json (>= 1.0)
fog-xml (~> 0.1) ipaddress (>= 0.8)
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-xml (0.1.2) fog-xml (0.1.2)
fog-core fog-core
nokogiri (~> 1.5, >= 1.5.11) nokogiri (~> 1.5, >= 1.5.11)
...@@ -422,11 +358,10 @@ GEM ...@@ -422,11 +358,10 @@ GEM
httpclient (2.7.0.1) httpclient (2.7.0.1)
i18n (0.7.0) i18n (0.7.0)
ice_nine (0.11.1) ice_nine (0.11.1)
inflecto (0.0.2)
influxdb (0.2.3) influxdb (0.2.3)
cause cause
json json
ipaddress (0.8.2) ipaddress (0.8.3)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1) jquery-rails (4.1.1)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
...@@ -443,6 +378,9 @@ GEM ...@@ -443,6 +378,9 @@ GEM
actionpack (>= 3.0.0) actionpack (>= 3.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
kgio (2.10.0) kgio (2.10.0)
knapsack (1.11.0)
rake
timecop (>= 0.1.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.4.1)
...@@ -451,6 +389,12 @@ GEM ...@@ -451,6 +389,12 @@ GEM
actionmailer (>= 3.2) actionmailer (>= 3.2)
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
license_finder (2.1.0)
bundler
httparty
rubyzip
thor
xml-simple
licensee (8.0.0) licensee (8.0.0)
rugged (>= 0.24b) rugged (>= 0.24b)
listen (3.0.5) listen (3.0.5)
...@@ -466,7 +410,7 @@ GEM ...@@ -466,7 +410,7 @@ GEM
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.1) mime-types (2.99.1)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_portile2 (2.0.0) mini_portile2 (2.1.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
multi_json (1.11.2) multi_json (1.11.2)
...@@ -477,8 +421,9 @@ GEM ...@@ -477,8 +421,9 @@ GEM
net-ldap (0.12.1) net-ldap (0.12.1)
net-ssh (3.0.1) net-ssh (3.0.1)
newrelic_rpm (3.14.1.311) newrelic_rpm (3.14.1.311)
nokogiri (1.6.7.2) nokogiri (1.6.8)
mini_portile2 (~> 2.0.0.rc2) mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
oauth (0.4.7) oauth (0.4.7)
oauth2 (1.0.0) oauth2 (1.0.0)
faraday (>= 0.8, < 0.10) faraday (>= 0.8, < 0.10)
...@@ -550,6 +495,7 @@ GEM ...@@ -550,6 +495,7 @@ GEM
parser (2.3.1.0) parser (2.3.1.0)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
pkg-config (1.1.7)
poltergeist (1.9.0) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
cliver (~> 0.3.1) cliver (~> 0.3.1)
...@@ -625,7 +571,7 @@ GEM ...@@ -625,7 +571,7 @@ GEM
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rdoc (3.12.2) rdoc (3.12.2)
json (~> 1.4) json (~> 1.4)
recaptcha (1.0.2) recaptcha (3.0.0)
json json
redcarpet (3.3.3) redcarpet (3.3.3)
redis (3.3.0) redis (3.3.0)
...@@ -654,7 +600,7 @@ GEM ...@@ -654,7 +600,7 @@ GEM
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rinku (1.7.3) rinku (1.7.3)
rotp (2.1.2) rotp (2.1.2)
rouge (1.10.1) rouge (1.11.0)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -703,6 +649,7 @@ GEM ...@@ -703,6 +649,7 @@ GEM
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
rubyntlm (0.5.2) rubyntlm (0.5.2)
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.0)
rufus-scheduler (3.1.10) rufus-scheduler (3.1.10)
rugged (0.24.0) rugged (0.24.0)
safe_yaml (1.0.4) safe_yaml (1.0.4)
...@@ -813,6 +760,7 @@ GEM ...@@ -813,6 +760,7 @@ GEM
thor (0.19.1) thor (0.19.1)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (2.0.2) tilt (2.0.2)
timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tinder (1.10.1) tinder (1.10.1)
eventmachine (~> 1.0) eventmachine (~> 1.0)
...@@ -832,6 +780,7 @@ GEM ...@@ -832,6 +780,7 @@ GEM
simple_oauth (~> 0.1.4) simple_oauth (~> 0.1.4)
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1)
uglifier (2.7.2) uglifier (2.7.2)
execjs (>= 0.3.0) execjs (>= 0.3.0)
json (>= 1.8.0) json (>= 1.8.0)
...@@ -900,7 +849,7 @@ DEPENDENCIES ...@@ -900,7 +849,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
brakeman (~> 3.2.0) brakeman (~> 3.2.0)
browser (~> 1.0.0) browser (~> 2.0.3)
bullet bullet
bundler-audit bundler-audit
byebug byebug
...@@ -909,7 +858,6 @@ DEPENDENCIES ...@@ -909,7 +858,6 @@ DEPENDENCIES
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0) connection_pool (~> 2.0)
coveralls (~> 0.8.2) coveralls (~> 0.8.2)
creole (~> 0.5.0) creole (~> 0.5.0)
...@@ -927,7 +875,12 @@ DEPENDENCIES ...@@ -927,7 +875,12 @@ DEPENDENCIES
ffaker (~> 2.0.0) ffaker (~> 2.0.0)
flay flay
flog 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) font-awesome-rails (~> 4.2)
foreman foreman
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
...@@ -956,7 +909,9 @@ DEPENDENCIES ...@@ -956,7 +909,9 @@ DEPENDENCIES
jquery-ui-rails (~> 5.0.0) jquery-ui-rails (~> 5.0.0)
jwt jwt
kaminari (~> 0.17.0) kaminari (~> 0.17.0)
knapsack
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder
licensee (~> 8.0.0) licensee (~> 8.0.0)
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.7) mail_room (~> 0.7)
...@@ -996,10 +951,11 @@ DEPENDENCIES ...@@ -996,10 +951,11 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rails (= 4.2.6) rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
raphael-rails (~> 2.1.2) raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
recaptcha recaptcha (~> 3.0)
redcarpet (~> 3.3.3) redcarpet (~> 3.3.3)
redis (~> 3.2) redis (~> 3.2)
redis-namespace redis-namespace
...@@ -1007,7 +963,7 @@ DEPENDENCIES ...@@ -1007,7 +963,7 @@ DEPENDENCIES
request_store (~> 1.3.0) request_store (~> 1.3.0)
rerun (~> 0.11.0) rerun (~> 0.11.0)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 1.10.1) rouge (~> 1.11)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.4.0) rspec-rails (~> 3.4.0)
rspec-retry rspec-retry
...@@ -1045,6 +1001,7 @@ DEPENDENCIES ...@@ -1045,6 +1001,7 @@ DEPENDENCIES
thin (~> 1.6.1) thin (~> 1.6.1)
tinder (~> 1.10.0) tinder (~> 1.10.0)
turbolinks (~> 2.5.0) turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0) underscore-rails (~> 1.8.0)
unf (~> 0.1.4) unf (~> 0.1.4)
...@@ -1057,4 +1014,4 @@ DEPENDENCIES ...@@ -1057,4 +1014,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.12.4 1.12.5
...@@ -8,3 +8,5 @@ relative_url_conf = File.expand_path('../config/initializers/relative_url', __FI ...@@ -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") require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks Gitlab::Application.load_tasks
Knapsack.load_tasks if defined?(Knapsack)
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
class @Activities class @Activities
constructor: -> constructor: ->
Pager.init 20, true Pager.init 20, true, false, @updateTooltips
$(".event-filter-link").on "click", (event) => $(".event-filter-link").on "click", (event) =>
event.preventDefault() event.preventDefault()
@toggleFilter($(event.currentTarget)) @toggleFilter($(event.currentTarget))
@reloadActivities() @reloadActivities()
updateTooltips: ->
gl.utils.localTimeAgo($('.js-timeago', '#activity'))
reloadActivities: -> reloadActivities: ->
$(".content_list").html '' $(".content_list").html ''
Pager.init 20, true Pager.init 20, true
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file. # the compiled file.
# #
#= require jquery #= require jquery2
#= require jquery-ui/autocomplete #= require jquery-ui/autocomplete
#= require jquery-ui/datepicker #= require jquery-ui/datepicker
#= require jquery-ui/draggable #= require jquery-ui/draggable
...@@ -35,7 +35,6 @@ ...@@ -35,7 +35,6 @@
#= require raphael #= require raphael
#= require g.raphael #= require g.raphael
#= require g.bar #= require g.bar
#= require Chart
#= require branch-graph #= require branch-graph
#= require ace/ace #= require ace/ace
#= require ace/ext-searchbox #= require ace/ext-searchbox
...@@ -56,9 +55,11 @@ ...@@ -56,9 +55,11 @@
#= require_directory ./commit #= require_directory ./commit
#= require_directory ./extensions #= require_directory ./extensions
#= require_directory ./lib #= require_directory ./lib
#= require_directory ./u2f
#= require_directory . #= require_directory .
#= require fuzzaldrin-plus #= require fuzzaldrin-plus
#= require cropper #= require cropper
#= require u2f
window.slugify = (text) -> window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
...@@ -224,6 +225,10 @@ $ -> ...@@ -224,6 +225,10 @@ $ ->
form = btn.closest("form") form = btn.closest("form")
new ConfirmDangerModal(form, text) new ConfirmDangerModal(form, text)
$(document).on 'click', 'button', ->
$(this).blur()
$('input[type="search"]').each -> $('input[type="search"]').each ->
$this = $(this) $this = $(this)
$this.attr 'value', $this.val() $this.attr 'value', $this.val()
...@@ -262,9 +267,10 @@ $ -> ...@@ -262,9 +267,10 @@ $ ->
$(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
$(window) $(window)
.off "resize" .off "resize.app"
.on "resize", (e) -> .on "resize.app", (e) ->
fitSidebarForSize() fitSidebarForSize()
gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize() checkInitialSidebarSize()
new Aside() new Aside()
class CiBuild class @CiBuild
@interval: null @interval: null
@state: null @state: null
constructor: (build_url, build_status, build_state) -> constructor: (@build_url, @build_status, @state) ->
clearInterval(CiBuild.interval) 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 # Bind autoscroll button to follow build output
# #
$("#autoscroll-button").bind "click", -> $('#autoscroll-button').on 'click', ->
state = $(this).data("state") state = $(this).data("state")
if "enabled" is state if "enabled" is state
$(this).data "state", "disabled" $(this).data "state", "disabled"
...@@ -27,26 +39,37 @@ class CiBuild ...@@ -27,26 +39,37 @@ class CiBuild
# Only valid for runnig build when output changes during time # Only valid for runnig build when output changes during time
# #
CiBuild.interval = setInterval => CiBuild.interval = setInterval =>
if window.location.href.split("#").first() is build_url if window.location.href.split("#").first() is @build_url
last_state = @state @getBuildTrace()
$.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
, 4000 , 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: -> checkAutoscroll: ->
$("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
...@@ -61,4 +84,22 @@ class CiBuild ...@@ -61,4 +84,22 @@ class CiBuild
$body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top) $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 ...@@ -17,6 +17,7 @@ class Dispatcher
switch page switch page
when 'projects:issues:index' when 'projects:issues:index'
Issuable.init() Issuable.init()
new IssuableBulkActions()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show' when 'projects:issues:show'
new Issue() new Issue()
...@@ -97,6 +98,8 @@ class Dispatcher ...@@ -97,6 +98,8 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:labels:new', 'projects:labels:edit' when 'projects:labels:new', 'projects:labels:edit'
new Labels() new Labels()
when 'projects:labels:index'
new LabelManager() if $('.prioritized-labels').length
when 'projects:network:show' when 'projects:network:show'
# Ensure we don't create a particular shortcut handler here. This is # Ensure we don't create a particular shortcut handler here. This is
# already created, where the network graph is created. # already created, where the network graph is created.
......
...@@ -21,7 +21,7 @@ class @DueDateSelect ...@@ -21,7 +21,7 @@ class @DueDateSelect
$dropdown.glDropdown( $dropdown.glDropdown(
hidden: -> hidden: ->
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
) )
addDueDate = (isDropdown) -> addDueDate = (isDropdown) ->
...@@ -42,12 +42,13 @@ class @DueDateSelect ...@@ -42,12 +42,13 @@ class @DueDateSelect
type: 'PUT' type: 'PUT'
url: issueUpdateURL url: issueUpdateURL
data: data data: data
dataType: 'json'
beforeSend: -> beforeSend: ->
$loading.fadeIn() $loading.fadeIn()
if isDropdown if isDropdown
$dropdown.trigger('loading.gl.dropdown') $dropdown.trigger('loading.gl.dropdown')
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
$valueContent.html(mediumDate) $valueContent.html(mediumDate)
$sidebarValue.html(mediumDate) $sidebarValue.html(mediumDate)
......
class @Flash class @Flash
constructor: (message, type)-> constructor: (message, type = 'alert')->
@flash = $(".flash-container") @flash = $(".flash-container")
@flash.html("") @flash.html("")
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
window.GitLab ?= {} window.GitLab ?= {}
GitLab.GfmAutoComplete = GitLab.GfmAutoComplete =
dataLoading: false dataLoading: false
dataLoaded: false
dataSource: '' dataSource: ''
...@@ -22,6 +23,24 @@ GitLab.GfmAutoComplete = ...@@ -22,6 +23,24 @@ GitLab.GfmAutoComplete =
Milestones: Milestones:
template: '<li>${title}</li>' 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. # Add GFM auto-completion to all input fields, that accept GFM input.
setup: (wrap) -> setup: (wrap) ->
@input = $('.js-gfm-input') @input = $('.js-gfm-input')
...@@ -53,18 +72,37 @@ GitLab.GfmAutoComplete = ...@@ -53,18 +72,37 @@ GitLab.GfmAutoComplete =
# Emoji # Emoji
@input.atwho @input.atwho
at: ':' at: ':'
displayTpl: @Emoji.template displayTpl: (value) =>
if value.path?
@Emoji.template
else
@Loading.template
insertTpl: ':${name}:' insertTpl: ':${name}:'
data: ['loading']
callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
# Team Members # Team Members
@input.atwho @input.atwho
at: '@' at: '@'
displayTpl: @Members.template displayTpl: (value) =>
if value.username?
@Members.template
else
@Loading.template
insertTpl: '${atwho-at}${username}' insertTpl: '${atwho-at}${username}'
searchKey: 'search' searchKey: 'search'
data: ['loading']
callbacks: callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (members) -> beforeSave: (members) ->
$.map members, (m) -> $.map members, (m) ->
return m if not m.username?
title = m.name title = m.name
title += " (#{m.count})" if m.count title += " (#{m.count})" if m.count
...@@ -76,11 +114,21 @@ GitLab.GfmAutoComplete = ...@@ -76,11 +114,21 @@ GitLab.GfmAutoComplete =
at: '#' at: '#'
alias: 'issues' alias: 'issues'
searchKey: 'search' searchKey: 'search'
displayTpl: @Issues.template displayTpl: (value) =>
if value.title?
@Issues.template
else
@Loading.template
data: ['loading']
insertTpl: '${atwho-at}${id}' insertTpl: '${atwho-at}${id}'
callbacks: callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (issues) -> beforeSave: (issues) ->
$.map issues, (i) -> $.map issues, (i) ->
return i if not i.title?
id: i.iid id: i.iid
title: sanitize(i.title) title: sanitize(i.title)
search: "#{i.iid} #{i.title}" search: "#{i.iid} #{i.title}"
...@@ -89,11 +137,18 @@ GitLab.GfmAutoComplete = ...@@ -89,11 +137,18 @@ GitLab.GfmAutoComplete =
at: '%' at: '%'
alias: 'milestones' alias: 'milestones'
searchKey: 'search' searchKey: 'search'
displayTpl: @Milestones.template displayTpl: (value) =>
if value.title?
@Milestones.template
else
@Loading.template
insertTpl: '${atwho-at}"${title}"' insertTpl: '${atwho-at}"${title}"'
data: ['loading']
callbacks: callbacks:
beforeSave: (milestones) -> beforeSave: (milestones) ->
$.map milestones, (m) -> $.map milestones, (m) ->
return m if not m.title?
id: m.iid id: m.iid
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.title}" search: "#{m.title}"
...@@ -102,11 +157,21 @@ GitLab.GfmAutoComplete = ...@@ -102,11 +157,21 @@ GitLab.GfmAutoComplete =
at: '!' at: '!'
alias: 'mergerequests' alias: 'mergerequests'
searchKey: 'search' searchKey: 'search'
displayTpl: @Issues.template displayTpl: (value) =>
if value.title?
@Issues.template
else
@Loading.template
data: ['loading']
insertTpl: '${atwho-at}${id}' insertTpl: '${atwho-at}${id}'
callbacks: callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (merges) -> beforeSave: (merges) ->
$.map merges, (m) -> $.map merges, (m) ->
return m if not m.title?
id: m.iid id: m.iid
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.iid} #{m.title}" search: "#{m.iid} #{m.title}"
...@@ -118,6 +183,8 @@ GitLab.GfmAutoComplete = ...@@ -118,6 +183,8 @@ GitLab.GfmAutoComplete =
$.getJSON(dataSource) $.getJSON(dataSource)
loadData: (data) -> loadData: (data) ->
@dataLoaded = true
# load members # load members
@input.atwho 'load', '@', data.members @input.atwho 'load', '@', data.members
# load issues # load issues
...@@ -128,3 +195,7 @@ GitLab.GfmAutoComplete = ...@@ -128,3 +195,7 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests @input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis # load emojis
@input.atwho 'load', ':', data.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 ...@@ -11,6 +11,8 @@ class GitLabDropdownFilter
$inputContainer = @input.parent() $inputContainer = @input.parent()
$clearButton = $inputContainer.find('.js-dropdown-input-clear') $clearButton = $inputContainer.find('.js-dropdown-input-clear')
@indeterminateIds = []
# Clear click # Clear click
$clearButton.on 'click', (e) => $clearButton.on 'click', (e) =>
e.preventDefault() e.preventDefault()
...@@ -35,20 +37,20 @@ class GitLabDropdownFilter ...@@ -35,20 +37,20 @@ class GitLabDropdownFilter
if keyCode is 13 if keyCode is 13
return false return false
clearTimeout timeout # Only filter asynchronously only if option remote is set
timeout = setTimeout => if @options.remote
blur_field = @shouldBlur keyCode clearTimeout timeout
search_text = @input.val() timeout = setTimeout =>
blur_field = @shouldBlur keyCode
if blur_field and @filterInputBlur if blur_field and @filterInputBlur
@input.blur() @input.blur()
if @options.remote @options.query @input.val(), (data) =>
@options.query search_text, (data) =>
@options.callback(data) @options.callback(data)
else , 250
@filter search_text else
, 250 @filter @input.val()
shouldBlur: (keyCode) -> shouldBlur: (keyCode) ->
return BLUR_KEYCODES.indexOf(keyCode) >= 0 return BLUR_KEYCODES.indexOf(keyCode) >= 0
...@@ -142,6 +144,7 @@ class GitLabDropdown ...@@ -142,6 +144,7 @@ class GitLabDropdown
LOADING_CLASS = "is-loading" LOADING_CLASS = "is-loading"
PAGE_TWO_CLASS = "is-page-two" PAGE_TWO_CLASS = "is-page-two"
ACTIVE_CLASS = "is-active" ACTIVE_CLASS = "is-active"
INDETERMINATE_CLASS = "is-indeterminate"
currentIndex = -1 currentIndex = -1
FILTER_INPUT = '.dropdown-input .dropdown-input-field' FILTER_INPUT = '.dropdown-input .dropdown-input-field'
...@@ -182,9 +185,6 @@ class GitLabDropdown ...@@ -182,9 +185,6 @@ class GitLabDropdown
@fullData = data @fullData = data
@parseData @fullData @parseData @fullData
if @options.filterable
@filterInput.trigger 'keyup'
} }
# Init filterable # Init filterable
...@@ -211,6 +211,7 @@ class GitLabDropdown ...@@ -211,6 +211,7 @@ class GitLabDropdown
@dropdown.on "shown.bs.dropdown", @opened @dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden @dropdown.on "hidden.bs.dropdown", @hidden
$(@el).on "update.label", @updateLabel
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
@dropdown.on 'keyup', (e) => @dropdown.on 'keyup', (e) =>
if e.which is 27 # Escape key if e.which is 27 # Escape key
...@@ -298,6 +299,13 @@ class GitLabDropdown ...@@ -298,6 +299,13 @@ class GitLabDropdown
opened: => opened: =>
@addArrowKeyEvent() @addArrowKeyEvent()
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
contentHtml = $('.dropdown-content', @dropdown).html() contentHtml = $('.dropdown-content', @dropdown).html()
if @remote && contentHtml is "" if @remote && contentHtml is ""
@remote.execute() @remote.execute()
...@@ -309,12 +317,18 @@ class GitLabDropdown ...@@ -309,12 +317,18 @@ class GitLabDropdown
hidden: (e) => hidden: (e) =>
@removeArrayKeyEvent() @removeArrayKeyEvent()
$input = @dropdown.find(".dropdown-input-field")
if @options.filterable if @options.filterable
@dropdown $input
.find(".dropdown-input-field")
.blur() .blur()
.val("") .val("")
.trigger("keyup")
# Triggering 'keyup' will re-render the dropdown which is not always required
# specially if we want to keep the state of the dropdown needed for bulk-assignment
if not @options.persistWhenHide
$input.trigger("keyup")
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
$('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
...@@ -358,7 +372,7 @@ class GitLabDropdown ...@@ -358,7 +372,7 @@ class GitLabDropdown
if @options.renderRow if @options.renderRow
# Call the render function # Call the render function
html = @options.renderRow(data) html = @options.renderRow.call(@options, data, @)
else else
if not selected if not selected
value = if @options.id then @options.id(data) else data.id value = if @options.id then @options.id(data) else data.id
...@@ -440,9 +454,20 @@ class GitLabDropdown ...@@ -440,9 +454,20 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel @updateLabel()
else else
selectedObject selectedObject
else if el.hasClass(INDETERMINATE_CLASS)
el.addClass ACTIVE_CLASS
el.removeClass INDETERMINATE_CLASS
if not value?
field.remove()
if not field.length and fieldName
@addInput(fieldName, value)
return selectedObject
else else
if not @options.multiSelect or el.hasClass('dropdown-clear-active') if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
...@@ -456,34 +481,45 @@ class GitLabDropdown ...@@ -456,34 +481,45 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) @updateLabel(selectedObject, el)
if value? if value?
if !field.length and fieldName if !field.length and fieldName
# Create hidden input for form @addInput(fieldName, value)
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId?
input = $(input)
.attr('id', @options.inputId)
@dropdown.before input
else else
field.val value field.val value
return selectedObject return selectedObject
selectRowAtIndex: (index) -> addInput: (fieldName, value)->
selector = ".dropdown-content li:not(.divider):eq(#{index}) a" # Create hidden input for form
$input = $('<input>').attr('type', 'hidden')
.attr('name', fieldName)
.val(value)
if @options.inputId?
$input.attr('id', @options.inputId)
@dropdown.before $input
selectRowAtIndex: (e, index) ->
selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link # simulate a click on the first link
$(selector, @dropdown).trigger "click" $el = $(selector, @dropdown)
if $el.length
e.preventDefault()
e.stopImmediatePropagation()
$(selector, @dropdown)[0].click()
addArrowKeyEvent: -> addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40] ARROW_KEY_CODES = [38, 40]
$input = @dropdown.find(".dropdown-input-field") $input = @dropdown.find(".dropdown-input-field")
selector = '.dropdown-content li:not(.divider)' selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
if @dropdown.find(".dropdown-toggle-page").length if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
...@@ -511,8 +547,8 @@ class GitLabDropdown ...@@ -511,8 +547,8 @@ class GitLabDropdown
return false return false
if currentKeyCode is 13 if currentKeyCode is 13 and currentIndex isnt -1
@selectRowAtIndex if currentIndex < 0 then 0 else currentIndex @selectRowAtIndex e, currentIndex
removeArrayKeyEvent: -> removeArrayKeyEvent: ->
$('body').off 'keydown' $('body').off 'keydown'
...@@ -544,6 +580,9 @@ class GitLabDropdown ...@@ -544,6 +580,9 @@ class GitLabDropdown
# Scroll the dropdown content up # Scroll the dropdown content up
$dropdownContent.scrollTop(listItemTop - dropdownContentTop) $dropdownContent.scrollTop(listItemTop - dropdownContentTop)
updateLabel: (selected = null, el = null) =>
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el)
$.fn.glDropdown = (opts) -> $.fn.glDropdown = (opts) ->
return @.each -> return @.each ->
if (!$.data @, 'glDropdown') if (!$.data @, 'glDropdown')
......
...@@ -4,4 +4,5 @@ ...@@ -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 # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file. # the compiled file.
# #
#= require Chart
#= require_tree . #= require_tree .
...@@ -6,12 +6,18 @@ issuable_created = false ...@@ -6,12 +6,18 @@ issuable_created = false
Issuable.initTemplates() Issuable.initTemplates()
Issuable.initSearch() Issuable.initSearch()
Issuable.initChecks() Issuable.initChecks()
Issuable.initLabelFilterRemove()
initTemplates: -> initTemplates: ->
Issuable.labelRow = _.template( Issuable.labelRow = _.template(
'<% _.each(labels, function(label){ %> '<% _.each(labels, function(label){ %>
<span class="label-row"> <span class="label-row btn-group" role="group" aria-label="<%= _.escape(label.title) %>" style="color: <%= label.text_color %>;">
<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> <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> </span>
<% }); %>' <% }); %>'
) )
...@@ -35,6 +41,21 @@ issuable_created = false ...@@ -35,6 +41,21 @@ issuable_created = false
Issuable.filterResults $form Issuable.filterResults $form
, 500) , 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: -> toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels') $filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0 if $filteredLabels.find('.label-row').length > 0
......
class @IssuableBulkActions
constructor: (opts = {}) ->
# Set defaults
{
@container = $('.content')
@form = @getElement('.bulk-update')
@issues = @getElement('.issues-list .issue')
} = opts
@bindEvents()
getElement: (selector) ->
@container.find selector
bindEvents: ->
@form.off('submit').on('submit', @onFormSubmit.bind(@))
onFormSubmit: (e) ->
e.preventDefault()
@submit()
submit: ->
_this = @
xhr = $.ajax
url: @form.attr 'action'
method: @form.attr 'method'
dataType: 'JSON',
data: @getFormDataAsObject()
xhr.done (response, status, xhr) ->
location.reload()
xhr.fail ->
new Flash("Issue update failed")
xhr.always @onFormSubmitAlways.bind(@)
onFormSubmitAlways: ->
@form.find('[type="submit"]').enable()
getSelectedIssues: ->
@issues.has('.selected_issue:checked')
getLabelsFromSelection: ->
labels = []
@getSelectedIssues().map ->
_labels = $(@).data('labels')
if _labels
_labels.map (labelId) ->
labels.push(labelId) if labels.indexOf(labelId) is -1
labels
###*
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
###
getUnmarkedIndeterminedLabels: ->
result = []
labelsToKeep = []
for el in @getElement('.labels-filter .is-indeterminate')
labelsToKeep.push $(el).data('labelId')
for id in @getLabelsFromSelection()
# Only the ones that we are not going to keep
result.push(id) if labelsToKeep.indexOf(id) is -1
result
###*
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
###
getFormDataAsObject: ->
formData =
update:
state_event : @form.find('input[name="update[state_event]"]').val()
assignee_id : @form.find('input[name="update[assignee_id]"]').val()
milestone_id : @form.find('input[name="update[milestone_id]"]').val()
issues_ids : @form.find('input[name="update[issues_ids]"]').val()
add_label_ids : []
remove_label_ids : []
@getLabelsToApply().map (id) ->
formData.update.add_label_ids.push id
@getLabelsToRemove().map (id) ->
formData.update.remove_label_ids.push id
formData
getLabelsToApply: ->
labelIds = []
$labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
$labels.each (k, label) ->
labelIds.push $(label).val() if label
labelIds
###*
* Just an alias of @getUnmarkedIndeterminedLabels
* @return {Array} Array of labels
###
getLabelsToRemove: ->
@getUnmarkedIndeterminedLabels()
class @LabelsSelect class @LabelsSelect
constructor: -> constructor: ->
_this = @
$('.js-label-select').each (i, dropdown) -> $('.js-label-select').each (i, dropdown) ->
$dropdown = $(dropdown) $dropdown = $(dropdown)
projectId = $dropdown.data('project-id') projectId = $dropdown.data('project-id')
...@@ -93,8 +95,11 @@ class @LabelsSelect ...@@ -93,8 +95,11 @@ class @LabelsSelect
$newLabelCreateButton.enable() $newLabelCreateButton.enable()
if label.message? if label.message?
errors = _.map label.message, (value, key) ->
"#{key} #{value[0]}"
$newLabelError $newLabelError
.text label.message .html errors.join("<br/>")
.show() .show()
else else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click' $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
...@@ -196,10 +201,18 @@ class @LabelsSelect ...@@ -196,10 +201,18 @@ class @LabelsSelect
callback data callback data
renderRow: (label) -> renderRow: (label, instance) ->
removesAll = label.id is 0 or not label.id? $li = $('<li>')
$a = $('<a href="#">')
selectedClass = [] selectedClass = []
removesAll = label.id is 0 or not label.id?
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
if $form.find("input[type='hidden']\ if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\ [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length [value='#{this.id(label)}']").length
...@@ -230,17 +243,21 @@ class @LabelsSelect ...@@ -230,17 +243,21 @@ class @LabelsSelect
else else
colorEl = '' colorEl = ''
"<li> # We need to identify which items are actually labels
<a href='#' class='#{selectedClass.join(' ')}'> if label.id
#{colorEl} selectedClass.push('label-item')
#{_.escape(label.title)} $a.attr('data-label-id', label.id)
</a>
</li>" $a.addClass(selectedClass.join(' '))
filterable: true .html("#{colorEl} #{_.escape(label.title)}")
# Return generated html
$li.html($a).prop('outerHTML')
persistWhenHide: $dropdown.data('persistWhenHide')
search: search:
fields: ['title'] fields: ['title']
selectable: true selectable: true
filterable: true
toggleLabel: (selected, el) -> toggleLabel: (selected, el) ->
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
...@@ -280,10 +297,19 @@ class @LabelsSelect ...@@ -280,10 +297,19 @@ class @LabelsSelect
else if $dropdown.hasClass('js-filter-submit') else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
saveLabelData() if not $dropdown.hasClass 'js-filter-bulk-update'
saveLabelData()
if $dropdown.hasClass('js-filter-bulk-update')
# If we are persisting state we need the classes
if not @options.persistWhenHide
$dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
multiSelect: $dropdown.hasClass 'js-multiselect' multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) -> clicked: (label) ->
if $dropdown.hasClass('js-filter-bulk-update')
return
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index' isMRIndex = page is 'projects:merge_requests:index'
...@@ -298,4 +324,31 @@ class @LabelsSelect ...@@ -298,4 +324,31 @@ class @LabelsSelect
return return
else else
saveLabelData() saveLabelData()
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
) )
@bindEvents()
bindEvents: ->
$('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
onSelectCheckboxIssue: ->
return if $('.selected_issue:checked').length
# Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
# Also restore button text
$('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
getIndeterminateIds: ->
label_ids = []
$('.selected_issue:checked').each (i, el) ->
issue_id = $(el).data('id')
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
((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 @@ ...@@ -12,6 +12,13 @@
$el.attr('title', gl.utils.formatDate($el.attr('datetime'))) $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 ) window
gl.emojiAliases = ->
JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
...@@ -24,11 +24,21 @@ class @MilestoneSelect ...@@ -24,11 +24,21 @@ class @MilestoneSelect
if issueUpdateURL if issueUpdateURL
milestoneLinkTemplate = _.template( 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>' 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( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
...@@ -83,7 +93,7 @@ class @MilestoneSelect ...@@ -83,7 +93,7 @@ class @MilestoneSelect
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
clicked: (selected) -> clicked: (selected) ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
...@@ -118,12 +128,13 @@ class @MilestoneSelect ...@@ -118,12 +128,13 @@ class @MilestoneSelect
$dropdown.trigger('loaded.gl.dropdown') $dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut() $loading.fadeOut()
$selectbox.hide() $selectbox.hide()
$value.removeAttr('style') $value.css('display', '')
if data.milestone? if data.milestone?
data.milestone.namespace = _this.currentProject.namespace data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path data.milestone.path = _this.currentProject.path
data.milestone.remaining = $.timefor data.milestone.due_date
$value.html(milestoneLinkTemplate(data.milestone)) $value.html(milestoneLinkTemplate(data.milestone))
$sidebarCollapsedValue.find('span').text(data.milestone.title) $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone))
else else
$value.html(milestoneLinkNoneTemplate) $value.html(milestoneLinkNoneTemplate)
$sidebarCollapsedValue.find('span').text('No') $sidebarCollapsedValue.find('span').text('No')
......
...@@ -162,13 +162,14 @@ class @Notes ...@@ -162,13 +162,14 @@ class @Notes
renderNote: (note) -> renderNote: (note) ->
unless note.valid unless note.valid
if note.award if note.award
flash = new Flash('You have already used this award emoji!', 'alert') flash = new Flash('You have already awarded this emoji!', 'alert')
flash.pinTo('.header-content') flash.pinTo('.header-content')
return return
if note.award if note.award
awardsHandler.addAwardToEmojiBar(note.note) votesBlock = $('.js-awards-block').eq 0
awardsHandler.scrollToAwards() gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
gl.awardsHandler.scrollToAwards()
# render note if it not present in loaded list # render note if it not present in loaded list
# or skip if rendered # or skip if rendered
...@@ -353,8 +354,7 @@ class @Notes ...@@ -353,8 +354,7 @@ class @Notes
Called in response to clicking the edit note link Called in response to clicking the edit note link
Replaces the note text with the note edit form 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 Adds a data attribute to the form with the original content of the note for cancellations
if the user cancels
### ###
showEditForm: (e, scrollTo, myLastNote) -> showEditForm: (e, scrollTo, myLastNote) ->
e.preventDefault() e.preventDefault()
...@@ -370,6 +370,8 @@ class @Notes ...@@ -370,6 +370,8 @@ class @Notes
done = ($noteText) -> done = ($noteText) ->
# Neat little trick to put the cursor at the end # Neat little trick to put the cursor at the end
noteTextVal = $noteText.val() 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); $noteText.val('').val(noteTextVal);
new GLForm form new GLForm form
...@@ -392,14 +394,16 @@ class @Notes ...@@ -392,14 +394,16 @@ class @Notes
### ###
Called in response to clicking the edit note link 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) -> cancelEdit: (e) ->
e.preventDefault() e.preventDefault()
note = $(this).closest(".note") note = $(this).closest(".note")
form = note.find(".current-note-edit-form")
note.removeClass "is-editting" note.removeClass "is-editting"
note.find(".current-note-edit-form") form.removeClass("current-note-edit-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. Called in response to deleting a note of any kind.
......
@Pager = @Pager =
init: (@limit = 0, preload, @disable = false) -> init: (@limit = 0, preload, @disable = false, @callback = $.noop) ->
@loading = $('.loading').first() @loading = $('.loading').first()
if preload if preload
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
@loading.hide() @loading.hide()
success: (data) -> success: (data) ->
Pager.append(data.count, data.html) Pager.append(data.count, data.html)
Pager.callback()
dataType: "json" dataType: "json"
append: (count, html) -> append: (count, html) ->
......
...@@ -7,12 +7,17 @@ class @ProjectNew ...@@ -7,12 +7,17 @@ class @ProjectNew
@toggleSettingsOnclick() @toggleSettingsOnclick()
toggleSettings: -> toggleSettings: =>
checked = $("#project_builds_enabled").prop("checked") @_showOrHide('#project_builds_enabled', '.builds-feature')
if checked @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature')
$('.builds-feature').show()
else
$('.builds-feature').hide()
toggleSettingsOnclick: -> 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 ...@@ -20,8 +20,7 @@ class @SearchAutocomplete
@dropdown = @wrap.find('.dropdown') @dropdown = @wrap.find('.dropdown')
@dropdownContent = @dropdown.find('.dropdown-content') @dropdownContent = @dropdown.find('.dropdown-content')
@locationBadgeEl = @getElement('.search-location-badge') @locationBadgeEl = @getElement('.location-badge')
@locationText = @getElement('.location-text')
@scopeInputEl = @getElement('#scope') @scopeInputEl = @getElement('#scope')
@searchInput = @getElement('.search-input') @searchInput = @getElement('.search-input')
@projectInputEl = @getElement('#search_project_id') @projectInputEl = @getElement('#search_project_id')
...@@ -133,7 +132,7 @@ class @SearchAutocomplete ...@@ -133,7 +132,7 @@ class @SearchAutocomplete
scope: @scopeInputEl.val() scope: @scopeInputEl.val()
# Location badge # Location badge
_location: @locationText.text() _location: @locationBadgeEl.text()
} }
bindEvents: -> bindEvents: ->
...@@ -143,23 +142,28 @@ class @SearchAutocomplete ...@@ -143,23 +142,28 @@ class @SearchAutocomplete
@searchInput.on 'click', @onSearchInputClick @searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus @searchInput.on 'focus', @onSearchInputFocus
@clearInput.on 'click', @onClearInputClick @clearInput.on 'click', @onClearInputClick
@locationBadgeEl.on 'click', =>
@searchInput.focus()
onDocumentClick: (e) => onDocumentClick: (e) =>
# If clicking outside the search box # If clicking outside the search box
# And search input is not focused # And search input is not focused
# And we are not clicking inside a suggestion # 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() @onSearchInputBlur()
enableAutocomplete: -> enableAutocomplete: ->
# No need to enable anything if user is not logged in # No need to enable anything if user is not logged in
return if !gon.current_user_id return if !gon.current_user_id
_this = @ unless @dropdown.hasClass('open')
@loadingSuggestions = false _this = @
@loadingSuggestions = false
@dropdown.addClass('open') @dropdown
@searchInput.removeClass('disabled') .addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled')
onSearchInputKeyDown: => onSearchInputKeyDown: =>
# Saves last length of the entered text # Saves last length of the entered text
...@@ -190,7 +194,7 @@ class @SearchAutocomplete ...@@ -190,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete() @disableAutocomplete()
else else
# We should display the menu only when input is not empty # We should display the menu only when input is not empty
@enableAutocomplete() @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value @wrap.toggleClass 'has-value', !!e.target.value
...@@ -221,10 +225,8 @@ class @SearchAutocomplete ...@@ -221,10 +225,8 @@ class @SearchAutocomplete
category = if item.category? then "#{item.category}: " else '' category = if item.category? then "#{item.category}: " else ''
value = if item.value? then item.value else '' value = if item.value? then item.value else ''
html = "<span class='location-badge'> badgeText = "#{category}#{value}"
<i class='location-text'>#{category}#{value}</i> @locationBadgeEl.text(badgeText).show()
</span>"
@locationBadgeEl.html(html)
@wrap.addClass('has-location-badge') @wrap.addClass('has-location-badge')
restoreOriginalState: -> restoreOriginalState: ->
...@@ -233,9 +235,8 @@ class @SearchAutocomplete ...@@ -233,9 +235,8 @@ class @SearchAutocomplete
for input in inputs for input in inputs
@getElement("##{input}").val(@originalState[input]) @getElement("##{input}").val(@originalState[input])
if @originalState._location is '' if @originalState._location is ''
@locationBadgeEl.empty() @locationBadgeEl.hide()
else else
@addLocationBadge( @addLocationBadge(
value: @originalState._location value: @originalState._location
...@@ -244,7 +245,7 @@ class @SearchAutocomplete ...@@ -244,7 +245,7 @@ class @SearchAutocomplete
@dropdown.removeClass 'open' @dropdown.removeClass 'open'
badgePresent: -> badgePresent: ->
@locationBadgeEl.children().length @locationBadgeEl.length
resetSearchState: -> resetSearchState: ->
inputs = Object.keys @originalState inputs = Object.keys @originalState
...@@ -257,7 +258,7 @@ class @SearchAutocomplete ...@@ -257,7 +258,7 @@ class @SearchAutocomplete
@getElement("##{input}").val('') @getElement("##{input}").val('')
removeLocationBadge: -> removeLocationBadge: ->
@locationBadgeEl.empty() @locationBadgeEl.hide()
# Reset state # Reset state
@resetSearchState() @resetSearchState()
......
...@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation ...@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@replyWithSelectedText() @replyWithSelectedText()
return false return false
) )
Mousetrap.bind('j', =>
@prevIssue()
return false
)
Mousetrap.bind('k', =>
@nextIssue()
return false
)
Mousetrap.bind('e', => Mousetrap.bind('e', =>
@editIssue() @editIssue()
return false return false
...@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation ...@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
else else
@enabledHelp.push('.hidden-shortcut.issues') @enabledHelp.push('.hidden-shortcut.issues')
prevIssue: ->
$prevBtn = $('.prev-btn')
if not $prevBtn.hasClass('disabled')
Turbolinks.visit($prevBtn.attr('href'))
nextIssue: ->
$nextBtn = $('.next-btn')
if not $nextBtn.hasClass('disabled')
Turbolinks.visit($nextBtn.attr('href'))
replyWithSelectedText: -> replyWithSelectedText: ->
if window.getSelection if window.getSelection
selected = window.getSelection().toString() selected = window.getSelection().toString()
......
...@@ -19,3 +19,8 @@ class @Subscription ...@@ -19,3 +19,8 @@ class @Subscription
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
btn.find('span').text(action) btn.find('span').text(action)
@subscription_status.find('>div').toggleClass('hidden') @subscription_status.find('>div').toggleClass('hidden')
if btn.attr('data-original-title')
btn.tooltip('hide')
.attr('data-original-title', action)
.tooltip('fixTitle')
# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> authenticated -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FAuthenticate
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@challenges = u2fParams.challenges
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
authenticate: () =>
u2f.sign(@appId, @challenges, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderAuthenticated(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
"inProgress": '#js-authenticate-u2f-in-progress',
"error": '#js-authenticate-u2f-error',
"authenticated": '#js-authenticate-u2f-authenticated'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-login-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@authenticate()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderAuthenticated: (deviceResponse) =>
@renderTemplate('authenticated')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
class @U2FError
constructor: (@errorCode) ->
@httpsDisabled = (window.location.protocol isnt 'https:')
console.error("U2F Error Code: #{@errorCode}")
message: () =>
switch
when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
"U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
"This device has already been registered with us."
else
"There was a problem communicating with your device."
# Register U2F (universal 2nd factor) devices for users to authenticate with.
#
# State Flow #1: setup -> in_progress -> registered -> POST to server
# State Flow #2: setup -> in_progress -> error -> setup
class @U2FRegister
constructor: (@container, u2fParams) ->
@appId = u2fParams.app_id
@registerRequests = u2fParams.register_requests
@signRequests = u2fParams.sign_requests
start: () =>
if U2FUtil.isU2FSupported()
@renderSetup()
else
@renderNotSupported()
register: () =>
u2f.register(@appId, @registerRequests, @signRequests, (response) =>
if response.errorCode
error = new U2FError(response.errorCode)
@renderError(error);
else
@renderRegistered(JSON.stringify(response))
, 10)
#############
# Rendering #
#############
templates: {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
"inProgress": '#js-register-u2f-in-progress',
"error": '#js-register-u2f-error',
"registered": '#js-register-u2f-registered'
}
renderTemplate: (name, params) =>
templateString = $(@templates[name]).html()
template = _.template(templateString)
@container.html(template(params))
renderSetup: () =>
@renderTemplate('setup')
@container.find('#js-setup-u2f-device').on('click', @renderInProgress)
renderInProgress: () =>
@renderTemplate('inProgress')
@register()
renderError: (error) =>
@renderTemplate('error', {error_message: error.message()})
@container.find('#js-u2f-try-again').on('click', @renderSetup)
renderRegistered: (deviceResponse) =>
@renderTemplate('registered')
# Prefer to do this instead of interpolating using Underscore templates
# because of JSON escaping issues.
@container.find("#js-device-response").val(deviceResponse)
renderNotSupported: () =>
@renderTemplate('notSupported')
# Helper class for U2F (universal 2nd factor) device registration and authentication.
class @U2FUtil
@isU2FSupported: ->
if @testMode
true
else
gon.u2f.browser_supports_u2f
@enableTestMode: ->
@testMode = true
<% if Rails.env.test? %>
U2FUtil.enableTestMode();
<% end %>
...@@ -122,6 +122,9 @@ class @UserTabs ...@@ -122,6 +122,9 @@ class @UserTabs
@parentEl.find(tabSelector).html(data.html) @parentEl.find(tabSelector).html(data.html)
@loaded[action] = true @loaded[action] = true
# Fix tooltips
gl.utils.localTimeAgo($('.js-timeago', tabSelector))
loadActivities: (source) -> loadActivities: (source) ->
return if @loaded['activity'] is true return if @loaded['activity'] is true
......
...@@ -95,7 +95,7 @@ class @UsersSelect ...@@ -95,7 +95,7 @@ class @UsersSelect
data: (term, callback) => data: (term, callback) =>
isAuthorFilter = $('.js-author-search') isAuthorFilter = $('.js-author-search')
@users term, term is '' and isAuthorFilter, (users) => @users term, (users) =>
if term.length is 0 if term.length is 0
showDivider = 0 showDivider = 0
...@@ -149,7 +149,7 @@ class @UsersSelect ...@@ -149,7 +149,7 @@ class @UsersSelect
hidden: (e) -> hidden: (e) ->
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.css('display', '')
clicked: (user) -> clicked: (user) ->
page = $('body').data 'page' page = $('body').data 'page'
...@@ -221,7 +221,7 @@ class @UsersSelect ...@@ -221,7 +221,7 @@ class @UsersSelect
multiple: $(select).hasClass('multiselect') multiple: $(select).hasClass('multiselect')
minimumInputLength: 0 minimumInputLength: 0
query: (query) => query: (query) =>
@users query.term, @projectId?, (users) => @users query.term, (users) =>
data = { results: users } data = { results: users }
if query.term.length == 0 if query.term.length == 0
...@@ -304,7 +304,7 @@ class @UsersSelect ...@@ -304,7 +304,7 @@ class @UsersSelect
# Return users list. Filtered by query # Return users list. Filtered by query
# Only active users retrieved # Only active users retrieved
users: (query, fromProject, callback) => users: (query, callback) =>
url = @buildUrl(@usersPath) url = @buildUrl(@usersPath)
$.ajax( $.ajax(
...@@ -313,7 +313,7 @@ class @UsersSelect ...@@ -313,7 +313,7 @@ class @UsersSelect
search: query search: query
per_page: 20 per_page: 20
active: true active: true
project_id: @projectId if fromProject project_id: @projectId
group_id: @groupId group_id: @groupId
current_user: @showCurrentUser current_user: @showCurrentUser
author_id: @authorId author_id: @authorId
......
...@@ -61,6 +61,11 @@ ...@@ -61,6 +61,11 @@
margin-bottom: -$gl-padding; margin-bottom: -$gl-padding;
} }
&.content-component-block {
padding: 11px 0;
background-color: $white-light;
}
.title { .title {
color: $gl-text-color; color: $gl-text-color;
} }
......
...@@ -79,6 +79,23 @@ ...@@ -79,6 +79,23 @@
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); @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 { .btn {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
...@@ -142,15 +159,9 @@ ...@@ -142,15 +159,9 @@
} }
&.btn-grouped { &.btn-grouped {
margin-right: 7px; @include btn-with-margin;
float: left;
&:last-child {
margin-right: 0;
}
&.btn-xs {
margin-right: 3px;
}
} }
&.disabled { &.disabled {
pointer-events: auto !important; pointer-events: auto !important;
} }
...@@ -192,11 +203,7 @@ ...@@ -192,11 +203,7 @@
.btn-group { .btn-group {
&.btn-grouped { &.btn-grouped {
margin-right: 7px; @include btn-with-margin;
float: left;
&:last-child {
margin-right: 0;
}
} }
} }
......
...@@ -122,10 +122,9 @@ ...@@ -122,10 +122,9 @@
a { a {
display: block; display: block;
position: relative; position: relative;
padding-left: 10px; padding: 5px 10px;
padding-right: 10px;
color: $dropdown-link-color; color: $dropdown-link-color;
line-height: 34px; line-height: initial;
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: 2px; border-radius: 2px;
white-space: nowrap; white-space: nowrap;
...@@ -162,6 +161,16 @@ ...@@ -162,6 +161,16 @@
} }
} }
.dropdown-menu-large {
width: 340px;
}
.dropdown-menu-no-wrap {
a {
white-space: normal;
}
}
.dropdown-menu-full-width { .dropdown-menu-full-width {
width: 100%; width: 100%;
} }
...@@ -232,13 +241,11 @@ ...@@ -232,13 +241,11 @@
a { a {
padding-left: 25px; padding-left: 25px;
&.is-active { &.is-indeterminate, &.is-active {
&::before { &::before {
content: "\f00c";
position: absolute; position: absolute;
left: 5px; left: 5px;
top: 50%; top: 8px;
margin-top: -7px;
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
text-rendering: auto; text-rendering: auto;
...@@ -246,6 +253,14 @@ ...@@ -246,6 +253,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
} }
&.is-indeterminate::before {
content: "\f068";
}
&.is-active::before {
content: "\f00c";
}
} }
} }
...@@ -525,3 +540,14 @@ ...@@ -525,3 +540,14 @@
background-color: $calendar-unselectable-bg; 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 { ...@@ -76,6 +76,7 @@ label {
.form-control { .form-control {
@include box-shadow(none); @include box-shadow(none);
border-radius: 3px; border-radius: 3px;
padding: $gl-vert-padding $gl-input-padding;
} }
.select-wrapper { .select-wrapper {
......
...@@ -22,17 +22,17 @@ ...@@ -22,17 +22,17 @@
&:hover { &:hover {
background-color: $color-dark; background-color: $color-dark;
a { a {
color: #fff; color: $white-light;
h3 { h3 {
color: #fff; color: $white-light;
} }
} }
} }
} }
.collapse-nav a { .collapse-nav a {
color: #fff; color: $white-light;
background: $color; background: $color;
} }
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
&:hover { &:hover {
background-color: $color-dark; background-color: $color-dark;
color: #fff; color: $white-light;
text-decoration: none; text-decoration: none;
} }
} }
...@@ -63,10 +63,20 @@ ...@@ -63,10 +63,20 @@
color: $color-light; color: $color-light;
} }
path,
polygon {
fill: $color-light;
}
.count { .count {
color: $color-light; color: $color-light;
background: $color-dark; background: $color-dark;
} }
svg {
position: relative;
top: 3px;
}
} }
&.separate-item { &.separate-item {
...@@ -74,7 +84,7 @@ ...@@ -74,7 +84,7 @@
} }
&.active a { &.active a {
color: #fff; color: $white-light;
background: $color-dark; background: $color-dark;
&.no-highlight { &.no-highlight {
...@@ -82,15 +92,23 @@ ...@@ -82,15 +92,23 @@
} }
i { i {
color: #fff color: $white-light
}
path,
polygon {
fill: $white-light;
} }
} }
} }
} }
} }
$theme-blue: #2980b9;
$theme-charcoal: #3d454d; $theme-charcoal: #3d454d;
$theme-charcoal-dark: #383f45;
$theme-charcoal-text: #b9bbbe;
$theme-blue: #2980b9;
$theme-graphite: #666; $theme-graphite: #666;
$theme-gray: #373737; $theme-gray: #373737;
$theme-green: #019875; $theme-green: #019875;
...@@ -102,7 +120,7 @@ body { ...@@ -102,7 +120,7 @@ body {
} }
&.ui_charcoal { &.ui_charcoal {
@include gitlab-theme(#d6d7d9, #485157, $theme-charcoal, #353b41); @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark);
} }
&.ui_graphite { &.ui_graphite {
......
...@@ -79,6 +79,10 @@ header { ...@@ -79,6 +79,10 @@ header {
&.header-collapsed { &.header-collapsed {
padding: 0 16px; padding: 0 16px;
.side-nav-toggle {
display: block;
}
} }
.side-nav-toggle { .side-nav-toggle {
...@@ -86,6 +90,7 @@ header { ...@@ -86,6 +90,7 @@ header {
position: absolute; position: absolute;
left: -10px; left: -10px;
margin: 6px 0; margin: 6px 0;
font-size: 18px;
padding: 6px 10px; padding: 6px 10px;
border: none; border: none;
background-color: $background-color; background-color: $background-color;
...@@ -97,10 +102,6 @@ header { ...@@ -97,10 +102,6 @@ header {
&:focus { &:focus {
outline: none; outline: none;
} }
@media (max-width: $screen-xs-min) {
display: block;
}
} }
} }
...@@ -171,31 +172,21 @@ header { ...@@ -171,31 +172,21 @@ header {
} }
} }
@mixin collapsed-header {
margin-left: $sidebar_collapsed_width;
}
.header-collapsed { .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) { .header-content {
margin-left: 0; padding-left: 30px;
transition-duration: .3s;
} }
} }
.header-expanded { .header-expanded {
margin-left: $sidebar_collapsed_width; margin-left: 0;
@media (min-width: $screen-md-min) { .header-content {
margin-left: $sidebar_width; margin-left: $sidebar_width;
} transition-duration: .3s;
@media (max-width: $screen-xs-min) {
margin-left: 0;
} }
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
font-family: $regular_font; font-family: $regular_font;
font-size: $font-size-base; font-size: $font-size-base;
&.ui-datepicker,
&.ui-datepicker-inline { &.ui-datepicker-inline {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
...@@ -10,6 +11,25 @@ ...@@ -10,6 +11,25 @@
.ui-datepicker-header { .ui-datepicker-header {
background: #fff; background: #fff;
border-color: #ddd; 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 { .ui-datepicker-calendar td a {
...@@ -36,21 +56,18 @@ ...@@ -36,21 +56,18 @@
} }
.ui-state-highlight { .ui-state-highlight {
border: 1px solid #eee; border: 0;
background: #eee; background: transparent;
} }
.ui-state-active { .ui-datepicker-calendar {
border: 1px solid $gl-primary; .ui-state-active,
background: $gl-primary; .ui-state-hover,
color: #fff; .ui-state-focus {
} border: 1px solid $gl-primary;
background: $gl-primary;
.ui-state-hover, color: #fff;
.ui-state-focus { }
border: 1px solid $row-hover;
background: $row-hover;
color: #333;
} }
} }
......
...@@ -137,10 +137,30 @@ ul.content-list { ...@@ -137,10 +137,30 @@ ul.content-list {
padding-top: 1px; padding-top: 1px;
float: right; float: right;
.btn { > .btn,
padding: 10px 14px; > .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 @@ ...@@ -2,18 +2,10 @@
* Generic mixins * Generic mixins
*/ */
@mixin box-shadow($shadow) { @mixin box-shadow($shadow) {
-webkit-box-shadow: $shadow;
-moz-box-shadow: $shadow;
-ms-box-shadow: $shadow;
-o-box-shadow: $shadow;
box-shadow: $shadow; box-shadow: $shadow;
} }
@mixin border-radius($radius) { @mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
-ms-border-radius: $radius;
-o-border-radius: $radius;
border-radius: $radius; border-radius: $radius;
} }
......
...@@ -66,10 +66,6 @@ ...@@ -66,10 +66,6 @@
display: none; display: none;
} }
%ul.notes .note-role, .note-actions {
display: none;
}
.nav-links, .nav-links { .nav-links, .nav-links {
li a { li a {
font-size: 14px; font-size: 14px;
......
...@@ -41,8 +41,7 @@ ...@@ -41,8 +41,7 @@
a { a {
display: inline-block; display: inline-block;
padding: 14px; padding: $gl-btn-padding;
padding-top: $gl-padding;
padding-bottom: 11px; padding-bottom: 11px;
margin-bottom: -1px; margin-bottom: -1px;
font-size: 15px; font-size: 15px;
...@@ -67,6 +66,27 @@ ...@@ -67,6 +66,27 @@
color: #78a; 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 { .top-area {
...@@ -81,6 +101,10 @@ ...@@ -81,6 +101,10 @@
width: 50%; width: 50%;
line-height: 28px; line-height: 28px;
&.wiki-page {
padding: 16px 10px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
width: 100%; width: 100%;
...@@ -104,6 +128,10 @@ ...@@ -104,6 +128,10 @@
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: none;
li a {
padding: 16px 10px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
width: 100%; width: 100%;
...@@ -143,6 +171,7 @@ ...@@ -143,6 +171,7 @@
> form { > form {
display: inline-block; display: inline-block;
margin-top: -1px; margin-top: -1px;
margin-bottom: 12px;
} }
.icon-label { .icon-label {
...@@ -179,7 +208,7 @@ ...@@ -179,7 +208,7 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding-bottom: 0; padding-bottom: 0;
width: 100%;
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control { .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px; margin: 0 0 10px;
display: block; display: block;
...@@ -210,16 +239,6 @@ ...@@ -210,16 +239,6 @@
margin: 0; 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 @@ ...@@ -276,6 +295,19 @@
border-bottom: none; border-bottom: none;
height: 51px; height: 51px;
svg {
position: relative;
top: 2px;
margin-right: 2px;
height: 15px;
width: auto;
path,
polygon {
fill: $layout-link-gray;
}
}
.fade-right { .fade-right {
@include fade(left, rgba(250, 250, 250, 0.4), $background-color); @include fade(left, rgba(250, 250, 250, 0.4), $background-color);
right: 0; right: 0;
...@@ -297,9 +329,17 @@ ...@@ -297,9 +329,17 @@
} }
&.active { &.active {
a, i { a, i {
color: $black; color: $black;
} }
svg {
path,
polygon {
fill: $black;
}
}
} }
.badge { .badge {
...@@ -309,8 +349,8 @@ ...@@ -309,8 +349,8 @@
} }
.nav-control { .nav-control {
.fade-right {
.fade-right {
@media (min-width: $screen-xs-max) { @media (min-width: $screen-xs-max) {
right: 67px; right: 67px;
} }
...@@ -321,6 +361,24 @@ ...@@ -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 { .nav-block {
position: relative; position: relative;
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
background: #fff; background: #fff;
border-color: $input-border; border-color: $input-border;
height: 35px; height: 35px;
padding: $gl-vert-padding $gl-btn-padding; padding: $gl-vert-padding $gl-input-padding;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 1.42857143; line-height: 1.42857143;
border-radius: $border-radius-base; border-radius: $border-radius-base;
......
#logo {
z-index: 2;
position: absolute;
width: 58px;
cursor: pointer;
margin-top: 8px;
}
.page-with-sidebar { .page-with-sidebar {
padding-top: $header-height; padding-top: $header-height;
transition-duration: .3s; transition-duration: .3s;
...@@ -20,12 +12,6 @@ ...@@ -20,12 +12,6 @@
height: 100%; height: 100%;
transition-duration: .3s; transition-duration: .3s;
} }
.gitlab-text-container-link {
z-index: 1;
position: absolute;
left: 0;
}
} }
.sidebar-wrapper { .sidebar-wrapper {
...@@ -50,55 +36,21 @@ ...@@ -50,55 +36,21 @@
.sidebar-wrapper { .sidebar-wrapper {
.header-logo { .header-logo {
border-bottom: 1px solid transparent;
float: left;
height: $header-height; height: $header-height;
padding: 8px 26px;
width: $sidebar_width; width: $sidebar_width;
position: fixed; position: fixed;
z-index: 999; z-index: 999;
overflow: hidden; overflow: hidden;
transition-duration: .3s; 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 { &:hover {
background-color: #eee; background-color: #eee;
} }
} }
.sidebar-user { .sidebar-user {
padding: 7px 22px; padding: 15px 22px;
position: fixed; position: fixed;
bottom: 40px; bottom: 40px;
width: $sidebar_width; width: $sidebar_width;
...@@ -126,8 +78,8 @@ ...@@ -126,8 +78,8 @@
.nav-sidebar { .nav-sidebar {
margin-top: 14 + $header-height; margin-top: 22 + $header-height;
margin-bottom: 100px; margin-bottom: 116px;
transition-duration: .3s; transition-duration: .3s;
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
...@@ -145,13 +97,12 @@ ...@@ -145,13 +97,12 @@
} }
a { a {
padding: 7px 15px; text-align: center;
padding: 8px;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 24px;
color: $gray; color: $gray;
display: block; display: block;
text-decoration: none; text-decoration: none;
padding-left: 23px;
font-weight: normal; font-weight: normal;
outline: none; outline: none;
...@@ -164,16 +115,13 @@ ...@@ -164,16 +115,13 @@
} }
i { i {
width: 16px; font-size: 16px;
color: $gray-light;
margin-right: 13px;
} }
.count { .nav-link-text {
float: right; margin-top: 3px;
background: #eee; font-size: 13px;
padding: 0 8px; line-height: 18px;
@include border-radius(6px);
} }
&.back-link i { &.back-link i {
...@@ -217,25 +165,14 @@ ...@@ -217,25 +165,14 @@
} }
.page-sidebar-collapsed { .page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width; padding-left: 0;
@media (max-width: $screen-xs-min) {
padding-left: 0;
}
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_collapsed_width; width: 0;
@media (max-width: $screen-xs-min) {
width: 0;
}
.header-logo { .header-logo {
width: $sidebar_collapsed_width; width: 0;
padding: 8px 0;
@media (max-width: $screen-xs-min) {
width: 0;
}
a { a {
padding-left: ($sidebar_collapsed_width - 36) / 2; padding-left: ($sidebar_collapsed_width - 36) / 2;
...@@ -246,6 +183,10 @@ ...@@ -246,6 +183,10 @@
} }
} }
#logo {
display: none;
}
.nav-sidebar { .nav-sidebar {
width: $sidebar_collapsed_width; width: $sidebar_collapsed_width;
...@@ -261,44 +202,23 @@ ...@@ -261,44 +202,23 @@
} }
.collapse-nav a { .collapse-nav a {
width: $sidebar_collapsed_width; width: 0;
@media (max-width: $screen-xs-min) {
width: 0;
}
} }
.sidebar-user { .sidebar-user {
padding-left: ($sidebar_collapsed_width - 36) / 2; width: 0;
width: $sidebar_collapsed_width; padding-left: 0;
padding-right: 0;
@media (max-width: $screen-xs-min) {
width: 0;
padding-left: 0;
padding-right: 0;
}
.username { .username {
display: none; display: none;
} }
} }
} }
.layout-nav {
padding-right: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
padding-right: 0;;
}
}
} }
.page-sidebar-expanded { .page-sidebar-expanded {
padding-left: $sidebar_collapsed_width; padding-left: $sidebar_width;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
@media (max-width: $screen-xs-min) { @media (max-width: $screen-xs-min) {
padding-left: 0; padding-left: 0;
...@@ -328,7 +248,7 @@ ...@@ -328,7 +248,7 @@
} }
@media (min-width: $screen-xs-min) and (max-width: $screen-md-min) { @media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
padding-right: 62px; padding-right: 90px;
} }
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
...@@ -353,7 +273,9 @@ ...@@ -353,7 +273,9 @@
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @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) { @media (min-width: $screen-md-min) {
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding; padding: $gl-padding $gl-btn-padding 11px;
border-color: $table-border-color; border-color: $table-border-color;
color: $gl-gray; color: $gl-gray;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
......
...@@ -192,3 +192,8 @@ ...@@ -192,3 +192,8 @@
.text-info:hover { .text-info:hover {
color: $brand-info; color: $brand-info;
} }
// Prevent datetimes on tooltips to break into two lines
.local-timeago {
white-space: nowrap;
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* Layout * Layout
*/ */
$sidebar_collapsed_width: 62px; $sidebar_collapsed_width: 62px;
$sidebar_width: 220px; $sidebar_width: 90px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 258px; $gutter_inner_width: 258px;
...@@ -57,6 +57,7 @@ $code_line_height: 1.5; ...@@ -57,6 +57,7 @@ $code_line_height: 1.5;
*/ */
$gl-padding: 16px; $gl-padding: 16px;
$gl-btn-padding: 10px; $gl-btn-padding: 10px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top: 10px; $gl-padding-top: 10px;
...@@ -79,6 +80,9 @@ $provider-btn-not-active-color: #4688f1; ...@@ -79,6 +80,9 @@ $provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee; $link-underline-blue: #4a8bee;
$layout-link-gray: #7e7c7c; $layout-link-gray: #7e7c7c;
$todo-alert-blue: #428bca; $todo-alert-blue: #428bca;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
/* /*
* Color schema * Color schema
...@@ -121,7 +125,7 @@ $border-white-normal: #d6dae2; ...@@ -121,7 +125,7 @@ $border-white-normal: #d6dae2;
$border-white-dark: #c6cacf; $border-white-dark: #c6cacf;
$border-gray-light: #dcdcdc; $border-gray-light: #dcdcdc;
$border-gray-normal: rgba(0, 0, 0, 0.10); $border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf; $border-gray-dark: #c6cacf;
$border-green-light: #2faa60; $border-green-light: #2faa60;
...@@ -256,3 +260,6 @@ $calendar-header-color: #b8b8b8; ...@@ -256,3 +260,6 @@ $calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe; $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1); $calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9; $calendar-unselectable-bg: #faf9f9;
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
@import "framework/variables"; @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 { table.code {
width: 100%; width: 100%;
font-family: monospace; font-family: monospace;
...@@ -11,33 +21,162 @@ table.code { ...@@ -11,33 +21,162 @@ table.code {
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
-premailer-width: 100%; -premailer-width: 100%;
td { > tr > td {
line-height: $code_line_height; line-height: $code_line_height;
font-family: monospace; font-family: monospace;
font-size: $code_font_size; font-size: $code_font_size;
&.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;
}
&.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
border: none;
white-space: pre;
}
} }
}
.line-numbers, .diff-line-num {
background-color: $background-color;
}
.diff-line-num, .diff-line-num a {
color: $black-transparent;
}
td.diff-line-num { pre.code, .diff-line-num {
margin: 0; border-color: $table-border-gray;
padding: 0; }
border: none;
background: $background-color; .code.white, pre.code, .line_content {
color: rgba(0, 0, 0, 0.3); background-color: #fff;
padding: 0 5px; color: #333;
border-right: 1px solid $border-color; }
text-align: right;
min-width: 35px; .diff-line-num {
max-width: 50px; &.old {
width: 35px; background-color: $line-number-old;
border-color: $line-removed-dark;
} }
td.line_content { &.new {
display: block; background-color: $line-number-new;
margin: 0; border-color: $line-added-dark;
padding: 0 0.5em;
border: none;
white-space: pre;
} }
&.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;
} }
@import "highlight/white"; .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 { ...@@ -6,19 +6,19 @@ p.details {
font-style: italic; font-style: italic;
color: #777 color: #777
} }
.footer p { .footer > p {
font-size: small; font-size: small;
color: #777 color: #777
} }
pre.commit-message { pre.commit-message {
white-space: pre-wrap; white-space: pre-wrap;
} }
.file-stats a { .file-stats > a {
text-decoration: none; text-decoration: none;
} > .new-file {
.file-stats .new-file { color: #090;
color: #090; }
} > .deleted-file {
.file-stats .deleted-file { color: #b00;
color: #b00; }
} }
.awards { .awards {
line-height: 34px;
.emoji-icon { .emoji-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
...@@ -9,8 +7,6 @@ ...@@ -9,8 +7,6 @@
.emoji-menu { .emoji-menu {
position: absolute; position: absolute;
top: 100%;
left: 0;
margin-top: 3px; margin-top: 3px;
z-index: 1000; z-index: 1000;
min-width: 160px; min-width: 160px;
...@@ -23,7 +19,12 @@ ...@@ -23,7 +19,12 @@
opacity: 0; opacity: 0;
transform: scale(.2); transform: scale(.2);
transform-origin: 0 -45px; transform-origin: 0 -45px;
transition: all .3s cubic-bezier(.87,-.41,.19,1.44); transition: .3s cubic-bezier(.87,-.41,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
transform-origin: 100% -45px;
}
&.is-visible { &.is-visible {
pointer-events: all; pointer-events: all;
...@@ -94,20 +95,30 @@ ...@@ -94,20 +95,30 @@
.award-control { .award-control {
margin-right: 5px; margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
line-height: 20px; line-height: 20px;
outline: 0; outline: 0;
&:hover,
&.active, &.active,
&:active { &:active {
background-color: $white-dark; background-color: $row-hover;
border-color: $row-hover-border;
box-shadow: none; box-shadow: none;
outline: 0; outline: 0;
} }
&.btn {
&:focus {
outline: 0;
}
}
&.is-loading { &.is-loading {
.award-control-icon { .award-control-icon-normal,
.emoji-icon {
display: none; display: none;
} }
......
...@@ -3,12 +3,7 @@ ...@@ -3,12 +3,7 @@
background: #111; background: #111;
color: #fff; color: #fff;
font-family: $monospace_font; font-family: $monospace_font;
white-space: pre; white-space: pre-wrap;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
overflow: auto; overflow: auto;
overflow-y: hidden; overflow-y: hidden;
font-size: 12px; font-size: 12px;
...@@ -58,37 +53,92 @@ ...@@ -58,37 +53,92 @@
left: 70px; left: 70px;
} }
} }
}
.build-widget { .build-header {
padding: 10px; position: relative;
background: $background-color; padding-right: 40px;
margin-bottom: 20px;
border-radius: 4px;
.title { @media (min-width: $screen-sm-min) {
margin-top: 0; padding-right: 0;
color: #666;
line-height: 1.5;
}
.attr-name {
color: #777;
}
} }
.alert-disabled { a {
background: $background-color; color: $gl-gray;
a { &:hover {
color: #3084bb !important; color: $gl-link-color;
text-decoration: none;
} }
} }
code {
color: $code-color;
}
.avatar {
float: none;
margin-right: 2px;
margin-left: 2px;
}
} }
table.builds { table.builds {
.build-link { .build-link {
a { a {
color: $gl-dark-link-color; 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 @@ ...@@ -2,13 +2,21 @@
margin-bottom: 20px; margin-bottom: 20px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
> h1 { > h1, h2, h3, h4, h5, h6 {
font-weight: 400; font-weight: 400;
} }
.lead { .lead {
margin-bottom: 20px; margin-bottom: 20px;
} }
ul, ol {
padding-left: 0;
}
li {
list-style-type: none;
}
} }
.confirmation-content { .confirmation-content {
......
...@@ -29,8 +29,6 @@ ...@@ -29,8 +29,6 @@
margin-top: 6px; margin-top: 6px;
p { p {
overflow-x: auto;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
} }
} }
.issuable-sidebar { .right-sidebar {
a { a {
color: inherit; color: inherit;
} }
...@@ -74,6 +74,10 @@ ...@@ -74,6 +74,10 @@
} }
} }
.block-first {
padding-top: 0;
}
.title { .title {
color: $gl-text-color; color: $gl-text-color;
margin-bottom: 10px; margin-bottom: 10px;
......
...@@ -50,11 +50,26 @@ ...@@ -50,11 +50,26 @@
.label-row { .label-row {
.label-name { .label-name {
display: inline-block; display: block;
width: 200px; margin-bottom: 10px;
@media (max-width: $screen-xs-min) { @media (min-width: $screen-sm-min) {
display: block; 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 @@ ...@@ -68,10 +83,6 @@
padding: 3px 4px; padding: 3px 4px;
} }
.label-subscription {
display: inline-block;
}
.dropdown-labels-error { .dropdown-labels-error {
padding: 5px 10px; padding: 5px 10px;
margin-bottom: 10px; margin-bottom: 10px;
...@@ -79,62 +90,95 @@ ...@@ -79,62 +90,95 @@
color: $white-light; color: $white-light;
} }
@mixin labels-mobile { .manage-labels-list {
@media (max-width: $screen-xs-min) { .btn-action {
display: block; color: $gl-dark-link-color;
width: 100%;
margin-left: 0;
padding: 10px 0;
}
}
.fa {
font-size: 18px;
vertical-align: middle;
}
.manage-labels-list { &:hover {
color: $gl-link-color;
.prepend-left-10, .prepend-description-left { &.remove-row {
display: inline-block; color: $gl-danger;
width: 40%; }
vertical-align: middle; }
}
@include labels-mobile; .dropdown {
@media (min-width: $screen-sm-min) {
float: right;
}
} }
}
.prepend-description-left { .prioritized-labels {
width: 57%; margin-bottom: 30px;
@include labels-mobile; .add-priority {
display: none;
color: $gray-light;
} }
}
.pull-info-right { .other-labels {
float: right; .remove-priority {
display: none;
}
}
@media (max-width: $screen-xs-min) { .toggle-priority {
float: none; display: inline-block;
} vertical-align: middle;
.action-buttons { button {
border-color: transparent; border-color: transparent;
padding: 6px; padding: 5px 8px;
color: $gl-text-color; vertical-align: top;
font-size: 14px;
&.label-subscribe-button { &:hover {
padding-left: 0; border-color: transparent;
}
} }
}
}
i { .filtered-labels {
color: $gl-text-color; .label-row {
&:not(:last-child) {
margin-right: 5px;
} }
}
.append-right-20 { .label-remove {
a { border-left: 1px solid rgba(0, 0, 0, .1);
color: $gl-text-color; z-index: 3;
} }
@media (max-width: $screen-xs-min) { .btn {
display: block; color: inherit;
margin-bottom: 10px; }
} }
.label-options-toggle {
width: 100%;
}
.label-subscribe-button {
.label-subscribe-button-loading {
display: none;
}
&.disabled {
.label-subscribe-button-icon {
display: none;
}
.label-subscribe-button-loading {
display: block;
} }
} }
} }
...@@ -79,11 +79,14 @@ ...@@ -79,11 +79,14 @@
} }
&.ci-failed, &.ci-failed,
&.ci-canceled,
&.ci-error { &.ci-error {
color: $gl-danger; color: $gl-danger;
} }
&.ci-canceled {
color: $gl-gray;
}
a.monospace { a.monospace {
color: inherit; color: inherit;
} }
...@@ -105,6 +108,11 @@ ...@@ -105,6 +108,11 @@
font-size: 17px; font-size: 17px;
margin: 5px 0; margin: 5px 0;
color: $gl-gray-dark; color: $gl-gray-dark;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
}
} }
p:last-child { p:last-child {
......
...@@ -87,6 +87,39 @@ ...@@ -87,6 +87,39 @@
} }
} }
.md-header .nav-links {
display: flex;
display: -webkit-flex;
flex-flow: row wrap;
-webkit-flex-flow: row wrap;
width: 100%;
.pull-right {
// Flexbox quirk to make sure right-aligned items stay right-aligned.
margin-left: auto;
}
}
.confidential-issue-warning {
background-color: $gray-normal;
border-radius: 3px;
padding: 3px 12px;
margin: auto;
margin-top: 0;
text-align: center;
font-size: 13px;
@media (max-width: $screen-md-min) {
// On smaller devices the warning becomes the fourth item in the list,
// rather than centering, and grows to span the full width of the
// comment area.
order: 4;
-webkit-order: 4;
margin: 6px auto;
width: 100%;
}
}
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
background-color: $white-light; background-color: $white-light;
...@@ -96,17 +129,8 @@ ...@@ -96,17 +129,8 @@
display: none; display: none;
font-size: 15px; font-size: 15px;
.form-actions { .md-area {
padding-left: 20px; background-color: #fff;
.btn-save {
float: left;
}
.note-form-option {
float: left;
padding: 2px 0 0 25px;
}
} }
} }
......
...@@ -69,6 +69,10 @@ ul.notes { ...@@ -69,6 +69,10 @@ ul.notes {
.note-edit-form { .note-edit-form {
display: block; display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
} }
} }
...@@ -116,8 +120,41 @@ ul.notes { ...@@ -116,8 +120,41 @@ ul.notes {
} }
} }
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
.award-control {
font-size: 13px;
padding: 2px 5px;
}
}
.note-header { .note-header {
padding-bottom: 3px; padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
} }
} }
...@@ -179,6 +216,8 @@ ul.notes { ...@@ -179,6 +216,8 @@ ul.notes {
.discussion-header, .discussion-header,
.note-header { .note-header {
position: relative;
a { a {
color: inherit; color: inherit;
...@@ -215,6 +254,16 @@ ul.notes { ...@@ -215,6 +254,16 @@ ul.notes {
color: $notes-action-color; color: $notes-action-color;
} }
.note-actions {
position: absolute;
right: 0;
top: 0;
@media (min-width: $screen-sm-min) {
position: relative;
}
}
.discussion-actions { .discussion-actions {
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
float: none; float: none;
...@@ -228,8 +277,13 @@ ul.notes { ...@@ -228,8 +277,13 @@ ul.notes {
.note-action-button { .note-action-button {
display: inline-block; display: inline-block;
margin-left: 10px; margin-left: 0;
line-height: 24px; line-height: 20px;
@media (min-width: $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
.fa { .fa {
color: $notes-action-color; color: $notes-action-color;
......
...@@ -32,6 +32,15 @@ ...@@ -32,6 +32,15 @@
.container-fluid { .container-fluid {
position: relative; 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 { .cover-controls {
...@@ -57,7 +66,6 @@ ...@@ -57,7 +66,6 @@
max-width: 86px; max-width: 86px;
min-width: 86px; min-width: 86px;
padding-right: 0; padding-right: 0;
margin: 11px 0;
@media (max-width: $screen-md-max) { @media (max-width: $screen-md-max) {
padding-left: 0; padding-left: 0;
...@@ -489,9 +497,11 @@ pre.light-well { ...@@ -489,9 +497,11 @@ pre.light-well {
margin: 0; margin: 0;
} }
.project-show-activity {
.activity-filter-block { .activity-filter-block {
margin-top: -1px; .controls {
padding-bottom: 10px;
border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
} }
.search-input { .search-input {
padding-right: 20px;
border: none; border: none;
font-size: 14px; font-size: 14px;
outline: none; outline: none;
...@@ -47,6 +48,7 @@ ...@@ -47,6 +48,7 @@
display: inline-block; display: inline-block;
background-color: $location-badge-bg; background-color: $location-badge-bg;
vertical-align: top; vertical-align: top;
cursor: default;
} }
.search-input-container { .search-input-container {
...@@ -55,7 +57,7 @@ ...@@ -55,7 +57,7 @@
position: relative; position: relative;
} }
.search-location-badge, .search-input-wrap { .search-input-wrap {
// Fallback if flexbox is not supported // Fallback if flexbox is not supported
display: inline-block; display: inline-block;
} }
...@@ -156,13 +158,11 @@ ...@@ -156,13 +158,11 @@
.search-holder { .search-holder {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox;
display: flex; display: flex;
} }
.search-field-holder { .search-field-holder {
-webkit-flex: 1 0 auto; -webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto; flex: 1 0 auto;
position: relative; position: relative;
margin-right: 0; margin-right: 0;
......
...@@ -11,18 +11,15 @@ ...@@ -11,18 +11,15 @@
$magenta: #cd00cd; $magenta: #cd00cd;
$cyan: #00cdcd; $cyan: #00cdcd;
$white: #e5e5e5; $white: #e5e5e5;
$l-black: #7f7f7f; $l-black: #373b41;
$l-red: #f00; $l-red: #c66;
$l-green: #0f0; $l-green: #b5bd68;
$l-yellow: #ff0; $l-yellow: #f0c674;
$l-blue: #5c5cff; $l-blue: #81a2be;
$l-magenta: #f0f; $l-magenta: #b294bb;
$l-cyan: #0ff; $l-cyan: #8abeb7;
$l-white: #fff; $l-white: $ci-text-color;
.term-bold {
font-weight: bold;
}
.term-italic { .term-italic {
font-style: italic; font-style: italic;
} }
......
...@@ -74,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -74,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:two_factor_grace_period, :two_factor_grace_period,
:gravatar_enabled, :gravatar_enabled,
:sign_in_text, :sign_in_text,
:after_sign_up_text,
:help_page_text, :help_page_text,
:home_page_url, :home_page_url,
:after_sign_out_path, :after_sign_out_path,
......
...@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base ...@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper include Gitlab::GonHelper
include GitlabRoutingHelper include GitlabRoutingHelper
include PageLayoutHelper include PageLayoutHelper
include WorkhorseHelper
before_action :authenticate_user_from_token! before_action :authenticate_user_from_token!
before_action :authenticate_user! before_action :authenticate_user!
...@@ -182,8 +183,8 @@ class ApplicationController < ActionController::Base ...@@ -182,8 +183,8 @@ class ApplicationController < ActionController::Base
end end
def check_2fa_requirement def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to new_profile_two_factor_auth_path redirect_to profile_two_factor_auth_path
end end
end end
...@@ -342,6 +343,10 @@ class ApplicationController < ActionController::Base ...@@ -342,6 +343,10 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current session[:skip_tfa] && session[:skip_tfa] > Time.current
end end
def browser_supports_u2f?
browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
end
def redirect_to_home_page_url? def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page # If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections # Don't redirect to the default URL to prevent endless redirections
...@@ -355,6 +360,13 @@ class ApplicationController < ActionController::Base ...@@ -355,6 +360,13 @@ class ApplicationController < ActionController::Base
current_user.nil? && root_path == request.path current_user.nil? && root_path == request.path
end end
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
def u2f_app_id
request.base_url
end
private private
def set_default_sort def set_default_sort
......
...@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor ...@@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
private
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
end
end
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
# Remove any lingering user data from login
session.delete(:otp_user_id)
session.delete(:challenges)
sign_in(user)
else
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
render 'devise/sessions/two_factor' and return if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
challenges = sign_requests.map(&:challenge)
session[:challenges] = challenges
gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
end end
module ToggleAwardEmoji
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, only: [:toggle_award_emoji]
end
def toggle_award_emoji
name = params.require(:name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
render json: { ok: true }
end
private
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
else
awardable
end
end
def awardable
raise NotImplementedError
end
end
...@@ -42,46 +42,8 @@ class JwtController < ApplicationController ...@@ -42,46 +42,8 @@ class JwtController < ApplicationController
end end
def authenticate_user(login, password) def authenticate_user(login, password)
# TODO: this is a copy and paste from grack_auth, user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
# it should be refactored in the future Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
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 user
end end
end end
...@@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
def verify_user_oauth_applications_enabled def verify_user_oauth_applications_enabled
return if current_application_settings.user_oauth_applications? return if current_application_settings.user_oauth_applications?
redirect_to applications_profile_url redirect_to profile_path
end end
def set_index_vars def set_index_vars
......
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement skip_before_action :check_2fa_requirement
def new def show
unless current_user.otp_secret unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32) current_user.otp_secret = User.generate_otp_secret(32)
end end
...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed? current_user.save! if current_user.changed?
if two_factor_authentication_required? if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired? if two_factor_grace_period_expired?
flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.'
else else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end end
end end
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
end end
def create def create
if current_user.validate_and_consume_otp!(params[:pin_code]) if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes! @codes = current_user.generate_otp_backup_codes!
current_user.save! current_user.save!
...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else else
@error = 'Invalid pin code' @error = 'Invalid pin code'
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
render 'show'
end
end
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
render 'new' if @u2f_registration.persisted?
session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
render :show
end end
end end
...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host def issuer_host
Gitlab.config.gitlab.host Gitlab.config.gitlab.host
end end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles)
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
...@@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController ...@@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
private private
def build def build
@build ||= project.builds.unscoped.find_by!(id: params[:build_id]) @build ||= project.builds.find_by!(id: params[:build_id])
end end
def artifacts_file def artifacts_file
......
...@@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController
redirect_to namespace_project_branches_path(@project.namespace, redirect_to namespace_project_branches_path(@project.namespace,
@project), status: 303 @project), status: 303
end end
format.js { render status: status[:return_code] } format.js { render nothing: true, status: status[:return_code] }
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment