Commit 6448368b authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

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

parents 115ffa37 7f78a78a

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"gon": false, "gon": false,
"localStorage": false "localStorage": false
}, },
"parser": "babel-eslint",
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
......
...@@ -3,3 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb ...@@ -3,3 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb lib/gitlab/diff/position_tracer.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
lib/gitlab/redis/*.rb
...@@ -21,6 +21,7 @@ eslint-report.html ...@@ -21,6 +21,7 @@ eslint-report.html
/.yarn-cache /.yarn-cache
/.byebug_history /.byebug_history
/Vagrantfile /Vagrantfile
/app/assets/javascripts/locale/**/app.js
/backups/* /backups/*
/config/aws.yml /config/aws.yml
/config/database.yml /config/database.yml
...@@ -30,6 +31,9 @@ eslint-report.html ...@@ -30,6 +31,9 @@ eslint-report.html
/config/initializers/smtp_settings.rb /config/initializers/smtp_settings.rb
/config/initializers/relative_url.rb /config/initializers/relative_url.rb
/config/resque.yml /config/resque.yml
/config/redis.cache.yml
/config/redis.queues.yml
/config/redis.shared_state.yml
/config/unicorn.rb /config/unicorn.rb
/config/secrets.yml /config/secrets.yml
/config/sidekiq.yml /config/sidekiq.yml
...@@ -59,3 +63,4 @@ eslint-report.html ...@@ -59,3 +63,4 @@ eslint-report.html
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/.rspec
This diff is collapsed.
--color
--format Fuubar
...@@ -965,6 +965,10 @@ RSpec/AnyInstance: ...@@ -965,6 +965,10 @@ RSpec/AnyInstance:
RSpec/BeEql: RSpec/BeEql:
Enabled: true Enabled: true
# We don't enforce this as we use this technique in a few places.
RSpec/BeforeAfterAll:
Enabled: false
# Check that the first argument to the top level describe is the tested class or # Check that the first argument to the top level describe is the tested class or
# module. # module.
RSpec/DescribeClass: RSpec/DescribeClass:
...@@ -1024,6 +1028,12 @@ RSpec/FilePath: ...@@ -1024,6 +1028,12 @@ RSpec/FilePath:
RSpec/Focus: RSpec/Focus:
Enabled: true Enabled: true
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: is_expected, should
RSpec/ImplicitExpect:
Enabled: true
EnforcedStyle: is_expected
# Checks for the usage of instance variables. # Checks for the usage of instance variables.
RSpec/InstanceVariable: RSpec/InstanceVariable:
Enabled: false Enabled: false
......
...@@ -6,10 +6,6 @@ ...@@ -6,10 +6,6 @@
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 54
RSpec/BeforeAfterAll:
Enabled: false
# Offense count: 233 # Offense count: 233
RSpec/EmptyLineAfterFinalLet: RSpec/EmptyLineAfterFinalLet:
Enabled: false Enabled: false
...@@ -24,12 +20,6 @@ RSpec/EmptyLineAfterSubject: ...@@ -24,12 +20,6 @@ RSpec/EmptyLineAfterSubject:
RSpec/HookArgument: RSpec/HookArgument:
Enabled: false Enabled: false
# Offense count: 12
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: is_expected, should
RSpec/ImplicitExpect:
Enabled: false
# Offense count: 11 # Offense count: 11
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: it_behaves_like, it_should_behave_like # SupportedStyles: it_behaves_like, it_should_behave_like
......
...@@ -10,7 +10,7 @@ linters: ...@@ -10,7 +10,7 @@ linters:
# Reports when you use improper spacing around ! (the "bang") in !default, # Reports when you use improper spacing around ! (the "bang") in !default,
# !global, !important, and !optional flags. # !global, !important, and !optional flags.
BangFormat: BangFormat:
enabled: false enabled: true
# Whether or not to prefer `border: 0` over `border: none`. # Whether or not to prefer `border: 0` over `border: none`.
BorderZero: BorderZero:
...@@ -43,10 +43,11 @@ linters: ...@@ -43,10 +43,11 @@ linters:
# Rule sets should be ordered as follows: # Rule sets should be ordered as follows:
# - @extend declarations # - @extend declarations
# - @include declarations without inner @content # - @include declarations without inner @content
# - properties, @include declarations with inner @content # - properties
# - @include declarations with inner @content
# - nested rule sets. # - nested rule sets.
DeclarationOrder: DeclarationOrder:
enabled: false enabled: true
# `scss-lint:disable` control comments should be preceded by a comment # `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file. # explaining why these linters are being disabled for this file.
...@@ -93,7 +94,7 @@ linters: ...@@ -93,7 +94,7 @@ linters:
# The basenames of @imported SCSS partials should not begin with an # The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension. # underscore and should not include the filename extension.
ImportPath: ImportPath:
enabled: false enabled: true
# Avoid using !important in properties. It is usually indicative of a # Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code. # misunderstanding of CSS specificity and can lead to brittle code.
...@@ -133,7 +134,7 @@ linters: ...@@ -133,7 +134,7 @@ linters:
# Reports when you use an unknown or disabled CSS property # Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties). # (ignoring vendor-prefixed properties).
PropertySpelling: PropertySpelling:
enabled: false enabled: true
# Configure which units are allowed for property values. # Configure which units are allowed for property values.
PropertyUnits: PropertyUnits:
...@@ -176,6 +177,10 @@ linters: ...@@ -176,6 +177,10 @@ linters:
# Commas in lists should be followed by a space. # Commas in lists should be followed by a space.
SpaceAfterComma: SpaceAfterComma:
enabled: true
# Comment literals should be followed by a space.
SpaceAfterComment:
enabled: false enabled: false
# Properties should be formatted with a single space separating the colon # Properties should be formatted with a single space separating the colon
...@@ -240,7 +245,7 @@ linters: ...@@ -240,7 +245,7 @@ linters:
# Do not use parent selector references (&) when they would otherwise # Do not use parent selector references (&) when they would otherwise
# be unnecessary. # be unnecessary.
UnnecessaryParentReference: UnnecessaryParentReference:
enabled: false enabled: true
# URLs should be valid and not contain protocols or domain names. # URLs should be valid and not contain protocols or domain names.
UrlFormat: UrlFormat:
......
This diff is collapsed.
...@@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._ ...@@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
Thank you for your interest in contributing to GitLab. This guide details how Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone. to contribute to GitLab in a way that is efficient for everyone.
Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for edition. Throughout this guide you will see references to CE and EE for
......
...@@ -2,7 +2,6 @@ source 'https://rubygems.org' ...@@ -2,7 +2,6 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8' gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with # Responders respond_to and respond_with
gem 'responders', '~> 2.0' gem 'responders', '~> 2.0'
...@@ -13,7 +12,7 @@ gem 'sprockets', '~> 3.7.0' ...@@ -13,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
gem 'default_value_for', '~> 3.0.0' gem 'default_value_for', '~> 3.0.0'
# Supported DBs # Supported DBs
gem 'mysql2', '~> 0.3.16', group: :mysql gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1' gem 'rugged', '~> 0.25.1.1'
...@@ -38,7 +37,7 @@ gem 'omniauth-saml', '~> 1.7.0' ...@@ -38,7 +37,7 @@ gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.0' gem 'omniauth-authentiq', '~> 0.3.1'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6' gem 'jwt', '~> 1.5.6'
...@@ -92,7 +91,7 @@ gem 'carrierwave', '~> 1.1' ...@@ -92,7 +91,7 @@ gem 'carrierwave', '~> 1.1'
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 1.4'
gem 'fog-core', '~> 1.44' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5' gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
...@@ -123,6 +122,7 @@ gem 'asciidoctor', '~> 1.5.2' ...@@ -123,6 +122,7 @@ gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7' gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8' gem 'truncato', '~> 0.7.8'
gem 'bootstrap_form', '~> 2.7.0'
# 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
...@@ -163,6 +163,9 @@ gem 'rainbow', '~> 2.2' ...@@ -163,6 +163,9 @@ gem 'rainbow', '~> 2.2'
# GitLab settings # GitLab settings
gem 'settingslogic', '~> 2.0.9' gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions
gem 're2', '~> 1.0.0'
# Misc # Misc
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
...@@ -237,7 +240,6 @@ gem 'webpack-rails', '~> 0.9.10' ...@@ -237,7 +240,6 @@ gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0' gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
...@@ -250,13 +252,12 @@ gem 'jquery-rails', '~> 4.1.0' ...@@ -250,13 +252,12 @@ gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3' gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 2.4.0' gem 'sentry-raven', '~> 2.5.3'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.7'
# I18n # I18n
gem 'ruby_parser', '~> 3.8', require: false gem 'ruby_parser', '~> 3.8', require: false
...@@ -270,7 +271,7 @@ gem 'peek', '~> 1.0.1' ...@@ -270,7 +271,7 @@ gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2' gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0' gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.2.1' gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0' gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0' gem 'peek-redis', '~> 1.2.0'
...@@ -283,7 +284,8 @@ group :metrics do ...@@ -283,7 +284,8 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta5' gem 'prometheus-client-mmap', '~>0.7.0.beta9'
gem 'raindrops', '~> 0.18'
end end
group :development do group :development do
...@@ -334,7 +336,7 @@ group :development, :test do ...@@ -334,7 +336,7 @@ group :development, :test do
gem 'rubocop', '~> 0.47.1', require: false gem 'rubocop', '~> 0.47.1', require: false
gem 'rubocop-rspec', '~> 1.15.0', require: false gem 'rubocop-rspec', '~> 1.15.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.54.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false gem 'haml_lint', '~> 0.21.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false gem 'flay', '~> 2.8.0', require: false
...@@ -354,7 +356,7 @@ group :test do ...@@ -354,7 +356,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2' gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.24.0' gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
...@@ -384,10 +386,13 @@ gem 'vmstat', '~> 2.3.0' ...@@ -384,10 +386,13 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.9.0' gem 'gitaly', '~> 0.17.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
# Feature toggles # Feature toggles
gem 'flipper', '~> 0.10.2' gem 'flipper', '~> 0.10.2'
gem 'flipper-active_record', '~> 0.10.2' gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
...@@ -83,11 +83,10 @@ GEM ...@@ -83,11 +83,10 @@ GEM
bindata (2.3.5) bindata (2.3.5)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
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)
bootstrap_form (2.7.0)
brakeman (3.6.1) brakeman (3.6.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.3) builder (3.2.3)
...@@ -123,13 +122,6 @@ GEM ...@@ -123,13 +122,6 @@ GEM
coderay (1.1.1) coderay (1.1.1)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.10.0)
colorize (0.7.7) colorize (0.7.7)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5) concurrent-ruby-ext (1.0.5)
...@@ -138,7 +130,7 @@ GEM ...@@ -138,7 +130,7 @@ GEM
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
creole (0.5.0) creole (0.5.0)
css_parser (1.4.1) css_parser (1.5.0)
addressable addressable
d3_rails (3.5.11) d3_rails (3.5.11)
railties (>= 3.1.0) railties (>= 3.1.0)
...@@ -187,7 +179,7 @@ GEM ...@@ -187,7 +179,7 @@ GEM
et-orbi (1.0.3) et-orbi (1.0.3)
tzinfo tzinfo
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.55.0) excon (0.57.1)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extlib (0.9.16) extlib (0.9.16)
...@@ -223,26 +215,26 @@ GEM ...@@ -223,26 +215,26 @@ GEM
fog-json (~> 1.0) fog-json (~> 1.0)
ipaddress (~> 0.8) ipaddress (~> 0.8)
xml-simple (~> 1.1) xml-simple (~> 1.1)
fog-aws (0.13.0) fog-aws (1.4.0)
fog-core (~> 1.38) fog-core (~> 1.38)
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
fog-core (1.44.1) fog-core (1.44.3)
builder builder
excon (~> 0.49) excon (~> 0.49)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (0.5.0) fog-google (0.5.3)
fog-core fog-core
fog-json fog-json
fog-xml fog-xml
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
fog-local (0.3.0) fog-local (0.3.1)
fog-core (~> 1.27) fog-core (~> 1.27)
fog-openstack (0.1.6) fog-openstack (0.1.21)
fog-core (>= 1.39) fog-core (>= 1.40)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
fog-rackspace (0.1.1) fog-rackspace (0.1.1)
...@@ -277,7 +269,7 @@ GEM ...@@ -277,7 +269,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.9.0) gitaly (0.17.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -353,7 +345,7 @@ GEM ...@@ -353,7 +345,7 @@ GEM
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grpc (1.2.5) grpc (1.4.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
...@@ -367,7 +359,7 @@ GEM ...@@ -367,7 +359,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashdiff (0.3.2) hashdiff (0.3.4)
hashie (3.5.5) hashie (3.5.5)
hashie-forbidden_attributes (0.1.1) hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0) hashie (>= 3.0)
...@@ -451,6 +443,10 @@ GEM ...@@ -451,6 +443,10 @@ GEM
logging (2.2.2) logging (2.2.2)
little-plugger (~> 1.1) little-plugger (~> 1.1)
multi_json (~> 1.10) multi_json (~> 1.10)
lograge (0.5.1)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.5) mail (2.6.5)
...@@ -462,9 +458,8 @@ GEM ...@@ -462,9 +458,8 @@ GEM
mimemagic (0.3.0) mimemagic (0.3.0)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.6) mmap2 (2.2.7)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
...@@ -472,9 +467,8 @@ GEM ...@@ -472,9 +467,8 @@ GEM
tool (~> 0.2) tool (~> 0.2)
mustermann-grape (0.4.0) mustermann-grape (0.4.0)
mustermann (= 0.4.0) mustermann (= 0.4.0)
mysql2 (0.3.20) mysql2 (0.4.5)
net-ldap (0.12.1) net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.6.8.1) nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
...@@ -494,7 +488,7 @@ GEM ...@@ -494,7 +488,7 @@ GEM
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.3.0) omniauth-authentiq (0.3.1)
omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6) omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0) jwt (~> 1.0)
...@@ -563,7 +557,7 @@ GEM ...@@ -563,7 +557,7 @@ GEM
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
peek peek
peek-performance_bar (1.2.1) peek-performance_bar (1.3.0)
peek (>= 0.1.0) peek (>= 0.1.0)
peek-pg (1.3.0) peek-pg (1.3.0)
concurrent-ruby concurrent-ruby
...@@ -591,14 +585,15 @@ GEM ...@@ -591,14 +585,15 @@ GEM
websocket-driver (>= 0.2.0) websocket-driver (>= 0.2.0)
posix-spawn (0.3.11) posix-spawn (0.3.11)
powerpack (0.1.1) powerpack (0.1.1)
premailer (1.8.6) premailer (1.10.4)
css_parser (>= 1.3.6) addressable
css_parser (>= 1.4.10)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.9.2) premailer-rails (1.9.7)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta5) prometheus-client-mmap (0.7.0.beta9)
mmap2 (~> 2.2.6) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -656,12 +651,13 @@ GEM ...@@ -656,12 +651,13 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.17.0) raindrops (0.18.0)
rake (10.5.0) rake (10.5.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2) rdoc (4.2.2)
json (~> 1.4) json (~> 1.4)
re2 (1.0.0)
recaptcha (3.0.0) recaptcha (3.0.0)
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.0)
...@@ -764,16 +760,16 @@ GEM ...@@ -764,16 +760,16 @@ GEM
sawyer (0.8.1) sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6) addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0) faraday (~> 0.8, < 1.0)
scss_lint (0.47.1) scss_lint (0.54.0)
rake (>= 0.9, < 11) rake (>= 0.9, < 13)
sass (~> 3.4.15) sass (~> 3.4.20)
securecompare (1.0.0) securecompare (1.0.0)
seed-fu (2.3.6) seed-fu (2.3.6)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
sentry-raven (2.4.0) sentry-raven (2.5.3)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.9.0) sexp_processor (4.9.0)
...@@ -781,7 +777,7 @@ GEM ...@@ -781,7 +777,7 @@ GEM
rack rack
shoulda-matchers (2.8.0) shoulda-matchers (2.8.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
sidekiq (5.0.0) sidekiq (5.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
...@@ -889,7 +885,7 @@ GEM ...@@ -889,7 +885,7 @@ GEM
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
webmock (1.24.6) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
...@@ -928,8 +924,8 @@ DEPENDENCIES ...@@ -928,8 +924,8 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0) brakeman (~> 3.6.0)
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.5.0) bullet (~> 5.5.0)
...@@ -940,7 +936,6 @@ DEPENDENCIES ...@@ -940,7 +936,6 @@ DEPENDENCIES
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0)
concurrent-ruby (~> 1.0.5) concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0) connection_pool (~> 2.0)
creole (~> 0.5.0) creole (~> 0.5.0)
...@@ -963,7 +958,7 @@ DEPENDENCIES ...@@ -963,7 +958,7 @@ DEPENDENCIES
flipper (~> 0.10.2) flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2) flipper-active_record (~> 0.10.2)
fog-aliyun (~> 0.1.0) fog-aliyun (~> 0.1.0)
fog-aws (~> 0.9) fog-aws (~> 1.4)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 0.5)
fog-local (~> 0.3) fog-local (~> 0.3)
...@@ -977,7 +972,7 @@ DEPENDENCIES ...@@ -977,7 +972,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.9.0) gitaly (~> 0.17.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
...@@ -1008,20 +1003,20 @@ DEPENDENCIES ...@@ -1008,20 +1003,20 @@ DEPENDENCIES
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 2.1.0) license_finder (~> 2.1.0)
licensee (~> 8.7.0) licensee (~> 8.7.0)
lograge (~> 0.5)
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.9.1) mail_room (~> 0.9.1)
method_source (~> 0.8) method_source (~> 0.8)
minitest (~> 5.7.0) minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16) mysql2 (~> 0.4.5)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.4.2) omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.0) omniauth-authentiq (~> 0.3.1)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2) omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
...@@ -1040,15 +1035,15 @@ DEPENDENCIES ...@@ -1040,15 +1035,15 @@ DEPENDENCIES
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-host (~> 1.0.0) peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.2.1) peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0) peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0) peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
poltergeist (~> 1.9.0) poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta5) prometheus-client-mmap (~> 0.7.0.beta9)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
...@@ -1059,8 +1054,10 @@ DEPENDENCIES ...@@ -1059,8 +1054,10 @@ DEPENDENCIES
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9) rails-i18n (~> 4.0.9)
rainbow (~> 2.2) rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
re2 (~> 1.0.0)
recaptcha (~> 3.0) recaptcha (~> 3.0)
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
...@@ -1083,10 +1080,10 @@ DEPENDENCIES ...@@ -1083,10 +1080,10 @@ DEPENDENCIES
rugged (~> 0.25.1.1) rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0) scss_lint (~> 0.54.0)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 2.4.0) sentry-raven (~> 2.5.3)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
...@@ -1119,7 +1116,7 @@ DEPENDENCIES ...@@ -1119,7 +1116,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0) version_sorter (~> 2.1.0)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
webmock (~> 1.24.0) webmock (~> 2.3.2)
webpack-rails (~> 0.9.10) webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
## Test coverage ## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby - [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
- [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=rake+karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript - [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript
## Canonical source ## Canonical source
......
9.3.0-pre 9.4.0-pre
This diff is collapsed.
import autosize from 'vendor/autosize'; import autosize from 'vendor/autosize';
$(() => { document.addEventListener('DOMContentLoaded', () => {
const $fields = $('.js-autosize'); const autosizeEls = document.querySelectorAll('.js-autosize');
$fields.on('autosize:resized', function resized() { autosize(autosizeEls);
const $field = $(this); autosize.update(autosizeEls);
$field.data('height', $field.outerHeight());
});
$fields.on('resize.autosize', function resize() {
const $field = $(this);
if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight());
autosize.destroy($field);
$field.css('max-height', window.outerHeight);
}
});
autosize($fields);
autosize.update($fields);
$fields.css('resize', 'vertical');
}); });
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json'; import isEmojiUnicodeSupported from '../emoji/support';
import emojiAliases from 'emojis/aliases.json';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
installCustomElements(window); installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap(); export default function installGlEmojiElement() {
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
function assembleFallbackImageSrc(inputName) {
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
...@@ -90,18 +24,26 @@ function installGlEmojiElement() { ...@@ -90,18 +24,26 @@ function installGlEmojiElement() {
if ( if (
emojiUnicode && emojiUnicode &&
isEmojiUnicode && isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) { ) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = assembleFallbackImageSrc(name); import(/* webpackChunkName: 'emoji' */ '../emoji')
this.innerHTML = emojiImageTag(name, src); .then(({ emojiImageTag, emojiFallbackImageSrc }) => {
if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
})
.catch(() => {
// do nothing
});
} }
} }
}; };
...@@ -110,9 +52,3 @@ function installGlEmojiElement() { ...@@ -110,9 +52,3 @@ function installGlEmojiElement() {
prototype: GlEmojiElementProto, prototype: GlEmojiElementProto,
}); });
} }
export {
installGlEmojiElement,
glEmojiTag,
emojiImageTag,
};
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
function isEmojiNameValid(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
return name && emojiMap[name];
}
export default isEmojiNameValid;
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
function isFlagEmoji(emojiUnicode) {
const cp = emojiUnicode.codePointAt(0);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
function isKeycapEmoji(emojiUnicode) {
return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
}
// Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
// Check for `family_*`, `kiss_*`, `couple_*`
// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
const zwj = 8205; // parseInt('200D', 16)
const personStartCodePoint = 128102; // parseInt('1F466', 16)
const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
} else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
hasPersonEmoji = true;
}
});
return hasPersonEmoji && hasZwj;
}
// Helper so we don't have to run `isFlagEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult
);
}
// Helper so we don't have to run `isSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
!isHorseRacingSkinToneResult
);
}
// Helper so we don't have to run `isPersonZwjEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return (
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
}
// Takes in a support map and determines whether
// the given unicode emoji is supported on the platform.
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
}
export {
isEmojiUnicodeSupported,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
};
import AccessorUtilities from '../../lib/utils/accessor';
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
// woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
// family_mwgb
// Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
// horse_racing_tone5
// Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
'\u{1F575}\u{1F3FF}',
// person_with_ball_tone5
'\u{26F9}\u{1F3FF}',
// angel_tone5
'\u{1F47C}\u{1F3FF}',
],
// rofl, http://emojipedia.org/unicode-9.0/
'9.0': '\u{1F923}',
// metal, http://emojipedia.org/unicode-8.0/
'8.0': '\u{1F918}',
// spy, http://emojipedia.org/unicode-7.0/
'7.0': '\u{1F575}',
// expressionless, http://emojipedia.org/unicode-6.1/
6.1: '\u{1F611}',
// japanese_goblin, http://emojipedia.org/unicode-6.0/
'6.0': '\u{1F47A}',
// sailboat, http://emojipedia.org/unicode-5.2/
5.2: '\u{26F5}',
// mahjong, http://emojipedia.org/unicode-5.1/
5.1: '\u{1F004}',
// gear, http://emojipedia.org/unicode-4.1/
4.1: '\u{2699}',
// zap, http://emojipedia.org/unicode-4.0/
'4.0': '\u{26A1}',
// recycle, http://emojipedia.org/unicode-3.2/
3.2: '\u{267B}',
// information_source, http://emojipedia.org/unicode-3.0/
'3.0': '\u{2139}',
// heart, http://emojipedia.org/unicode-1.1/
1.1: '\u{2764}',
};
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
// Check for some sort of color other than black
if (hasColor && isVisible) {
return true;
}
return false;
}
const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
const isChrome = chromeMatches && chromeMatches.length > 0;
const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize);
canvas.height = (numTestEntries * fontSize);
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
writeIndex += 1;
});
});
// Read from the canvas
const resultMap = {};
let readIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
// Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData(
0,
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
}
}
readIndex += 1;
return isSatisfied && isValidEmoji;
}, true);
resultMap[testKey] = isTestSatisfied;
});
resultMap.meta = {
isChrome,
chromeVersion,
};
return resultMap;
}
function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
}
return unicodeSupportMap;
}
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import './details_behavior'; import './details_behavior';
import { installGlEmojiElement } from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
import './requires_input'; import './requires_input';
import './toggler_behavior'; import './toggler_behavior';
......
...@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { ...@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
e.preventDefault(); e.preventDefault();
const $form = $(e.target).closest('form'); const $form = $(e.target).closest('form');
const $submitButton = $form.find('input[type=submit], button[type=submit]'); const $submitButton = $form.find('input[type=submit], button[type=submit]').first();
if (!$submitButton.attr('disabled')) { if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]); $submitButton.trigger('click', [e]);
......
...@@ -51,8 +51,9 @@ export default () => { ...@@ -51,8 +51,9 @@ export default () => {
methods: { methods: {
loadFile() { loadFile() {
this.$http.get(el.dataset.endpoint) this.$http.get(el.dataset.endpoint)
.then(response => response.json())
.then((res) => { .then((res) => {
this.json = res.json(); this.json = res;
this.loading = false; this.loading = false;
}) })
.catch((e) => { .catch((e) => {
......
...@@ -81,8 +81,9 @@ $(() => { ...@@ -81,8 +81,9 @@ $(() => {
mounted () { mounted () {
Store.disabled = this.disabled; Store.disabled = this.disabled;
gl.boardService.all() gl.boardService.all()
.then(response => response.json())
.then((resp) => { .then((resp) => {
resp.json().forEach((board) => { resp.forEach((board) => {
const list = Store.addList(board, this.defaultAvatar); const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') { if (list.type === 'closed') {
...@@ -97,7 +98,8 @@ $(() => { ...@@ -97,7 +98,8 @@ $(() => {
Store.addBlankState(); Store.addBlankState();
this.loading = false; this.loading = false;
}).catch(() => new Flash('An error occurred. Please try again.')); })
.catch(() => new Flash('An error occurred. Please try again.'));
}, },
methods: { methods: {
updateTokens() { updateTokens() {
......
...@@ -64,8 +64,9 @@ export default { ...@@ -64,8 +64,9 @@ export default {
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService.generateDefaultLists()
.then((resp) => { .then(resp => resp.json())
resp.json().forEach((listObj) => { .then((data) => {
data.forEach((listObj) => {
const list = Store.findList('title', listObj.title); const list = Store.findList('title', listObj.title);
list.id = listObj.id; list.id = listObj.id;
......
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
methods: { methods: {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
if (this.title.trim() === '') return; if (this.title.trim() === '') return Promise.resolve();
this.error = false; this.error = false;
...@@ -29,7 +29,10 @@ export default { ...@@ -29,7 +29,10 @@ export default {
assignees: [], assignees: [],
}); });
this.list.newIssue(issue) eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
return this.list.newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
...@@ -47,9 +50,6 @@ export default { ...@@ -47,9 +50,6 @@ export default {
// Show error message // Show error message
this.error = true; this.error = true;
}); });
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
......
...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}, },
milestoneTitle() { milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
} },
canRemove() {
return !this.list.preset;
},
}, },
watch: { watch: {
detail: { detail: {
......
...@@ -88,9 +88,9 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -88,9 +88,9 @@ gl.issueBoards.IssuesModal = Vue.extend({
return gl.boardService.getBacklog(queryData(this.filter.path, { return gl.boardService.getBacklog(queryData(this.filter.path, {
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
})).then((res) => { }))
const data = res.json(); .then(resp => resp.json())
.then((data) => {
if (clearIssues) { if (clearIssues) {
this.issues = []; this.issues = [];
} }
......
...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
template: ` template: `
<div <div
class="block list" class="block list">
v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
...@@ -40,9 +40,8 @@ class List { ...@@ -40,9 +40,8 @@ class List {
save () { save () {
return gl.boardService.createList(this.label.id) return gl.boardService.createList(this.label.id)
.then((resp) => { .then(resp => resp.json())
const data = resp.json(); .then((data) => {
this.id = data.id; this.id = data.id;
this.type = data.list_type; this.type = data.list_type;
this.position = data.position; this.position = data.position;
...@@ -91,8 +90,8 @@ class List { ...@@ -91,8 +90,8 @@ class List {
} }
return gl.boardService.getIssuesForList(this.id, data) return gl.boardService.getIssuesForList(this.id, data)
.then((resp) => { .then(resp => resp.json())
const data = resp.json(); .then((data) => {
this.loading = false; this.loading = false;
this.issuesSize = data.size; this.issuesSize = data.size;
...@@ -109,11 +108,10 @@ class List { ...@@ -109,11 +108,10 @@ class List {
this.issuesSize += 1; this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue) return gl.boardService.newIssue(this.id, issue)
.then((resp) => { .then(resp => resp.json())
const data = resp.json(); .then((data) => {
issue.id = data.iid; issue.id = data.iid;
})
.then(() => {
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id; const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
......
...@@ -23,11 +23,6 @@ class BoardService { ...@@ -23,11 +23,6 @@ class BoardService {
url: bulkUpdatePath, url: bulkUpdatePath,
}, },
}); });
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
next();
});
} }
all () { all () {
......
...@@ -13,25 +13,21 @@ window.Build = (function () { ...@@ -13,25 +13,21 @@ window.Build = (function () {
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl; this.pageUrl = this.options.pageUrl;
this.buildUrl = this.options.buildUrl;
this.buildStatus = this.options.buildStatus; this.buildStatus = this.options.buildStatus;
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.logBytes = 0; this.logBytes = 0;
this.scrollOffsetPadding = 30;
this.hasBeenScrolled = false; this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this); this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this); this.getBuildTrace = this.getBuildTrace.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.$body = $('body');
this.$buildTrace = $('#build-trace'); this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
this.$truncatedInfo = $('.js-truncated-info'); this.$truncatedInfo = $('.js-truncated-info');
this.$buildTraceOutput = $('.js-build-output'); this.$buildTraceOutput = $('.js-build-output');
this.$scrollContainer = $('.js-scroll-container'); this.$topBar = $('.js-top-bar');
// Scroll controllers // Scroll controllers
this.$scrollTopBtn = $('.js-scroll-up'); this.$scrollTopBtn = $('.js-scroll-up');
...@@ -63,13 +59,22 @@ window.Build = (function () { ...@@ -63,13 +59,22 @@ window.Build = (function () {
.off('click') .off('click')
.on('click', this.scrollToBottom.bind(this)); .on('click', this.scrollToBottom.bind(this));
const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$scrollContainer $(window)
.off('scroll') .off('scroll')
.on('scroll', () => { .on('scroll', () => {
this.hasBeenScrolled = true; const contentHeight = this.$buildTraceOutput.prop('scrollHeight');
scrollThrottled(); if (contentHeight > this.windowSize) {
// means the user did not scroll, the content was updated.
this.windowSize = contentHeight;
} else {
// User scrolled
this.hasBeenScrolled = true;
this.toggleScrollAnimation(false);
}
this.scrollThrottled();
}); });
$(window) $(window)
...@@ -77,60 +82,73 @@ window.Build = (function () { ...@@ -77,60 +82,73 @@ window.Build = (function () {
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.updateArtifactRemoveDate(); this.updateArtifactRemoveDate();
this.initAffixTopArea();
// eslint-disable-next-line this.getBuildTrace();
this.getBuildTrace()
.then(() => this.toggleScroll())
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
this.verifyTopPosition();
} }
Build.prototype.initAffixTopArea = function () {
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
then we default back to Bootstraps affix
**/
if (this.$topBar.css('position') !== 'static') return;
const offsetTop = this.$buildTrace.offset().top;
this.$topBar.affix({
offset: {
top: offsetTop,
},
});
};
Build.prototype.canScroll = function () { Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); return document.body.scrollHeight > window.innerHeight;
}; };
/**
* | | Up | Down |
* |--------------------------|----------|----------|
* | on scroll bottom | active | disabled |
* | on scroll top | disabled | active |
* | no scroll | disabled | disabled |
* | on.('scroll') is on top | disabled | active |
* | on('scroll) is on bottom | active | disabled |
*
*/
Build.prototype.toggleScroll = function () { Build.prototype.toggleScroll = function () {
const currentPosition = this.$scrollContainer.scrollTop(); const currentPosition = document.body.scrollTop;
const bottomScroll = currentPosition + this.$scrollContainer.innerHeight(); const windowHeight = window.innerHeight;
if (this.canScroll()) { if (this.canScroll()) {
if (currentPosition === 0) { if (currentPosition > 0 &&
(document.body.scrollHeight - currentPosition !== windowHeight)) {
// User is in the middle of the log
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Build Log
this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { } else if (document.body.scrollHeight - currentPosition === windowHeight) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, true);
} else {
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} }
} else {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
} }
}; };
Build.prototype.scrollToTop = function () { Build.prototype.scrollDown = function () {
document.body.scrollTop = document.body.scrollHeight;
};
Build.prototype.scrollToBottom = function () {
this.scrollDown();
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(0);
this.toggleScroll(); this.toggleScroll();
}; };
Build.prototype.scrollToBottom = function () { Build.prototype.scrollToTop = function () {
document.body.scrollTop = 0;
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
this.toggleScroll(); this.toggleScroll();
}; };
...@@ -143,47 +161,6 @@ window.Build = (function () { ...@@ -143,47 +161,6 @@ window.Build = (function () {
this.$scrollBottomBtn.toggleClass('animate', toggle); this.$scrollBottomBtn.toggleClass('animate', toggle);
}; };
/**
* Build trace top position depends on the space ocupied by the elments rendered before
*/
Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page');
const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage);
const prependTopDefault = 20;
// header + navigation + margin
let topPostion = 168;
if ($header.length) {
topPostion += $header.outerHeight();
}
if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight();
}
if ($startsEnvironment.length) {
topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
}
if ($erased.length) {
topPostion += $erased.outerHeight() + prependTopDefault;
}
if ($flashError.length) {
topPostion += $flashError.outerHeight();
}
this.$buildTrace.css({
top: topPostion,
});
};
Build.prototype.initSidebar = function () { Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar'); this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll(); this.$sidebar.niceScroll();
...@@ -196,10 +173,13 @@ window.Build = (function () { ...@@ -196,10 +173,13 @@ window.Build = (function () {
}) })
.done((log) => { .done((log) => {
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
} }
this.windowSize = this.$buildTraceOutput.prop('scrollHeight');
if (log.append) { if (log.append) {
this.$buildTraceOutput.append(log.html); this.$buildTraceOutput.append(log.html);
this.logBytes += log.size; this.logBytes += log.size;
...@@ -220,16 +200,14 @@ window.Build = (function () { ...@@ -220,16 +200,14 @@ window.Build = (function () {
} }
if (!log.complete) { if (!log.complete) {
this.toggleScrollAnimation(true); if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
Build.timeout = setTimeout(() => { Build.timeout = setTimeout(() => {
//eslint-disable-next-line this.getBuildTrace();
this.getBuildTrace()
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
});
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
...@@ -242,7 +220,13 @@ window.Build = (function () { ...@@ -242,7 +220,13 @@ window.Build = (function () {
}) })
.fail(() => { .fail(() => {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
}); })
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollDown();
}
})
.then(() => this.toggleScroll());
}; };
Build.prototype.shouldHideSidebarForViewport = function () { Build.prototype.shouldHideSidebarForViewport = function () {
...@@ -254,14 +238,11 @@ window.Build = (function () { ...@@ -254,14 +238,11 @@ window.Build = (function () {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header'); const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide); .toggleClass('right-sidebar-collapsed', shouldHide);
$('.js-build-page') this.$topBar
.toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide); .toggleClass('sidebar-collapsed', shouldHide);
...@@ -274,17 +255,10 @@ window.Build = (function () { ...@@ -274,17 +255,10 @@ window.Build = (function () {
Build.prototype.sidebarOnResize = function () { Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport()); this.toggleSidebar(this.shouldHideSidebarForViewport());
this.verifyTopPosition();
if (this.canScroll()) {
this.toggleScroll();
}
}; };
Build.prototype.sidebarOnClick = function () { Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
this.verifyTopPosition();
}; };
Build.prototype.updateArtifactRemoveDate = function () { Build.prototype.updateArtifactRemoveDate = function () {
......
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CloseReopenReportToggle {
constructor(opts = {}) {
this.dropdownTrigger = opts.dropdownTrigger;
this.dropdownList = opts.dropdownList;
this.button = opts.button;
}
initDroplab() {
this.reopenItem = this.dropdownList.querySelector('.reopen-item');
this.closeItem = this.dropdownList.querySelector('.close-item');
this.droplab = new DropLab();
const config = this.setConfig();
this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config);
}
updateButton(isClosed) {
this.toggleButtonType(isClosed);
this.button.blur();
}
toggleButtonType(isClosed) {
const [showItem, hideItem] = this.getButtonTypes(isClosed);
showItem.classList.remove('hidden');
showItem.classList.add('droplab-item-selected');
hideItem.classList.add('hidden');
hideItem.classList.remove('droplab-item-selected');
showItem.click();
}
getButtonTypes(isClosed) {
return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem];
}
setDisable(shouldDisable = true) {
if (shouldDisable) {
this.button.setAttribute('disabled', 'true');
this.dropdownTrigger.setAttribute('disabled', 'true');
} else {
this.button.removeAttribute('disabled');
this.dropdownTrigger.removeAttribute('disabled');
}
}
setConfig() {
const config = {
InputSetter: [
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: this.button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: this.dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: this.button,
valueAttribute: 'data-url',
inputAttribute: 'href',
},
{
input: this.button,
valueAttribute: 'data-method',
inputAttribute: 'data-method',
},
],
};
return config;
}
}
export default CloseReopenReportToggle;
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import InputSetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CommentTypeToggle { class CommentTypeToggle {
constructor(opts = {}) { constructor(opts = {}) {
......
...@@ -18,13 +18,26 @@ window.gl.CommitPipelinesTable = CommitPipelinesTable; ...@@ -18,13 +18,26 @@ window.gl.CommitPipelinesTable = CommitPipelinesTable;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl) {
const table = new CommitPipelinesTable({ // Update MR and Commits tabs
propsData: { pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => {
endpoint: pipelineTableViewEl.dataset.endpoint, if (event.detail.pipelines &&
helpPagePath: pipelineTableViewEl.dataset.helpPagePath, event.detail.pipelines.count &&
}, event.detail.pipelines.count.all) {
}).$mount(); const badge = document.querySelector('.js-pipelines-mr-count');
pipelineTableViewEl.appendChild(table.$el);
badge.textContent = event.detail.pipelines.count.all;
}
});
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
const table = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
}
} }
}); });
...@@ -51,11 +51,22 @@ ...@@ -51,11 +51,22 @@
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
const response = resp.json(); return resp.json().then((response) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.setCommonData(pipelines);
// depending of the endpoint the response can either bring a `pipelines` key or not. const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
const pipelines = response.pipelines || response; detail: {
this.setCommonData(pipelines); pipelines: response,
},
});
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
});
}, },
}, },
}; };
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -8,8 +10,14 @@ let isBound = false; ...@@ -8,8 +10,14 @@ let isBound = false;
class Diff { class Diff {
constructor() { constructor() {
const $diffFile = $('.files .diff-file'); const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff();
$diffFile.filesCommentButton(); $diffFile.each((index, file) => {
if (!$.data(file, 'singleFileDiff')) {
$.data(file, 'singleFileDiff', new SingleFileDiff(file));
}
});
FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file)); $diffFile.each((index, file) => new gl.ImageFile(file));
......
...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount; const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container') $(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0) .toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container') .nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0); .toggleClass('no-comment-btn', notesCount > 0);
}, },
toggleDiscussionsToggleState() { toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
......
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ /* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */ /* global CommentsStore */
/* global ResolveService */ /* global ResolveService */
/* global Flash */ /* global Flash */
...@@ -64,8 +64,6 @@ const ResolveBtn = Vue.extend({ ...@@ -64,8 +64,6 @@ const ResolveBtn = Vue.extend({
}); });
}, },
resolve: function () { resolve: function () {
const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.';
if (!this.canResolve) return; if (!this.canResolve) return;
let promise; let promise;
...@@ -79,24 +77,20 @@ const ResolveBtn = Vue.extend({ ...@@ -79,24 +77,20 @@ const ResolveBtn = Vue.extend({
.resolve(this.noteId); .resolve(this.noteId);
} }
promise.then((response) => { promise
this.loading = false; .then(resp => resp.json())
.then((data) => {
this.loading = false;
if (response.status === 200) {
const data = response.json();
const resolved_by = data ? data.resolved_by : null; const resolved_by = data ? data.resolved_by : null;
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data); this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus(); gl.mrWidget.checkStatus();
} else {
new Flash(errorFlashMsg);
}
this.updateTooltip(); this.updateTooltip();
}).catch(() => { })
new Flash(errorFlashMsg); .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.'));
});
} }
}, },
mounted: function () { mounted: function () {
......
/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
/* global Flash */ /* global Flash */
/* global CommentsStore */ /* global CommentsStore */
...@@ -32,27 +31,22 @@ class ResolveServiceClass { ...@@ -32,27 +31,22 @@ class ResolveServiceClass {
promise = this.resolveAll(mergeRequestId, discussionId); promise = this.resolveAll(mergeRequestId, discussionId);
} }
promise.then((response) => { promise
discussion.loading = false; .then(resp => resp.json())
.then((data) => {
if (response.status === 200) { discussion.loading = false;
const data = response.json(); const resolvedBy = data ? data.resolved_by : null;
const resolved_by = data ? data.resolved_by : null;
if (isResolved) { if (isResolved) {
discussion.unResolveAllNotes(); discussion.unResolveAllNotes();
} else { } else {
discussion.resolveAllNotes(resolved_by); discussion.resolveAllNotes(resolvedBy);
} }
gl.mrWidget.checkStatus(); gl.mrWidget.checkStatus();
discussion.updateHeadline(data); discussion.updateHeadline(data);
} else { })
throw new Error('An error occurred when trying to resolve discussion.'); .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
}
}).catch(() => {
new Flash('An error occurred when trying to resolve a discussion. Please try again.');
});
} }
resolveAll(mergeRequestId, discussionId) { resolveAll(mergeRequestId, discussionId) {
...@@ -62,7 +56,7 @@ class ResolveServiceClass { ...@@ -62,7 +56,7 @@ class ResolveServiceClass {
return this.discussionResource.save({ return this.discussionResource.save({
mergeRequestId, mergeRequestId,
discussionId discussionId,
}, {}); }, {});
} }
...@@ -73,7 +67,7 @@ class ResolveServiceClass { ...@@ -73,7 +67,7 @@ class ResolveServiceClass {
return this.discussionResource.delete({ return this.discussionResource.delete({
mergeRequestId, mergeRequestId,
discussionId discussionId,
}, {}); }, {});
} }
} }
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */ /* global ProjectSelect */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global IssuableIndex */ /* global IssuableIndex */
/* global ShortcutsIssuable */ /* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */ /* global Milestone */
/* global IssuableForm */ /* global IssuableForm */
/* global LabelsSelect */ /* global LabelsSelect */
...@@ -24,7 +22,6 @@ ...@@ -24,7 +22,6 @@
/* global ProjectAvatar */ /* global ProjectAvatar */
/* global CompareAutocomplete */ /* global CompareAutocomplete */
/* global ProjectNew */ /* global ProjectNew */
/* global Star */
/* global ProjectShow */ /* global ProjectShow */
/* global Labels */ /* global Labels */
/* global Shortcuts */ /* global Shortcuts */
...@@ -53,8 +50,20 @@ import UsersSelect from './users_select'; ...@@ -53,8 +50,20 @@ import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown'; import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob'; import ShortcutsBlob from './shortcuts_blob';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import Star from './star';
import Todos from './todos';
import TreeView from './tree';
import UsagePing from './usage_ping';
import UsernameValidator from './username_validator';
import VersionCheckImage from './version_check_image';
import Wikis from './wikis';
import ZenMode from './zen_mode';
import initSettingsPanels from './settings_panels'; import initSettingsPanels from './settings_panels';
import ScrollHelper from './helpers/scroll_helper'; import ScrollHelper from './helpers/scroll_helper';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -123,9 +132,13 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -123,9 +132,13 @@ import ScrollHelper from './helpers/scroll_helper';
} }
switch (page) { switch (page) {
case 'profiles:preferences:show':
initExperimentalFlags();
break;
case 'sessions:new': case 'sessions:new':
new UsernameValidator(); new UsernameValidator();
new ActiveTabMemoizer(); new SigninTabsMemoizer();
new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents();
break; break;
case 'projects:boards:show': case 'projects:boards:show':
case 'projects:boards:index': case 'projects:boards:index':
...@@ -149,6 +162,9 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -149,6 +162,9 @@ import ScrollHelper from './helpers/scroll_helper';
shortcut_handler = new ShortcutsIssuable(); shortcut_handler = new ShortcutsIssuable();
new ZenMode(); new ZenMode();
break; break;
case 'dashboard:milestones:index':
new ProjectSelect();
break;
case 'projects:milestones:show': case 'projects:milestones:show':
case 'groups:milestones:show': case 'groups:milestones:show':
case 'dashboard:milestones:show': case 'dashboard:milestones:show':
...@@ -158,9 +174,10 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -158,9 +174,10 @@ import ScrollHelper from './helpers/scroll_helper';
case 'groups:issues': case 'groups:issues':
case 'groups:merge_requests': case 'groups:merge_requests':
new UsersSelect(); new UsersSelect();
new ProjectSelect();
break; break;
case 'dashboard:todos:index': case 'dashboard:todos:index':
new gl.Todos(); new Todos();
break; break;
case 'dashboard:projects:index': case 'dashboard:projects:index':
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
...@@ -208,8 +225,8 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -208,8 +225,8 @@ import ScrollHelper from './helpers/scroll_helper';
new MilestoneSelect(); new MilestoneSelect();
new gl.IssuableTemplateSelectors(); new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:creations:new':
case 'projects:merge_requests:new_diffs': case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new gl.Diff(); new gl.Diff();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -246,15 +263,12 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -246,15 +263,12 @@ import ScrollHelper from './helpers/scroll_helper';
shortcut_handler = new ShortcutsIssuable(true); shortcut_handler = new ShortcutsIssuable(true);
new ZenMode(); new ZenMode();
break; break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
break;
case 'dashboard:activity': case 'dashboard:activity':
new gl.Activities(); new gl.Activities();
break; break;
case 'dashboard:issues': case 'dashboard:issues':
case 'dashboard:merge_requests': case 'dashboard:merge_requests':
new ProjectSelect();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
...@@ -312,7 +326,7 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -312,7 +326,7 @@ import ScrollHelper from './helpers/scroll_helper';
new gl.Members(); new gl.Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:members:show': case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); new GroupsSelect();
new gl.MemberExpirationDate(); new gl.MemberExpirationDate();
...@@ -365,12 +379,12 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -365,12 +379,12 @@ import ScrollHelper from './helpers/scroll_helper';
new BlobViewer(); new BlobViewer();
break; break;
case 'help:index': case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break; break;
case 'search:show': case 'search:show':
new Search(); new Search();
break; break;
case 'projects:repository:show': case 'projects:settings:repository:show':
// Initialize Protected Branch Settings // Initialize Protected Branch Settings
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
...@@ -380,7 +394,8 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -380,7 +394,8 @@ import ScrollHelper from './helpers/scroll_helper';
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
break; break;
case 'projects:ci_cd:show': case 'projects:settings:ci_cd:show':
case 'groups:settings:ci_cd:show':
new gl.ProjectVariables(); new gl.ProjectVariables();
break; break;
case 'ci:lints:create': case 'ci:lints:create':
...@@ -417,7 +432,7 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -417,7 +432,7 @@ import ScrollHelper from './helpers/scroll_helper';
new Admin(); new Admin();
switch (path[1]) { switch (path[1]) {
case 'cohorts': case 'cohorts':
new gl.UsagePing(); new UsagePing();
break; break;
case 'groups': case 'groups':
new UsersSelect(); new UsersSelect();
...@@ -469,7 +484,7 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -469,7 +484,7 @@ import ScrollHelper from './helpers/scroll_helper';
new NotificationsDropdown(); new NotificationsDropdown();
break; break;
case 'wikis': case 'wikis':
new gl.Wikis(); new Wikis();
shortcut_handler = new ShortcutsWiki(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form'), true); new gl.GLForm($('.wiki-form'), true);
...@@ -501,6 +516,10 @@ import ScrollHelper from './helpers/scroll_helper'; ...@@ -501,6 +516,10 @@ import ScrollHelper from './helpers/scroll_helper';
if (!shortcut_handler) { if (!shortcut_handler) {
new Shortcuts(); new Shortcuts();
} }
if (document.querySelector('#peek')) {
new PerformanceBar({ container: '#peek' });
}
}; };
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
......
...@@ -5,21 +5,28 @@ import './preview_markdown'; ...@@ -5,21 +5,28 @@ import './preview_markdown';
window.DropzoneInput = (function() { window.DropzoneInput = (function() {
function DropzoneInput(form) { function DropzoneInput(form) {
var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile, addFileToForm;
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
divHover = '<div class="div-dropzone-hover"></div>'; const divHover = '<div class="div-dropzone-hover"></div>';
iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
$attachButton = form.find('.button-attach-file'); const $attachButton = form.find('.button-attach-file');
$attachingFileMessage = form.find('.attaching-file-message'); const $attachingFileMessage = form.find('.attaching-file-message');
$cancelButton = form.find('.button-cancel-uploading-files'); const $cancelButton = form.find('.button-cancel-uploading-files');
$retryLink = form.find('.retry-uploading-link'); const $retryLink = form.find('.retry-uploading-link');
$uploadProgress = form.find('.uploading-progress'); const $uploadProgress = form.find('.uploading-progress');
$uploadingErrorContainer = form.find('.uploading-error-container'); const $uploadingErrorContainer = form.find('.uploading-error-container');
$uploadingErrorMessage = form.find('.uploading-error-message'); const $uploadingErrorMessage = form.find('.uploading-error-message');
$uploadingProgressContainer = form.find('.uploading-progress-container'); const $uploadingProgressContainer = form.find('.uploading-progress-container');
uploadsPath = window.uploads_path || null; const uploadsPath = window.uploads_path || null;
maxFileSize = gon.max_file_size || 10; const maxFileSize = gon.max_file_size || 10;
formTextarea = form.find('.js-gfm-input'); const formTextarea = form.find('.js-gfm-input');
let handlePaste;
let pasteText;
let addFileToForm;
let updateAttachingMessage;
let isImage;
let getFilename;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>'); formTextarea.wrap('<div class="div-dropzone"></div>');
formTextarea.on('paste', (function(_this) { formTextarea.on('paste', (function(_this) {
return function(event) { return function(event) {
...@@ -28,16 +35,16 @@ window.DropzoneInput = (function() { ...@@ -28,16 +35,16 @@ window.DropzoneInput = (function() {
})(this)); })(this));
// Add dropzone area to the form. // Add dropzone area to the form.
$mdArea = formTextarea.closest('.md-area'); const $mdArea = formTextarea.closest('.md-area');
form.setupMarkdownPreview(); form.setupMarkdownPreview();
$formDropzone = form.find('.div-dropzone'); const $formDropzone = form.find('.div-dropzone');
$formDropzone.parent().addClass('div-dropzone-wrapper'); $formDropzone.parent().addClass('div-dropzone-wrapper');
$formDropzone.append(divHover); $formDropzone.append(divHover);
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip); $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
if (!uploadsPath) return; if (!uploadsPath) return;
dropzone = $formDropzone.dropzone({ const dropzone = $formDropzone.dropzone({
url: uploadsPath, url: uploadsPath,
dictDefaultMessage: '', dictDefaultMessage: '',
clickable: true, clickable: true,
...@@ -117,7 +124,7 @@ window.DropzoneInput = (function() { ...@@ -117,7 +124,7 @@ window.DropzoneInput = (function() {
} }
}); });
child = $(dropzone[0]).children('textarea'); const child = $(dropzone[0]).children('textarea');
// removeAllFiles(true) stops uploading files (if any) // removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue. // and remove them from dropzone files queue.
...@@ -214,6 +221,35 @@ window.DropzoneInput = (function() { ...@@ -214,6 +221,35 @@ window.DropzoneInput = (function() {
return value.first(); return value.first();
}; };
const showSpinner = function(e) {
return $uploadingProgressContainer.removeClass('hide');
};
const closeSpinner = function() {
return $uploadingProgressContainer.addClass('hide');
};
const showError = function(message) {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
const closeAlertMessage = function() {
return form.find('.div-dropzone-alert').alert('close');
};
const insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
return val.replace(`{{${filename}}}`, url);
});
};
const appendToTextArea = function(url) {
return $(child).val(function(index, val) {
return val + url + "\n";
});
};
uploadFile = function(item, filename) { uploadFile = function(item, filename) {
var formData; var formData;
formData = new FormData(); formData = new FormData();
...@@ -262,35 +298,6 @@ window.DropzoneInput = (function() { ...@@ -262,35 +298,6 @@ window.DropzoneInput = (function() {
messageContainer.text(attachingMessage); messageContainer.text(attachingMessage);
}; };
insertToTextArea = function(filename, url) {
return $(child).val(function(index, val) {
return val.replace(`{{${filename}}}`, url);
});
};
appendToTextArea = function(url) {
return $(child).val(function(index, val) {
return val + url + "\n";
});
};
showSpinner = function(e) {
return $uploadingProgressContainer.removeClass('hide');
};
closeSpinner = function() {
return $uploadingProgressContainer.addClass('hide');
};
showError = function(message) {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
closeAlertMessage = function() {
return form.find('.div-dropzone-alert').alert('close');
};
form.find('.markdown-selector').click(function(e) { form.find('.markdown-selector').click(function(e) {
e.preventDefault(); e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click(); $(this).closest('.gfm-form').find('.div-dropzone').click();
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
/* global dateFormat */ /* global dateFormat */
/* global Pikaday */ /* global Pikaday */
import DateFix from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
constructor({ $dropdown, $loading } = {}) { constructor({ $dropdown, $loading } = {}) {
const $dropdownParent = $dropdown.closest('.dropdown'); const $dropdownParent = $dropdown.closest('.dropdown');
...@@ -43,14 +45,13 @@ class DueDateSelect { ...@@ -43,14 +45,13 @@ class DueDateSelect {
initDatePicker() { initDatePicker() {
const $dueDateInput = $(`input[name='${this.fieldName}']`); const $dueDateInput = $(`input[name='${this.fieldName}']`);
const dateFix = DateFix.dashedFix($dueDateInput.val());
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $dueDateInput.get(0), field: $dueDateInput.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
onSelect: (dateText) => { onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
$dueDateInput.val(formattedDate); $dueDateInput.val(formattedDate);
if (this.$dropdown.hasClass('js-issue-boards-due-date')) { if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
...@@ -62,7 +63,7 @@ class DueDateSelect { ...@@ -62,7 +63,7 @@ class DueDateSelect {
} }
}); });
calendar.setDate(new Date($dueDateInput.val())); calendar.setDate(dateFix);
this.$datePicker.append(calendar.el); this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar); this.$datePicker.data('pikaday', calendar);
} }
...@@ -168,6 +169,7 @@ class DueDateSelectors { ...@@ -168,6 +169,7 @@ class DueDateSelectors {
initMilestoneDatePicker() { initMilestoneDatePicker() {
$('.datepicker').each(function() { $('.datepicker').each(function() {
const $datePicker = $(this); const $datePicker = $(this);
const dateFix = DateFix.dashedFix($datePicker.val());
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $datePicker.get(0), field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker', theme: 'gitlab-theme animate-picker',
...@@ -177,7 +179,8 @@ class DueDateSelectors { ...@@ -177,7 +179,8 @@ class DueDateSelectors {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
}); });
calendar.setDate(new Date($datePicker.val()));
calendar.setDate(dateFix);
$datePicker.data('pikaday', calendar); $datePicker.data('pikaday', calendar);
}); });
......
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
export function filterEmojiNames(filter) {
const match = filter.toLowerCase();
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
}
export function filterEmojiNamesByAlias(filter) {
return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap) {
emojiCategoryMap = {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
};
Object.keys(emojiMap).forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
}
});
}
return emojiCategoryMap;
}
export function getEmojiInfo(query) {
let name = normalizeEmojiName(query);
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
return { ...emojiInfo, name };
}
export function emojiFallbackImageSrc(inputName) {
const { name, digest } = getEmojiInfo(inputName);
return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`;
}
export function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
export function glEmojiTag(inputName, options) {
const opts = { sprite: false, forceFallback: false, ...options };
const { name, ...emojiInfo } = getEmojiInfo(inputName);
const fallbackImageSrc = emojiFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
import isEmojiUnicodeSupported from './is_emoji_unicode_supported';
import getUnicodeSupportMap from './unicode_support_map';
// cache browser support map between calls
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
function isFlagEmoji(emojiUnicode) {
const cp = emojiUnicode.codePointAt(0);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
function isKeycapEmoji(emojiUnicode) {
return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
}
// Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
// Check for `family_*`, `kiss_*`, `couple_*`
// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
const zwj = 8205; // parseInt('200D', 16)
const personStartCodePoint = 128102; // parseInt('1F466', 16)
const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
} else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
hasPersonEmoji = true;
}
});
return hasPersonEmoji && hasZwj;
}
// Helper so we don't have to run `isFlagEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult
);
}
// Helper so we don't have to run `isSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
!isHorseRacingSkinToneResult
);
}
// Helper so we don't have to run `isPersonZwjEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return (
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
}
// Takes in a support map and determines whether
// the given unicode emoji is supported on the platform.
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
}
export {
isEmojiUnicodeSupported as default,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
};
import AccessorUtilities from '../../lib/utils/accessor';
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
// woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
// family_mwgb
// Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
// horse_racing_tone5
// Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
'\u{1F575}\u{1F3FF}',
// person_with_ball_tone5
'\u{26F9}\u{1F3FF}',
// angel_tone5
'\u{1F47C}\u{1F3FF}',
],
// rofl, http://emojipedia.org/unicode-9.0/
'9.0': '\u{1F923}',
// metal, http://emojipedia.org/unicode-8.0/
'8.0': '\u{1F918}',
// spy, http://emojipedia.org/unicode-7.0/
'7.0': '\u{1F575}',
// expressionless, http://emojipedia.org/unicode-6.1/
6.1: '\u{1F611}',
// japanese_goblin, http://emojipedia.org/unicode-6.0/
'6.0': '\u{1F47A}',
// sailboat, http://emojipedia.org/unicode-5.2/
5.2: '\u{26F5}',
// mahjong, http://emojipedia.org/unicode-5.1/
5.1: '\u{1F004}',
// gear, http://emojipedia.org/unicode-4.1/
4.1: '\u{2699}',
// zap, http://emojipedia.org/unicode-4.0/
'4.0': '\u{26A1}',
// recycle, http://emojipedia.org/unicode-3.2/
3.2: '\u{267B}',
// information_source, http://emojipedia.org/unicode-3.0/
'3.0': '\u{2139}',
// heart, http://emojipedia.org/unicode-1.1/
1.1: '\u{2764}',
};
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
// Check for some sort of color other than black
if (hasColor && isVisible) {
return true;
}
return false;
}
const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
const isChrome = chromeMatches && chromeMatches.length > 0;
const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize);
canvas.height = (numTestEntries * fontSize);
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
writeIndex += 1;
});
});
// Read from the canvas
const resultMap = {};
let readIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
// Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData(
0,
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
}
}
readIndex += 1;
return isSatisfied && isValidEmoji;
}, true);
resultMap[testKey] = isTestSatisfied;
});
resultMap.meta = {
isChrome,
chromeVersion,
};
return resultMap;
}
export default function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
}
return unicodeSupportMap;
}
...@@ -32,7 +32,6 @@ export default { ...@@ -32,7 +32,6 @@ export default {
state: store.state, state: store.state,
visibility: 'available', visibility: 'available',
isLoading: false, isLoading: false,
isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint, endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment, canCreateDeployment: environmentsData.canCreateDeployment,
...@@ -86,9 +85,6 @@ export default { ...@@ -86,9 +85,6 @@ export default {
errorCallback: this.errorCallback, errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => { notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
// We need to verify if any folder is open to also fecth it
this.openFolders = this.store.getOpenFolders();
}, },
}); });
...@@ -119,7 +115,7 @@ export default { ...@@ -119,7 +115,7 @@ export default {
this.store.toggleFolder(folder); this.store.toggleFolder(folder);
if (!folder.isOpen) { if (!folder.isOpen) {
this.fetchChildEnvironments(folder, folderUrl); this.fetchChildEnvironments(folder, folderUrl, true);
} }
}, },
...@@ -147,19 +143,17 @@ export default { ...@@ -147,19 +143,17 @@ export default {
.catch(this.errorCallback); .catch(this.errorCallback);
}, },
fetchChildEnvironments(folder, folderUrl) { fetchChildEnvironments(folder, folderUrl, showLoader = false) {
this.isLoadingFolderContent = true; this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folderUrl) this.service.getFolderContent(folderUrl)
.then(resp => resp.json()) .then(resp => resp.json())
.then((response) => { .then(response => this.store.setfolderContent(folder, response.environments))
this.store.setfolderContent(folder, response.environments); .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
this.isLoadingFolderContent = false;
})
.catch(() => { .catch(() => {
this.isLoadingFolderContent = false;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
}); });
}, },
...@@ -176,13 +170,13 @@ export default { ...@@ -176,13 +170,13 @@ export default {
successCallback(resp) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
// If folders are open while polling we need to open them again // We need to verify if any folder is open to also update it
if (this.openFolders.length) { const openFolders = this.store.getOpenFolders();
this.openFolders.map((folder) => { if (openFolders.length) {
openFolders.forEach((folder) => {
// TODO - Move this to the backend // TODO - Move this to the backend
const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`; const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
this.store.updateFolder(folder, 'isOpen', true);
return this.fetchChildEnvironments(folder, folderUrl); return this.fetchChildEnvironments(folder, folderUrl);
}); });
} }
...@@ -267,7 +261,7 @@ export default { ...@@ -267,7 +261,7 @@ export default {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:is-loading-folder-content="isLoadingFolderContent" /> />
</div> </div>
<table-pagination <table-pagination
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import playIconSvg from 'icons/_icon_play.svg'; import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { props: {
...@@ -12,6 +13,10 @@ export default { ...@@ -12,6 +13,10 @@ export default {
}, },
}, },
directives: {
tooltip,
},
components: { components: {
loadingIcon, loadingIcon,
}, },
...@@ -33,8 +38,6 @@ export default { ...@@ -33,8 +38,6 @@ export default {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint); eventHub.$emit('postAction', endpoint);
}, },
...@@ -53,11 +56,11 @@ export default { ...@@ -53,11 +56,11 @@ export default {
class="btn-group" class="btn-group"
role="group"> role="group">
<button <button
v-tooltip
type="button" type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body" data-container="body"
data-toggle="dropdown" data-toggle="dropdown"
ref="tooltip"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:disabled="isLoading"> :disabled="isLoading">
......
<script> <script>
import tooltip from '../../vue_shared/directives/tooltip';
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
...@@ -10,6 +12,10 @@ export default { ...@@ -10,6 +12,10 @@ export default {
}, },
}, },
directives: {
tooltip,
},
computed: { computed: {
title() { title() {
return 'Open'; return 'Open';
...@@ -19,7 +25,8 @@ export default { ...@@ -19,7 +25,8 @@ export default {
</script> </script>
<template> <template>
<a <a
class="btn external-url has-tooltip" v-tooltip
class="btn external-url"
data-container="body" data-container="body"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
......
...@@ -498,9 +498,9 @@ export default { ...@@ -498,9 +498,9 @@ export default {
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell"> <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a <a
v-if="shouldRenderBuildName" v-if="shouldRenderBuildName"
class="build-link" class="build-link flex-truncate-parent"
:href="buildPath"> :href="buildPath">
{{buildName}} <span class="flex-truncate-child">{{buildName}}</span>
</a> </a>
</div> </div>
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { props: {
monitoringUrl: { monitoringUrl: {
...@@ -10,6 +12,10 @@ export default { ...@@ -10,6 +12,10 @@ export default {
}, },
}, },
directives: {
tooltip,
},
computed: { computed: {
title() { title() {
return 'Monitoring'; return 'Monitoring';
...@@ -19,7 +25,8 @@ export default { ...@@ -19,7 +25,8 @@ export default {
</script> </script>
<template> <template>
<a <a
class="btn monitoring-url has-tooltip hidden-xs hidden-sm" v-tooltip
class="btn monitoring-url hidden-xs hidden-sm"
data-container="body" data-container="body"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
:href="monitoringUrl" :href="monitoringUrl"
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
*/ */
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { props: {
...@@ -14,6 +15,10 @@ export default { ...@@ -14,6 +15,10 @@ export default {
}, },
}, },
directives: {
tooltip,
},
data() { data() {
return { return {
isLoading: false, isLoading: false,
...@@ -46,8 +51,9 @@ export default { ...@@ -46,8 +51,9 @@ export default {
</script> </script>
<template> <template>
<button <button
v-tooltip
type="button" type="button"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm" class="btn stop-env-link hidden-xs hidden-sm"
data-container="body" data-container="body"
@click="onClick" @click="onClick"
:disabled="isLoading" :disabled="isLoading"
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* Used in environments table. * Used in environments table.
*/ */
import terminalIconSvg from 'icons/_icon_terminal.svg'; import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { props: {
...@@ -14,6 +15,10 @@ export default { ...@@ -14,6 +15,10 @@ export default {
}, },
}, },
directives: {
tooltip,
},
data() { data() {
return { return {
terminalIconSvg, terminalIconSvg,
...@@ -29,7 +34,8 @@ export default { ...@@ -29,7 +34,8 @@ export default {
</script> </script>
<template> <template>
<a <a
class="btn terminal-button has-tooltip hidden-xs hidden-sm" v-tooltip
class="btn terminal-button hidden-xs hidden-sm"
data-container="body" data-container="body"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
......
...@@ -29,12 +29,6 @@ export default { ...@@ -29,12 +29,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
}, },
methods: { methods: {
...@@ -74,7 +68,7 @@ export default { ...@@ -74,7 +68,7 @@ export default {
/> />
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<div v-if="isLoadingFolderContent"> <div v-if="model.isLoadingFolderContent">
<loading-icon size="2" /> <loading-icon size="2" />
</div> </div>
......
export default { export default {
methods: { methods: {
saveData(resp) { saveData(resp) {
const response = { const headers = resp.headers;
headers: resp.headers, return resp.json().then((response) => {
body: resp.json(), this.isLoading = false;
};
this.isLoading = false; this.store.storeAvailableCount(response.available_count);
this.store.storeStoppedCount(response.stopped_count);
this.store.storeAvailableCount(response.body.available_count); this.store.storeEnvironments(response.environments);
this.store.storeStoppedCount(response.body.stopped_count); this.store.setPagination(headers);
this.store.storeEnvironments(response.body.environments); });
this.store.setPagination(response.headers);
}, },
}, },
}; };
...@@ -35,14 +35,18 @@ export default class EnvironmentsStore { ...@@ -35,14 +35,18 @@ export default class EnvironmentsStore {
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments
.find(element => element.id === env.latest.id) || {};
let filtered = {}; let filtered = {};
if (env.size > 1) { if (env.size > 1) {
filtered = Object.assign({}, env, { filtered = Object.assign({}, env, {
isFolder: true, isFolder: true,
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
folderName: env.name, folderName: env.name,
isOpen: false, isOpen: oldEnvironmentState.isOpen || false,
children: [], children: oldEnvironmentState.children || [],
}); });
} }
...@@ -98,7 +102,7 @@ export default class EnvironmentsStore { ...@@ -98,7 +102,7 @@ export default class EnvironmentsStore {
* @return {Array} * @return {Array}
*/ */
toggleFolder(folder) { toggleFolder(folder) {
return this.updateFolder(folder, 'isOpen', !folder.isOpen); return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
} }
/** /**
...@@ -125,23 +129,23 @@ export default class EnvironmentsStore { ...@@ -125,23 +129,23 @@ export default class EnvironmentsStore {
return updated; return updated;
}); });
return this.updateFolder(folder, 'children', updatedEnvironments); return this.updateEnvironmentProp(folder, 'children', updatedEnvironments);
} }
/** /**
* Given a folder a prop and a new value updates the correct folder. * Given a environment, a prop and a new value updates the correct environment.
* *
* @param {Object} folder * @param {Object} environment
* @param {String} prop * @param {String} prop
* @param {String|Boolean|Object|Array} newValue * @param {String|Boolean|Object|Array} newValue
* @return {Array} * @return {Array}
*/ */
updateFolder(folder, prop, newValue) { updateEnvironmentProp(environment, prop, newValue) {
const environments = this.state.environments; const environments = this.state.environments;
const updatedEnvironments = environments.map((env) => { const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env); const updateEnv = Object.assign({}, env);
if (env.isFolder && env.id === folder.id) { if (env.id === environment.id) {
updateEnv[prop] = newValue; updateEnv[prop] = newValue;
} }
...@@ -149,8 +153,6 @@ export default class EnvironmentsStore { ...@@ -149,8 +153,6 @@ export default class EnvironmentsStore {
}); });
this.state.environments = updatedEnvironments; this.state.environments = updatedEnvironments;
return updatedEnvironments;
} }
getOpenFolders() { getOpenFolders() {
......
import Cookies from 'js-cookie';
export default () => {
$('.js-experiment-feature-toggle').on('change', (e) => {
const el = e.target;
Cookies.set(el.name, el.value, {
expires: 365 * 10,
});
document.body.scrollTop = 0;
window.location.reload();
});
};
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
/* global notes */ /* global notes */
let $commentButtonTemplate; /* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
window.FilesCommentButton = (function() { * bottleneck for pages with large diffs. For a comprehensive list of what
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
COMMENT_BUTTON_CLASS = '.add-diff-note';
const LINE_NUMBER_CLASS = 'diff-line-num';
LINE_HOLDER_CLASS = '.line_holder'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
LINE_NUMBER_CLASS = 'diff-line-num'; const EMPTY_CELL_CLASS = 'empty-cell';
const OLD_LINE_CLASS = 'old_line';
LINE_CONTENT_CLASS = 'line_content'; const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
const DIFF_CONTAINER_SELECTOR = '.files';
UNFOLDABLE_LINE_CLASS = 'js-unfold'; const DIFF_EXPANDED_CLASS = 'diff-expanded';
EMPTY_CELL_CLASS = 'empty-cell'; export default {
init($diffFile) {
OLD_LINE_CLASS = 'old_line'; /* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; * will be true in all cases */
TEXT_FILE_SELECTOR = '.text-file'; if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
function FilesCommentButton(filesContainerElement) { this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
this.render = this.render.bind(this);
this.hideButton = this.hideButton.bind(this);
this.isParallelView = notes.isParallelView();
filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
}
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
} }
textFileElement = this.getTextFileElement($currentTarget); if (typeof notes !== 'undefined' && !this.isParallelView) {
buttonParentElement.append(this.buildButton({ this.isParallelView = notes.isParallelView && notes.isParallelView();
discussionID: lineContentElement.attr('data-discussion-id'),
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
FilesCommentButton.prototype.hideButton = function(e) {
var $currentTarget = $(e.currentTarget);
var buttonParentElement = this.getButtonParent($currentTarget);
buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
};
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
// DiffNote
'data-position': buttonAttributes.position
});
};
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return hoveredElement.closest(TEXT_FILE_SELECTOR);
};
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement;
}
if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
} }
};
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { if (this.userCanCreateNote) {
if (!this.isParallelView) { $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
if (hoveredElement.hasClass(OLD_LINE_CLASS)) { .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
return hoveredElement;
}
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
} else {
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
return hoveredElement;
}
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
} }
}; },
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { showButton(isParallelView, e) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { if (!this.validateButtonParent(buttonParentElement)) return;
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
};
return FilesCommentButton; buttonParentElement.classList.add('is-over');
})(); buttonParentElement.nextElementSibling.classList.add('is-over');
},
$.fn.filesCommentButton = function() { hideButton(isParallelView, e) {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
if (!(this && (this.parent().data('can-create-note') != null))) { buttonParentElement.classList.remove('is-over');
return; buttonParentElement.nextElementSibling.classList.remove('is-over');
} },
return this.each(function() {
if (!$.data(this, 'filesCommentButton')) { getButtonParent(hoveredElement, isParallelView) {
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); if (isParallelView) {
if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
return hoveredElement.previousElementSibling;
}
} else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
} }
}); return hoveredElement;
},
validateButtonParent(buttonParentElement) {
return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
},
}; };
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import AjaxFilter from '~/droplab/plugins/ajax_filter'; import AjaxFilter from '~/droplab/plugins/ajax_filter';
import './filtered_search_dropdown'; import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class DropdownUser extends gl.FilteredSearchDropdown { class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, tokenKeys, filter) { constructor(droplab, dropdown, input, tokenKeys, filter) {
...@@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { ...@@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
} }
hideCurrentUser() { hideCurrentUser() {
const currenUserItem = this.dropdown.querySelector('.js-current-user'); addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden');
currenUserItem.classList.add('hidden');
} }
itemClicked(e) { itemClicked(e) {
......
...@@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root'; ...@@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service'; import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class FilteredSearchManager { class FilteredSearchManager {
constructor(page) { constructor(page) {
...@@ -40,6 +41,10 @@ class FilteredSearchManager { ...@@ -40,6 +41,10 @@ class FilteredSearchManager {
return []; return [];
}) })
.then((searches) => { .then((searches) => {
if (!searches) {
return;
}
// Put any searches that may have come in before // Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones // we fetched the saved searches ahead of the already saved ones
const resultantSearches = this.recentSearchesStore.setRecentSearches( const resultantSearches = this.recentSearchesStore.setRecentSearches(
...@@ -223,11 +228,7 @@ class FilteredSearchManager { ...@@ -223,11 +228,7 @@ class FilteredSearchManager {
} }
addInputContainerFocus() { addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
if (inputContainer) {
inputContainer.classList.add('focus');
}
} }
removeInputContainerFocus(e) { removeInputContainerFocus(e) {
......
import emojiMap from 'emojis/digests.json'; import glRegexp from './lib/utils/regexp';
import emojiAliases from 'emojis/aliases.json'; import AjaxCache from './lib/utils/ajax_cache';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
function sanitize(str) { function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, ''); return str.replace(/<(?:.|\n)*?>/gm, '');
...@@ -33,6 +30,7 @@ class GfmAutoComplete { ...@@ -33,6 +30,7 @@ class GfmAutoComplete {
this.input.each((i, input) => { this.input.each((i, input) => {
const $input = $(input); const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
$input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
// This triggers at.js again // This triggers at.js again
// Needed for quick actions with suffixes (ex: /label ~) // Needed for quick actions with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
...@@ -375,7 +373,12 @@ class GfmAutoComplete { ...@@ -375,7 +373,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); import(/* webpackChunkName: 'emoji' */ './emoji')
.then(({ validEmojiNames, glEmojiTag }) => {
this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
} else { } else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => { .then((data) => {
...@@ -398,6 +401,13 @@ class GfmAutoComplete { ...@@ -398,6 +401,13 @@ class GfmAutoComplete {
this.cachedData = {}; this.cachedData = {};
} }
destroy() {
this.input.each((i, input) => {
const $input = $(input);
$input.atwho('destroy');
});
}
static isLoading(data) { static isLoading(data) {
let dataToInspect = data; let dataToInspect = data;
if (data && data.length > 0) { if (data && data.length > 0) {
...@@ -423,12 +433,14 @@ GfmAutoComplete.atTypeMap = { ...@@ -423,12 +433,14 @@ GfmAutoComplete.atTypeMap = {
}; };
// Emoji // Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = { GfmAutoComplete.Emoji = {
templateFunction(name) { templateFunction(name) {
return `<li> // glEmojiTag helper is loaded on-demand in fetchData()
${name} ${glEmojiTag(name)} if (GfmAutoComplete.glEmojiTag) {
</li> return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
`; }
return `<li>${name}</li>`;
}, },
}; };
// Team Members // Team Members
......
...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) { ...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
GLForm.prototype.destroy = function() { GLForm.prototype.destroy = function() {
// Clean form listeners // Clean form listeners
this.clearEventListeners(); this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
return this.form.data('gl-form', null); return this.form.data('gl-form', null);
}; };
...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() { ...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form'); this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true, emojis: true,
members: this.enableGFM, members: this.enableGFM,
issues: this.enableGFM, issues: this.enableGFM,
......
import Cookies from 'js-cookie';
import _ from 'underscore'; import _ from 'underscore';
export default class GroupName { export default class GroupName {
constructor() { constructor() {
this.titleContainer = document.querySelector('.title-container'); this.titleContainer = document.querySelector('.js-title-container');
this.title = document.querySelector('.title'); this.title = this.titleContainer.querySelector('.title');
this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title'); if (this.title) {
this.groups = document.querySelectorAll('.group-path'); this.titleWidth = this.title.offsetWidth;
this.toggle = null; this.groupTitle = this.titleContainer.querySelector('.group-title');
this.isHidden = false; this.groups = this.titleContainer.querySelectorAll('.group-path');
this.init(); this.toggle = null;
this.isHidden = false;
this.init();
}
} }
init() { init() {
...@@ -33,11 +36,20 @@ export default class GroupName { ...@@ -33,11 +36,20 @@ export default class GroupName {
createToggle() { createToggle() {
this.toggle = document.createElement('button'); this.toggle = document.createElement('button');
this.toggle.setAttribute('type', 'button');
this.toggle.className = 'text-expander group-name-toggle'; this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...'; if (Cookies.get('new_nav') === 'true') {
this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
} else {
this.toggle.innerHTML = '...';
}
this.toggle.addEventListener('click', this.toggleGroups.bind(this)); this.toggle.addEventListener('click', this.toggleGroups.bind(this));
this.titleContainer.insertBefore(this.toggle, this.title); if (Cookies.get('new_nav') === 'true') {
this.title.insertBefore(this.toggle, this.groupTitle);
} else {
this.titleContainer.insertBefore(this.toggle, this.title);
}
this.toggleGroups(); this.toggleGroups();
} }
......
...@@ -99,8 +99,10 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -99,8 +99,10 @@ document.addEventListener('DOMContentLoaded', () => {
page: currentPath, page: currentPath,
}, document.title, currentPath); }, document.title, currentPath);
this.updateGroups(response.json()); return response.json().then((data) => {
this.updatePagination(response.headers); this.updateGroups(data);
this.updatePagination(response.headers);
});
}) })
.catch(this.handleErrorResponse); .catch(this.handleErrorResponse);
}, },
...@@ -114,18 +116,19 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -114,18 +116,19 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
leaveGroup(group, collection) { leaveGroup(group, collection) {
this.service.leaveGroup(group.leavePath) this.service.leaveGroup(group.leavePath)
.then(resp => resp.json())
.then((response) => { .then((response) => {
$.scrollTo(0); $.scrollTo(0);
this.store.removeGroup(group, collection); this.store.removeGroup(group, collection);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Flash(response.json().notice, 'notice'); new Flash(response.notice, 'notice');
}) })
.catch((response) => { .catch((error) => {
let message = 'An error occurred. Please try again.'; let message = 'An error occurred. Please try again.';
if (response.status === 403) { if (error.status === 403) {
message = 'Failed to leave the group. Please make sure you are not the only owner'; message = 'Failed to leave the group. Please make sure you are not the only owner';
} }
......
import CloseReopenReportToggle from '../close_reopen_report_toggle';
function initCloseReopenReport() {
const container = document.querySelector('.js-issuable-close-dropdown');
if (!container) return undefined;
const dropdownTrigger = container.querySelector('.js-issuable-close-toggle');
const dropdownList = container.querySelector('.js-issuable-close-menu');
const button = container.querySelector('.js-issuable-close-button');
const closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.initDroplab();
return closeReopenReportToggle;
}
const IssuablesHelper = {
initCloseReopenReport,
};
export default IssuablesHelper;
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */ /* global GitLab */
/* global ZenMode */
/* global Autosave */ /* global Autosave */
/* global dateFormat */ /* global dateFormat */
/* global Pikaday */ /* global Pikaday */
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
(function() { (function() {
this.IssuableForm = (function() { this.IssuableForm = (function() {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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