Commit ab9a8643 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into fix/status-of-pipeline-without-builds

* master: (538 commits)
  Fix broken URI joining for `teamcity_url` with suffixes
  Factorize duplicated code into a method in BambooService and update specs
  Fix broken URI joining for `bamboo_url` with suffixes
  Honor credentials on calling Bamboo CI trigger
  Update CHANGELOG
  Use Issue.visible_to_user in Notes.search to avoid query duplication
  Project members with guest role can't access confidential issues
  Allow users to create confidential issues in private projects
  Update CHANGELOG
  Remove deprecated issues_tracker and issues_tracker_id from project
  Schema doesn’t reflect the changes of the last 3 migrations
  Apply reviewer notes: update CHANGELOG, adjust code formatting
  Move issue rendering tests into separate contexts
  Move change description to proper release and fix typo
  Add more information into RSS fead for issues
  Revert CHANGELOG
  Also rename "find" in the specs
  Change to new Notes styleguide
  Add guide on changing a document's location
  Change logs.md location in README
  ...

Conflicts:
	app/services/ci/create_builds_service.rb
	app/services/ci/create_pipeline_service.rb
	app/services/create_commit_builds_service.rb
	spec/models/ci/commit_spec.rb
	spec/services/ci/create_builds_service_spec.rb
	spec/services/create_commit_builds_service_spec.rb
parents d8c4556d 066020fc
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:
...@@ -348,7 +349,7 @@ Style/MultilineArrayBraceLayout: ...@@ -348,7 +349,7 @@ Style/MultilineArrayBraceLayout:
# Avoid multi-line chains of blocks. # Avoid multi-line chains of blocks.
Style/MultilineBlockChain: Style/MultilineBlockChain:
Enabled: false Enabled: true
# Ensures newlines after multiline block do statements. # Ensures newlines after multiline block do statements.
Style/MultilineBlockLayout: Style/MultilineBlockLayout:
...@@ -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)
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- 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
- Don't show tags for revert and cherry-pick operations
- 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
- Fix SVG sanitizer to allow more elements
- 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 - Reduce number of fog gem dependencies
- Remove project notification settings associated with deleted projects - Remove project notification settings associated with deleted projects
...@@ -13,26 +25,70 @@ v 8.9.0 (unreleased) ...@@ -13,26 +25,70 @@ v 8.9.0 (unreleased)
- 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
- Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
- Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
- TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore - Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature - Remove 'main language' feature
- Pipelines can be canceled only when there are running builds - Pipelines can be canceled only when there are running builds
- 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
- 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 - Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment
- Cache project build count in sidebar nav - 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 - Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects - Improve error handling importing projects
- Remove duplicated notification settings
- Put project Files and Commits tabs under Code tab - Put project Files and Commits tabs under Code tab
- Decouple global notification level from user model
- 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
- All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects
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 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
...@@ -144,6 +200,7 @@ v 8.8.0 ...@@ -144,6 +200,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
...@@ -153,6 +210,7 @@ v 8.7.6 ...@@ -153,6 +210,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
...@@ -85,6 +86,7 @@ gem 'dropzonejs-rails', '~> 0.7.1' ...@@ -85,6 +86,7 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 0.9'
gem 'fog-azure', '~> 0.0'
gem 'fog-core', '~> 1.40' gem 'fog-core', '~> 1.40'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-google', '~> 0.3' gem 'fog-google', '~> 0.3'
...@@ -110,7 +112,7 @@ gem 'org-ruby', '~> 0.9.12' ...@@ -110,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
...@@ -143,7 +145,7 @@ gem 'redis-namespace' ...@@ -143,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'
...@@ -243,7 +245,7 @@ end ...@@ -243,7 +245,7 @@ end
group :development do group :development do
gem "foreman" gem "foreman"
gem 'brakeman', '~> 3.2.0', require: false gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'quiet_assets', '~> 1.0.2' gem 'quiet_assets', '~> 1.0.2'
...@@ -305,6 +307,9 @@ group :development, :test do ...@@ -305,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
......
...@@ -70,6 +70,21 @@ GEM ...@@ -70,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)
...@@ -82,17 +97,8 @@ GEM ...@@ -82,17 +97,8 @@ GEM
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.2.1) brakeman (3.3.2)
erubis (~> 2.6) browser (2.0.3)
haml (>= 3.0, < 5.0)
highline (>= 1.6.20, < 2.0)
ruby2ruby (~> 2.3.0)
ruby_parser (~> 3.8.1)
safe_yaml (>= 1.0)
sass (~> 3.0)
slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
browser (1.0.1)
builder (3.2.2) builder (3.2.2)
bullet (5.0.0) bullet (5.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
...@@ -213,6 +219,11 @@ GEM ...@@ -213,6 +219,11 @@ GEM
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
fog-azure (0.0.2)
azure (~> 0.6)
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
fog-core (1.40.0) fog-core (1.40.0)
builder builder
excon (~> 0.49) excon (~> 0.49)
...@@ -318,7 +329,6 @@ GEM ...@@ -318,7 +329,6 @@ GEM
hashie (3.4.3) hashie (3.4.3)
health_check (1.5.1) health_check (1.5.1)
rails (>= 2.3.0) rails (>= 2.3.0)
highline (1.7.8)
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
...@@ -358,6 +368,9 @@ GEM ...@@ -358,6 +368,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)
...@@ -366,6 +379,12 @@ GEM ...@@ -366,6 +379,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)
...@@ -381,7 +400,7 @@ GEM ...@@ -381,7 +400,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)
...@@ -392,8 +411,9 @@ GEM ...@@ -392,8 +411,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)
...@@ -465,6 +485,7 @@ GEM ...@@ -465,6 +485,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)
...@@ -540,7 +561,7 @@ GEM ...@@ -540,7 +561,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)
...@@ -569,7 +590,7 @@ GEM ...@@ -569,7 +590,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)
...@@ -611,19 +632,17 @@ GEM ...@@ -611,19 +632,17 @@ GEM
ruby-saml (1.1.2) ruby-saml (1.1.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
uuid (~> 2.3) uuid (~> 2.3)
ruby2ruby (2.3.0) ruby_parser (3.8.2)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
ruby_parser (3.8.1)
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)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.21) sass (3.4.22)
sass-rails (5.0.4) sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.0)
sass (~> 3.1) sass (~> 3.1)
...@@ -672,9 +691,6 @@ GEM ...@@ -672,9 +691,6 @@ GEM
tilt (>= 1.3, < 3) tilt (>= 1.3, < 3)
six (0.2.0) six (0.2.0)
slack-notifier (1.2.1) slack-notifier (1.2.1)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0) slop (3.6.0)
spinach (0.8.10) spinach (0.8.10)
colorize colorize
...@@ -715,10 +731,8 @@ GEM ...@@ -715,10 +731,8 @@ GEM
railties (>= 3.2.5, < 6) railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0) teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0) teaspoon (>= 1.0.0)
temple (0.7.6)
term-ansicolor (1.3.2) term-ansicolor (1.3.2)
tins (~> 1.0) tins (~> 1.0)
terminal-table (1.5.2)
test_after_commit (0.4.2) test_after_commit (0.4.2)
activerecord (>= 3.2) activerecord (>= 3.2)
thin (1.6.4) thin (1.6.4)
...@@ -727,7 +741,8 @@ GEM ...@@ -727,7 +741,8 @@ GEM
rack (~> 1.0) rack (~> 1.0)
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.5)
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)
...@@ -747,6 +762,7 @@ GEM ...@@ -747,6 +762,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)
...@@ -788,6 +804,7 @@ GEM ...@@ -788,6 +804,7 @@ GEM
builder builder
expression_parser expression_parser
rinku rinku
xml-simple (1.1.5)
xpath (2.0.0) xpath (2.0.0)
nokogiri (~> 1.3) nokogiri (~> 1.3)
...@@ -813,8 +830,8 @@ DEPENDENCIES ...@@ -813,8 +830,8 @@ DEPENDENCIES
better_errors (~> 1.0.1) better_errors (~> 1.0.1)
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.3.0)
browser (~> 1.0.0) browser (~> 2.0.3)
bullet bullet
bundler-audit bundler-audit
byebug byebug
...@@ -823,7 +840,6 @@ DEPENDENCIES ...@@ -823,7 +840,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)
...@@ -842,6 +858,7 @@ DEPENDENCIES ...@@ -842,6 +858,7 @@ DEPENDENCIES
flay flay
flog flog
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-azure (~> 0.0)
fog-core (~> 1.40) fog-core (~> 1.40)
fog-google (~> 0.3) fog-google (~> 0.3)
fog-local (~> 0.3) fog-local (~> 0.3)
...@@ -874,7 +891,9 @@ DEPENDENCIES ...@@ -874,7 +891,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)
...@@ -914,10 +933,11 @@ DEPENDENCIES ...@@ -914,10 +933,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
...@@ -925,7 +945,7 @@ DEPENDENCIES ...@@ -925,7 +945,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
...@@ -963,6 +983,7 @@ DEPENDENCIES ...@@ -963,6 +983,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)
...@@ -975,4 +996,4 @@ DEPENDENCIES ...@@ -975,4 +996,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()
...@@ -161,19 +162,6 @@ $ -> ...@@ -161,19 +162,6 @@ $ ->
$el.data('placement') || 'bottom' $el.data('placement') || 'bottom'
) )
$('.header-logo .home').tooltip(
placement: (_, el) ->
$el = $(el)
if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom'
container: 'body'
)
$('.page-with-sidebar').tooltip(
selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user'
placement: 'right'
container: 'body'
)
# Form submitter # Form submitter
$('.trigger-submit').on 'change', -> $('.trigger-submit').on 'change', ->
$(@).parents('form').submit() $(@).parents('form').submit()
...@@ -206,6 +194,7 @@ $ -> ...@@ -206,6 +194,7 @@ $ ->
$('.navbar-toggle').on 'click', -> $('.navbar-toggle').on 'click', ->
$('.header-content .title').toggle() $('.header-content .title').toggle()
$('.header-content .header-logo').toggle()
$('.header-content .navbar-collapse').toggle() $('.header-content .navbar-collapse').toggle()
$('.navbar-toggle').toggleClass('active') $('.navbar-toggle').toggleClass('active')
$('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left") $('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
...@@ -224,6 +213,10 @@ $ -> ...@@ -224,6 +213,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()
...@@ -236,7 +229,6 @@ $ -> ...@@ -236,7 +229,6 @@ $ ->
$this.attr 'value', $this.val() $this.attr 'value', $this.val()
$sidebarGutterToggle = $('.js-sidebar-toggle') $sidebarGutterToggle = $('.js-sidebar-toggle')
$navIconToggle = $('.toggle-nav-collapse')
$(document) $(document)
.off 'breakpoint:change' .off 'breakpoint:change'
...@@ -246,10 +238,6 @@ $ -> ...@@ -246,10 +238,6 @@ $ ->
if $gutterIcon.hasClass('fa-angle-double-right') if $gutterIcon.hasClass('fa-angle-double-right')
$sidebarGutterToggle.trigger('click') $sidebarGutterToggle.trigger('click')
$navIcon = $navIconToggle.find('.fa')
if $navIcon.hasClass('fa-angle-left')
$navIconToggle.trigger('click')
fitSidebarForSize = -> fitSidebarForSize = ->
oldBootstrapBreakpoint = bootstrapBreakpoint oldBootstrapBreakpoint = bootstrapBreakpoint
bootstrapBreakpoint = bp.getBreakpointSize() bootstrapBreakpoint = bp.getBreakpointSize()
...@@ -262,9 +250,10 @@ $ -> ...@@ -262,9 +250,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
$(window)
.off 'resize.build'
.on 'resize.build', @hideSidebar
if $('#build-trace').length
@getInitialBuildTrace()
@initScrollButtonAffix() @initScrollButtonAffix()
if build_status == "running" || build_status == "pending" 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,25 +39,36 @@ class CiBuild ...@@ -27,25 +39,36 @@ 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()
, 4000
getInitialBuildTrace: ->
$.ajax $.ajax
url: build_url + "/trace.json?state=" + encodeURIComponent(@state) 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" dataType: "json"
success: (log) => success: (log) =>
return unless last_state is @state if log.state
if log.state and log.status is "running"
@state = log.state @state = log.state
if log.status is "running"
if log.append if log.append
$('.fa-refresh').before log.html $('.js-build-output').append log.html
else else
$('#build-trace code').html log.html $('.js-build-output').html log.html
$('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
@checkAutoscroll() @checkAutoscroll()
else if log.status isnt build_status else if log.status isnt @build_status
Turbolinks.visit build_url Turbolinks.visit @build_url
, 4000
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
# Only filter asynchronously only if option remote is set
if @options.remote
clearTimeout timeout clearTimeout timeout
timeout = setTimeout => timeout = setTimeout =>
blur_field = @shouldBlur keyCode blur_field = @shouldBlur keyCode
search_text = @input.val()
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
@filter search_text
, 250 , 250
else
@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 parseInt($(label).val()) if label
labelIds
###*
* Returns Label IDs that will be removed from issue selection
* @return {Array} Array of labels IDs
###
getLabelsToRemove: ->
result = []
indeterminatedLabels = @getUnmarkedIndeterminedLabels()
labelsToApply = @getLabelsToApply()
indeterminatedLabels.map (id) ->
# We need to exclude label IDs that will be applied
# By not doing this will cause issues from selection to not add labels at all
result.push(id) if labelsToApply.indexOf(id) is -1
result
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
if not $dropdown.hasClass 'js-filter-bulk-update'
saveLabelData() 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 %>')
...@@ -47,4 +47,4 @@ $ -> ...@@ -47,4 +47,4 @@ $ ->
# Make logo clickable as part of a workaround for Safari visited # Make logo clickable as part of a workaround for Safari visited
# link behaviour (See !2690). # link behaviour (See !2690).
$('#logo').on 'click', -> $('#logo').on 'click', ->
$('#js-shortcuts-home').get(0).click() Turbolinks.visit('/')
...@@ -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()
...@@ -156,10 +156,13 @@ class @SearchAutocomplete ...@@ -156,10 +156,13 @@ class @SearchAutocomplete
# No need to enable anything if user is not logged in # No need to enable anything if user is not logged in
return if !gon.current_user_id return if !gon.current_user_id
unless @dropdown.hasClass('open')
_this = @ _this = @
@loadingSuggestions = false @loadingSuggestions = false
@dropdown.addClass('open') @dropdown
.addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled') @searchInput.removeClass('disabled')
onSearchInputKeyDown: => onSearchInputKeyDown: =>
...@@ -191,7 +194,7 @@ class @SearchAutocomplete ...@@ -191,7 +194,7 @@ class @SearchAutocomplete
@disableAutocomplete() @disableAutocomplete()
else else
# We should display the menu only when input is not empty # We should display the menu only when input is not empty
@enableAutocomplete() @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value @wrap.toggleClass 'has-value', !!e.target.value
......
...@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation ...@@ -10,14 +10,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
@replyWithSelectedText() @replyWithSelectedText()
return false return false
) )
Mousetrap.bind('j', =>
@prevIssue()
return false
)
Mousetrap.bind('k', =>
@nextIssue()
return false
)
Mousetrap.bind('e', => Mousetrap.bind('e', =>
@editIssue() @editIssue()
return false return false
...@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation ...@@ -29,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation
else else
@enabledHelp.push('.hidden-shortcut.issues') @enabledHelp.push('.hidden-shortcut.issues')
prevIssue: ->
$prevBtn = $('.prev-btn')
if not $prevBtn.hasClass('disabled')
Turbolinks.visit($prevBtn.attr('href'))
nextIssue: ->
$nextBtn = $('.next-btn')
if not $nextBtn.hasClass('disabled')
Turbolinks.visit($nextBtn.attr('href'))
replyWithSelectedText: -> replyWithSelectedText: ->
if window.getSelection if window.getSelection
selected = window.getSelection().toString() selected = window.getSelection().toString()
......
...@@ -4,8 +4,6 @@ expanded = 'page-sidebar-expanded' ...@@ -4,8 +4,6 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = -> toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded") $('header').toggleClass("header-collapsed header-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( -> setTimeout ( ->
niceScrollBars = $('.nicescroll').niceScroll(); niceScrollBars = $('.nicescroll').niceScroll();
...@@ -17,10 +15,3 @@ $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> ...@@ -17,10 +15,3 @@ $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
toggleSidebar() toggleSidebar()
) )
$ ->
size = bp.getBreakpointSize()
if size is "xs" or size is "sm"
if $('.page-with-sidebar').hasClass(expanded)
toggleSidebar()
...@@ -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 {
......
...@@ -8,34 +8,16 @@ ...@@ -8,34 +8,16 @@
*/ */
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar { .page-with-sidebar {
.header-logo {
background: $color-darker;
a {
color: $color-light;
h3 { .collapse-nav a {
color: $color-light; color: $color-light;
} background: $color;
}
&:hover { &:hover {
background-color: $color-dark; color: $white-light;
a {
color: #fff;
h3 {
color: #fff;
}
}
} }
} }
.collapse-nav a {
color: #fff;
background: $color;
}
.sidebar-wrapper { .sidebar-wrapper {
background: $color-darker; background: $color-darker;
...@@ -45,7 +27,7 @@ ...@@ -45,7 +27,7 @@
&:hover { &:hover {
background-color: $color-dark; background-color: $color-dark;
color: #fff; color: $white-light;
text-decoration: none; text-decoration: none;
} }
} }
...@@ -63,10 +45,20 @@ ...@@ -63,10 +45,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 +66,7 @@ ...@@ -74,7 +66,7 @@
} }
&.active a { &.active a {
color: #fff; color: $white-light;
background: $color-dark; background: $color-dark;
&.no-highlight { &.no-highlight {
...@@ -82,15 +74,23 @@ ...@@ -82,15 +74,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 +102,7 @@ body { ...@@ -102,7 +102,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;
}
} }
} }
...@@ -108,10 +109,8 @@ header { ...@@ -108,10 +109,8 @@ header {
position: relative; position: relative;
height: $header-height; height: $header-height;
padding-right: 40px; padding-right: 40px;
padding-left: 30px;
@media (max-width: $screen-xs-min) { transition-duration: .3s;
padding-left: 40px;
}
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: 0; padding-right: 0;
...@@ -121,9 +120,29 @@ header { ...@@ -121,9 +120,29 @@ header {
margin-top: -5px; margin-top: -5px;
} }
.header-logo {
position: absolute;
left: 50%;
margin-left: -18px;
top: 7px;
transition-duration: .3s;
z-index: 999;
&:hover {
cursor: pointer;
}
@media (max-width: $screen-xs-max) {
right: 25px;
left: auto;
}
}
.title { .title {
margin: 0; margin: 0;
font-size: 19px; font-size: 19px;
max-width: 400px;
display: inline-block;
line-height: $header-height; line-height: $header-height;
font-weight: normal; font-weight: normal;
color: $gl-text-color; color: $gl-text-color;
...@@ -132,6 +151,10 @@ header { ...@@ -132,6 +151,10 @@ header {
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
@media (max-width: $screen-sm-max) {
max-width: 190px;
}
a { a {
color: $gl-text-color; color: $gl-text-color;
&:hover { &:hover {
...@@ -159,6 +182,10 @@ header { ...@@ -159,6 +182,10 @@ header {
.navbar-collapse { .navbar-collapse {
float: right; float: right;
border-top: none; border-top: none;
@media (max-width: $screen-xs-max) {
float: none;
}
} }
} }
...@@ -171,31 +198,24 @@ header { ...@@ -171,31 +198,24 @@ 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) { .header-content {
@include collapsed-header;
}
@media (max-width: $screen-xs-min) { @media (min-width: $screen-sm-max) {
margin-left: 0; padding-left: 30px;
transition-duration: .3s;
}
} }
} }
.header-expanded { .tanuki-shape {
margin-left: $sidebar_collapsed_width; transition: all 0.8s;
@media (min-width: $screen-md-min) {
margin-left: $sidebar_width;
}
@media (max-width: $screen-xs-min) { &:hover, &.highlight {
margin-left: 0; fill: rgb(255, 255, 255);
transition: all 0.1s;
} }
} }
......
...@@ -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 {
.ui-state-active,
.ui-state-hover,
.ui-state-focus {
border: 1px solid $gl-primary; border: 1px solid $gl-primary;
background: $gl-primary; background: $gl-primary;
color: #fff; color: #fff;
} }
.ui-state-hover,
.ui-state-focus {
border: 1px solid $row-hover;
background: $row-hover;
color: #333;
} }
} }
......
...@@ -137,11 +137,31 @@ ul.content-list { ...@@ -137,11 +137,31 @@ 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;
}
}
} }
.panel > .content-list > li { .panel > .content-list > li {
......
...@@ -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;
......
@mixin fade($gradient-direction, $rgba, $gradient-color) { @mixin fade($gradient-direction, $rgba, $gradient-color) {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
z-index: 2;
position: absolute; position: absolute;
bottom: 12px; bottom: 12px;
width: 43px; width: 43px;
...@@ -41,8 +42,7 @@ ...@@ -41,8 +42,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 +67,28 @@ ...@@ -67,6 +67,28 @@
color: #78a; color: #78a;
} }
} }
&.sub-nav {
text-align: center;
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 +103,10 @@ ...@@ -81,6 +103,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 +130,10 @@ ...@@ -104,6 +130,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%;
...@@ -179,7 +209,7 @@ ...@@ -179,7 +209,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 +240,6 @@ ...@@ -210,16 +240,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;
}
}
} }
} }
...@@ -231,6 +251,7 @@ ...@@ -231,6 +251,7 @@
background: $background-color; background: $background-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
transition-duration: .3s; transition-duration: .3s;
text-align: center;
.container-fluid { .container-fluid {
position: relative; position: relative;
...@@ -276,6 +297,19 @@ ...@@ -276,6 +297,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 +331,17 @@ ...@@ -297,9 +331,17 @@
} }
&.active { &.active {
a, i { a, i {
color: $black; color: $black;
} }
svg {
path,
polygon {
fill: $black;
}
}
} }
.badge { .badge {
...@@ -309,10 +351,10 @@ ...@@ -309,10 +351,10 @@
} }
.nav-control { .nav-control {
.fade-right {
.fade-right {
@media (min-width: $screen-xs-max) { @media (min-width: $screen-xs-max) {
right: 67px; right: 68px;
} }
@media (max-width: $screen-xs-min) { @media (max-width: $screen-xs-min) {
right: 0; right: 0;
...@@ -321,6 +363,24 @@ ...@@ -321,6 +363,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 {
...@@ -49,58 +35,11 @@ ...@@ -49,58 +35,11 @@
} }
.sidebar-wrapper { .sidebar-wrapper {
.header-logo {
border-bottom: 1px solid transparent;
float: left;
height: $header-height;
width: $sidebar_width;
position: fixed;
z-index: 999;
overflow: hidden;
transition-duration: .3s;
a {
float: left;
height: $header-height;
width: 100%;
padding-left: 22px;
overflow: hidden;
outline: none;
transition-duration: .3s;
img {
width: 36px;
height: 36px;
}
#tanuki-logo, img {
float: left;
}
.gitlab-text-container {
width: 230px;
h3 {
width: 158px;
float: left;
margin: 0;
margin-left: 50px;
font-size: 19px;
line-height: 50px;
font-weight: normal;
}
}
}
&:hover {
background-color: #eee;
}
}
.sidebar-user { .sidebar-user {
padding: 7px 22px; padding: 15px 22px;
position: fixed; position: fixed;
bottom: 40px; bottom: 0;
width: $sidebar_width; width: $sidebar_width;
overflow: hidden; overflow: hidden;
transition-duration: .3s; transition-duration: .3s;
...@@ -126,8 +65,8 @@ ...@@ -126,8 +65,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 +84,12 @@ ...@@ -145,13 +84,12 @@
} }
a { a {
padding: 7px 15px; width: $sidebar_width;
padding: 7px 15px 7px 23px;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 24px; line-height: 24px;
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 +102,12 @@ ...@@ -164,16 +102,12 @@
} }
i { i {
width: 16px; font-size: 16px;
color: $gray-light;
margin-right: 13px;
} }
.count { i,
float: right; svg {
background: #eee; margin-right: 13px;
padding: 0 8px;
@include border-radius(6px);
} }
&.back-link i { &.back-link i {
...@@ -181,6 +115,12 @@ ...@@ -181,6 +115,12 @@
} }
} }
} }
.count {
float: right;
padding: 0 8px;
@include border-radius(6px);
}
} }
.sidebar-subnav { .sidebar-subnav {
...@@ -195,11 +135,12 @@ ...@@ -195,11 +135,12 @@
.collapse-nav a { .collapse-nav a {
width: $sidebar_width; width: $sidebar_width;
position: fixed; position: fixed;
bottom: 0; top: 0;
left: 0; left: 0;
font-size: 13px; padding: 5px 0;
font-size: 18px;
background: transparent; background: transparent;
height: 40px; height: 50px;
text-align: center; text-align: center;
line-height: 40px; line-height: 40px;
transition-duration: .3s; transition-duration: .3s;
...@@ -217,37 +158,13 @@ ...@@ -217,37 +158,13 @@
} }
.page-sidebar-collapsed { .page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
padding-left: 0; padding-left: 0;
}
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0; width: 0;
}
.header-logo {
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0;
}
a {
padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
}
}
}
.nav-sidebar { .nav-sidebar {
width: $sidebar_collapsed_width; width: 0;
li { li {
width: auto; width: auto;
...@@ -261,46 +178,28 @@ ...@@ -261,46 +178,28 @@
} }
.collapse-nav a { .collapse-nav a {
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0; width: 0;
i {
display: none;
} }
} }
.sidebar-user { .sidebar-user {
padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
@media (max-width: $screen-xs-min) {
width: 0; width: 0;
padding-left: 0; padding-left: 0;
padding-right: 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;
@media (min-width: $screen-md-min) {
padding-left: $sidebar_width;
}
@media (max-width: $screen-xs-min) { @media (max-width: $screen-sm-max) {
padding-left: 0; padding-left: 0;
} }
...@@ -321,20 +220,6 @@ ...@@ -321,20 +220,6 @@
} }
} }
} }
.layout-nav {
@media (max-width: $screen-xs-min) {
padding-right: 0;
}
@media (min-width: $screen-xs-min) and (max-width: $screen-md-min) {
padding-right: 62px;
}
@media (min-width: $screen-md-min) {
padding-right: $sidebar_width;
}
}
} }
.right-sidebar-collapsed { .right-sidebar-collapsed {
...@@ -353,8 +238,10 @@ ...@@ -353,8 +238,10 @@
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) {
&:not(.build-sidebar) {
padding-right: $sidebar_collapsed_width; padding-right: $sidebar_collapsed_width;
} }
}
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; padding-right: $gutter_width;
......
...@@ -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;
}
...@@ -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;
}
td.diff-line-num { &.diff-line-num {
margin: 0; margin: 0;
padding: 0; padding: 0;
border: none; border: none;
background: $background-color;
color: rgba(0, 0, 0, 0.3);
padding: 0 5px; padding: 0 5px;
border-right: 1px solid $border-color; border-right: 1px solid;
text-align: right; text-align: right;
min-width: 35px; min-width: 35px;
max-width: 50px; max-width: 50px;
width: 35px; width: 35px;
} }
td.line_content { &.line_content {
display: block; display: block;
margin: 0; margin: 0;
padding: 0 0.5em; padding: 0 0.5em;
border: none; border: none;
white-space: pre; white-space: pre;
} }
}
}
.line-numbers, .diff-line-num {
background-color: $background-color;
}
.diff-line-num, .diff-line-num a {
color: $black-transparent;
}
pre.code, .diff-line-num {
border-color: $table-border-gray;
}
.code.white, pre.code, .line_content {
background-color: #fff;
color: #333;
}
.diff-line-num {
&.old {
background-color: $line-number-old;
border-color: $line-removed-dark;
}
&.new {
background-color: $line-number-new;
border-color: $line-added-dark;
}
&.hll:not(.empty-cell) {
background-color: $line-number-select;
border-color: $line-select-yellow-dark;
}
}
.line_content {
&.old {
background-color: $line-removed;
> .line > span.idiff, > .line > span > span.idiff {
background-color: $line-removed-dark;
}
}
&.new {
background-color: $line-added;
> .line > span.idiff, > .line > span > span.idiff {
background-color: $line-added-dark;
}
}
&.match {
color: $black-transparent;
background-color: $match-line;
}
&.hll:not(.empty-cell) {
background-color: $line-select-yellow;
}
}
pre > .hll {
background-color: #f8eec7 !important;
}
span.highlight_word {
background-color: #fafe3d !important;
} }
@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;
} }
.file-stats .deleted-file { > .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; a {
color: $gl-gray;
&:hover {
color: $gl-link-color;
text-decoration: none;
} }
} }
.alert-disabled { code {
background: $background-color; color: $code-color;
a {
color: #3084bb !important;
} }
.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,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: block;
margin-bottom: 10px;
@media (min-width: $screen-sm-min) {
display: inline-block; display: inline-block;
width: 200px; width: 200px;
margin-bottom: 0;
}
}
@media (max-width: $screen-xs-min) { .label-description {
display: block; 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 {
@media (max-width: $screen-xs-min) {
display: block;
width: 100%;
margin-left: 0;
padding: 10px 0;
}
}
.manage-labels-list { .manage-labels-list {
.btn-action {
color: $gl-dark-link-color;
.prepend-left-10, .prepend-description-left { .fa {
display: inline-block; font-size: 18px;
width: 40%;
vertical-align: middle; vertical-align: middle;
@include labels-mobile;
} }
.prepend-description-left { &:hover {
width: 57%; color: $gl-link-color;
@include labels-mobile; &.remove-row {
color: $gl-danger;
}
}
} }
.pull-info-right { .dropdown {
@media (min-width: $screen-sm-min) {
float: right; float: right;
}
}
}
.prioritized-labels {
margin-bottom: 30px;
@media (max-width: $screen-xs-min) { .add-priority {
float: none; display: none;
color: $gray-light;
} }
}
.action-buttons { .other-labels {
.remove-priority {
display: none;
}
}
.toggle-priority {
display: inline-block;
vertical-align: middle;
button {
border-color: transparent;
padding: 5px 8px;
vertical-align: top;
font-size: 14px;
&:hover {
border-color: transparent; border-color: transparent;
padding: 6px; }
color: $gl-text-color; }
}
&.label-subscribe-button { .filtered-labels {
padding-left: 0; .label-row {
&:not(:last-child) {
margin-right: 5px;
} }
} }
i { .label-remove {
color: $gl-text-color; border-left: 1px solid rgba(0, 0, 0, .1);
z-index: 3;
} }
.append-right-20 { .btn {
a { color: inherit;
color: $gl-text-color;
} }
}
@media (max-width: $screen-xs-min) { .label-options-toggle {
display: block; width: 100%;
margin-bottom: 10px; }
.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: 0;
line-height: 20px;
@media (min-width: $screen-sm-min) {
margin-left: 10px; margin-left: 10px;
line-height: 24px; 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;
} }
} }
......
...@@ -158,13 +158,11 @@ ...@@ -158,13 +158,11 @@
.search-holder { .search-holder {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox;
display: flex; display: flex;
} }
.search-field-holder { .search-field-holder {
-webkit-flex: 1 0 auto; -webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto; flex: 1 0 auto;
position: relative; position: relative;
margin-right: 0; margin-right: 0;
......
...@@ -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
render 'devise/sessions/two_factor' and return # Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
challenges = sign_requests.map(&:challenge)
session[:challenges] = challenges
gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
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_with_user_password(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
......
...@@ -3,10 +3,11 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -3,10 +3,11 @@ class Profiles::NotificationsController < Profiles::ApplicationController
@user = current_user @user = current_user
@group_notifications = current_user.notification_settings.for_groups @group_notifications = current_user.notification_settings.for_groups
@project_notifications = current_user.notification_settings.for_projects @project_notifications = current_user.notification_settings.for_projects
@global_notification_setting = current_user.global_notification_setting
end end
def update def update
if current_user.update_attributes(user_params) if current_user.update_attributes(user_params) && update_notification_settings
flash[:notice] = "Notification settings saved" flash[:notice] = "Notification settings saved"
else else
flash[:alert] = "Failed to save new settings" flash[:alert] = "Failed to save new settings"
...@@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController ...@@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:notification_email, :notification_level) params.require(:user).permit(:notification_email)
end
def global_notification_setting_params
params.require(:global_notification_setting).permit(:level)
end
private
def update_notification_settings
return true unless global_notification_setting_params
current_user.global_notification_setting.update_attributes(global_notification_setting_params)
end end
end end
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement skip_before_action :check_2fa_requirement
def new def show
unless current_user.otp_secret unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32) current_user.otp_secret = User.generate_otp_secret(32)
end end
...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed? current_user.save! if current_user.changed?
if two_factor_authentication_required? if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired? if two_factor_grace_period_expired?
flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.'
else else
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}."
end end
end end
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
end end
def create def create
if current_user.validate_and_consume_otp!(params[:pin_code]) if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes! @codes = current_user.generate_otp_backup_codes!
current_user.save! current_user.save!
...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
else else
@error = 'Invalid pin code' @error = 'Invalid pin code'
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration
render 'show'
end
end
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
render 'new' if @u2f_registration.persisted?
session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
render :show
end end
end end
...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def issuer_host def issuer_host
Gitlab.config.gitlab.host Gitlab.config.gitlab.host
end end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles)
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
sign_requests: sign_requests,
browser_supports_u2f: browser_supports_u2f? })
end
end end
...@@ -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
......
...@@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController ...@@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController
return if cached_blob? return if cached_blob?
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) send_git_blob @repository, @blob
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob)
head :ok # 'render nothing: true' messes up the Content-Type
else else
render_404 render_404
end end
......
This diff is collapsed.
...@@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController ...@@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController
if @blob.lfs_pointer? if @blob.lfs_pointer?
send_lfs_object send_lfs_object
else else
headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) send_git_blob @repository, @blob
headers['Content-Disposition'] = 'inline'
headers['Content-Type'] = safe_content_type(@blob)
head :ok # 'render nothing: true' messes up the Content-Type
end end
else else
render_404 render_404
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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