Commit 83874edb authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'update-kubeclient'

 Conflicts:
   Gemfile.lock
parents 53e2987b ea5221ae
...@@ -35,15 +35,22 @@ ...@@ -35,15 +35,22 @@
"import/no-commonjs": "error", "import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }], "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error", "promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__", "_links"]}], "no-underscore-dangle": ["error", { "allow": ["__", "_links"] }],
"vue/html-self-closing": ["error", { "no-mixed-operators": 0,
"html": { "space-before-function-paren": 0,
"void": "always", "curly": 0,
"normal": "never", "arrow-parens": 0,
"component": "always" "vue/html-self-closing": [
}, "error",
"svg": "always", {
"math": "always" "html": {
}] "void": "always",
"normal": "never",
"component": "always"
},
"svg": "always",
"math": "always"
}
]
} }
} }
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
eslint-report.html eslint-report.html
/.gitlab_shell_secret /.gitlab_shell_secret
.idea .idea
/.vscode/*
/.rbenv-version /.rbenv-version
.rbx/ .rbx/
/.ruby-gemset /.ruby-gemset
......
...@@ -257,7 +257,7 @@ stages: ...@@ -257,7 +257,7 @@ stages:
## ##
# Trigger a package build in omnibus-gitlab repository # Trigger a package build in omnibus-gitlab repository
# #
package-qa: package-and-qa:
<<: *dedicated-runner <<: *dedicated-runner
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
......
{
"singleQuote": true,
"trailingComma": "all"
}
...@@ -2,6 +2,20 @@ ...@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.5.5 (2018-03-15)
### Fixed (3 changes)
- Fix missing uploads after group transfer. !17658
- Fix code and wiki search results when filename is non-ASCII.
- Remove double caching of Repository#empty?.
### Performance (2 changes)
- Adding missing indexes on taggings table.
- Add index on section_name_id on ci_build_trace_sections table.
## 10.5.4 (2018-03-08) ## 10.5.4 (2018-03-08)
### Fixed (11 changes) ### Fixed (11 changes)
......
...@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2' ...@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2' gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-saml', '~> 1.10.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'
...@@ -104,7 +104,7 @@ gem 'carrierwave', '~> 1.2' ...@@ -104,7 +104,7 @@ gem 'carrierwave', '~> 1.2'
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 1.4' gem 'fog-aws', '~> 2.0'
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'
...@@ -152,7 +152,7 @@ end ...@@ -152,7 +152,7 @@ end
gem 'state_machines-activerecord', '~> 0.4.0' gem 'state_machines-activerecord', '~> 0.4.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 5.0' gem 'sidekiq', '~> 5.0'
...@@ -257,14 +257,13 @@ gem 'font-awesome-rails', '~> 4.7' ...@@ -257,14 +257,13 @@ gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3' gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.1.0' gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.3.1'
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 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 2.5.3' gem 'sentry-raven', '~> 2.7'
gem 'premailer-rails', '~> 1.9.7' gem 'premailer-rails', '~> 1.9.7'
...@@ -300,7 +299,7 @@ group :metrics do ...@@ -300,7 +299,7 @@ group :metrics do
end end
group :development do group :development do
gem 'foreman', '~> 0.78.0' gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 3.6.0', require: false gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
...@@ -360,7 +359,7 @@ group :development, :test do ...@@ -360,7 +359,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'license_finder', '~> 3.1', require: false gem 'license_finder', '~> 3.1', require: false
gem 'knapsack', '~> 1.11.0' gem 'knapsack', '~> 1.16'
gem 'activerecord_sane_schema_dumper', '0.2' gem 'activerecord_sane_schema_dumper', '0.2'
...@@ -381,14 +380,14 @@ group :test do ...@@ -381,14 +380,14 @@ group :test do
gem 'test-prof', '~> 0.2.5' gem 'test-prof', '~> 0.2.5'
end end
gem 'octokit', '~> 4.6.2' gem 'octokit', '~> 4.8'
gem 'mail_room', '~> 0.9.1' gem 'mail_room', '~> 0.9.1'
gem 'email_reply_trimmer', '~> 0.1' gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text' gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.17.0'
# OAuth # OAuth
gem 'oauth2', '~> 1.4' gem 'oauth2', '~> 1.4'
...@@ -401,7 +400,7 @@ gem 'vmstat', '~> 2.3.0' ...@@ -401,7 +400,7 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support # SSH host key support
gem 'net-ssh', '~> 4.1.0' gem 'net-ssh', '~> 4.2.0'
gem 'sshkey', '~> 1.9.0' gem 'sshkey', '~> 1.9.0'
# Required for ED25519 SSH host key support # Required for ED25519 SSH host key support
...@@ -421,9 +420,9 @@ gem 'google-protobuf', '= 3.5.1' ...@@ -421,9 +420,9 @@ gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 1.0.0', require: false gem 'toml-rb', '~> 1.0.0', require: false
# Feature toggles # Feature toggles
gem 'flipper', '~> 0.11.0' gem 'flipper', '~> 0.13.0'
gem 'flipper-active_record', '~> 0.11.0' gem 'flipper-active_record', '~> 0.13.0'
gem 'flipper-active_support_cache_store', '~> 0.11.0' gem 'flipper-active_support_cache_store', '~> 0.13.0'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
......
...@@ -40,8 +40,8 @@ GEM ...@@ -40,8 +40,8 @@ GEM
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0) acts-as-taggable-on (5.0.0)
activerecord (>= 4.0) activerecord (>= 4.2.8)
adamantium (0.2.0) adamantium (0.2.0)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
memoizable (~> 0.4.0) memoizable (~> 0.4.0)
...@@ -194,7 +194,7 @@ GEM ...@@ -194,7 +194,7 @@ GEM
et-orbi (1.0.3) et-orbi (1.0.3)
tzinfo tzinfo
eventmachine (1.0.8) eventmachine (1.0.8)
excon (0.57.1) excon (0.60.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
factory_bot (4.8.2) factory_bot (4.8.2)
...@@ -218,13 +218,13 @@ GEM ...@@ -218,13 +218,13 @@ GEM
path_expander (~> 1.0) path_expander (~> 1.0)
ruby_parser (~> 3.0) ruby_parser (~> 3.0)
sexp_processor (~> 4.0) sexp_processor (~> 4.0)
flipper (0.11.0) flipper (0.13.0)
flipper-active_record (0.11.0) flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6) activerecord (>= 3.2, < 6)
flipper (~> 0.11.0) flipper (~> 0.13.0)
flipper-active_support_cache_store (0.11.0) flipper-active_support_cache_store (0.13.0)
activesupport (>= 3.2, < 6) activesupport (>= 3.2, < 6)
flipper (~> 0.11.0) flipper (~> 0.13.0)
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
...@@ -233,14 +233,14 @@ GEM ...@@ -233,14 +233,14 @@ 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 (1.4.0) fog-aws (2.0.1)
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.3) fog-core (1.45.0)
builder builder
excon (~> 0.49) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (0.5.3) fog-google (0.5.3)
fog-core fog-core
...@@ -265,7 +265,7 @@ GEM ...@@ -265,7 +265,7 @@ GEM
nokogiri (>= 1.5.11, < 2.0.0) nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1) font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
foreman (0.78.0) foreman (0.84.0)
thor (~> 0.19.1) thor (~> 0.19.1)
formatador (0.2.5) formatador (0.2.5)
fuubar (2.2.0) fuubar (2.2.0)
...@@ -388,7 +388,7 @@ GEM ...@@ -388,7 +388,7 @@ GEM
thor thor
tilt tilt
hashdiff (0.3.4) hashdiff (0.3.4)
hashie (3.5.6) hashie (3.5.7)
hashie-forbidden_attributes (0.1.1) hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0) hashie (>= 3.0)
health_check (2.6.0) health_check (2.6.0)
...@@ -415,7 +415,7 @@ GEM ...@@ -415,7 +415,7 @@ GEM
json (~> 1.8) json (~> 1.8)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.2) httpclient (2.8.2)
i18n (0.9.1) i18n (0.9.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ice_nine (0.11.2) ice_nine (0.11.2)
influxdb (0.2.3) influxdb (0.2.3)
...@@ -427,10 +427,6 @@ GEM ...@@ -427,10 +427,6 @@ GEM
multipart-post multipart-post
oauth (~> 0.5, >= 0.5.0) oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6) json (1.8.6)
json-jwt (1.7.2) json-jwt (1.7.2)
activesupport activesupport
...@@ -454,7 +450,7 @@ GEM ...@@ -454,7 +450,7 @@ GEM
kaminari-core (= 1.0.1) kaminari-core (= 1.0.1)
kaminari-core (1.0.1) kaminari-core (1.0.1)
kgio (2.10.0) kgio (2.10.0)
knapsack (1.11.0) knapsack (1.16.0)
rake rake
timecop (>= 0.1.0) timecop (>= 0.1.0)
kubeclient (3.0.0) kubeclient (3.0.0)
...@@ -513,7 +509,7 @@ GEM ...@@ -513,7 +509,7 @@ GEM
mustermann (~> 1.0.0) mustermann (~> 1.0.0)
mysql2 (0.4.10) mysql2 (0.4.10)
net-ldap (0.16.0) net-ldap (0.16.0)
net-ssh (4.1.0) net-ssh (4.2.0)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.8.2) nokogiri (1.8.2)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
...@@ -525,12 +521,12 @@ GEM ...@@ -525,12 +521,12 @@ GEM
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.6.2) octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.5) oj (2.17.5)
omniauth (1.4.2) omniauth (1.4.3)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.6.2, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.3.1) omniauth-authentiq (0.3.1)
...@@ -569,9 +565,9 @@ GEM ...@@ -569,9 +565,9 @@ GEM
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2) omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
omniauth-saml (1.7.0) omniauth-saml (1.10.0)
omniauth (~> 1.3) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.4) ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
omniauth-twitter (1.2.1) omniauth-twitter (1.2.1)
...@@ -650,7 +646,7 @@ GEM ...@@ -650,7 +646,7 @@ GEM
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (3.0.2) public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.8) rack (1.6.9)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
...@@ -803,9 +799,9 @@ GEM ...@@ -803,9 +799,9 @@ GEM
i18n i18n
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-prof (0.16.2) ruby-prof (0.17.0)
ruby-progressbar (1.9.0) ruby-progressbar (1.9.0)
ruby-saml (1.4.1) ruby-saml (1.7.2)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
ruby_parser (3.9.0) ruby_parser (3.9.0)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
...@@ -844,7 +840,7 @@ GEM ...@@ -844,7 +840,7 @@ GEM
selenium-webdriver (3.5.0) selenium-webdriver (3.5.0)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.0) rubyzip (~> 1.0)
sentry-raven (2.5.3) sentry-raven (2.7.2)
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)
...@@ -933,7 +929,7 @@ GEM ...@@ -933,7 +929,7 @@ GEM
truncato (0.7.10) truncato (0.7.10)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.8.0, >= 1.7.0) nokogiri (~> 1.8.0, >= 1.7.0)
tzinfo (1.2.4) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1) u2f (0.2.1)
uber (0.1.0) uber (0.1.0)
...@@ -994,7 +990,7 @@ DEPENDENCIES ...@@ -994,7 +990,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord_sane_schema_dumper (= 0.2) activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0) acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2) addressable (~> 2.5.2)
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
...@@ -1044,18 +1040,18 @@ DEPENDENCIES ...@@ -1044,18 +1040,18 @@ DEPENDENCIES
fast_blank fast_blank
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.10.0) flay (~> 2.10.0)
flipper (~> 0.11.0) flipper (~> 0.13.0)
flipper-active_record (~> 0.11.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.11.0) flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 1.4) fog-aws (~> 2.0)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 0.5) fog-google (~> 0.5)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7) font-awesome-rails (~> 4.7)
foreman (~> 0.78.0) foreman (~> 0.84.0)
fuubar (~> 2.2.0) fuubar (~> 2.2.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.3) gemojione (~> 3.3)
...@@ -1090,11 +1086,10 @@ DEPENDENCIES ...@@ -1090,11 +1086,10 @@ DEPENDENCIES
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.3.1)
json-schema (~> 2.8.0) json-schema (~> 2.8.0)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.11.0) knapsack (~> 1.16)
kubeclient (~> 3.0) kubeclient (~> 3.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 3.1)
...@@ -1107,10 +1102,10 @@ DEPENDENCIES ...@@ -1107,10 +1102,10 @@ DEPENDENCIES
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10) mysql2 (~> 0.4.10)
net-ldap net-ldap
net-ssh (~> 4.1.0) net-ssh (~> 4.2.0)
nokogiri (~> 1.8.2) nokogiri (~> 1.8.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
octokit (~> 4.6.2) octokit (~> 4.8)
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)
...@@ -1123,7 +1118,7 @@ DEPENDENCIES ...@@ -1123,7 +1118,7 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.5.2) omniauth-google-oauth2 (~> 0.5.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.7.0) omniauth-saml (~> 1.10.0)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
...@@ -1173,7 +1168,7 @@ DEPENDENCIES ...@@ -1173,7 +1168,7 @@ DEPENDENCIES
rubocop (~> 0.52.1) rubocop (~> 0.52.1)
rubocop-rspec (~> 1.22.1) rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.17.0)
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.26.0) rugged (~> 0.26.0)
...@@ -1183,7 +1178,7 @@ DEPENDENCIES ...@@ -1183,7 +1178,7 @@ DEPENDENCIES
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3) sentry-raven (~> 2.7)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 3.1.2) shoulda-matchers (~> 3.1.2)
......
...@@ -132,9 +132,8 @@ class GfmAutoComplete { ...@@ -132,9 +132,8 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
matcher(flag, subtext) { matcher(flag, subtext) {
const relevantText = subtext.trim().split(/\s/).pop();
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
const match = regexp.exec(relevantText); const match = regexp.exec(subtext);
return match && match.length ? match[1] : null; return match && match.length ? match[1] : null;
}, },
......
...@@ -73,6 +73,7 @@ export default class MergeRequestTabs { ...@@ -73,6 +73,7 @@ export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix'); const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab'); const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('peek');
const paddingTop = 16; const paddingTop = 16;
this.diffsLoaded = false; this.diffsLoaded = false;
...@@ -86,6 +87,10 @@ export default class MergeRequestTabs { ...@@ -86,6 +87,10 @@ export default class MergeRequestTabs {
this.showTab = this.showTab.bind(this); this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
if (mergeRequestTabs) { if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight; this.stickyTop += mergeRequestTabs.offsetHeight;
} }
......
...@@ -209,6 +209,7 @@ ...@@ -209,6 +209,7 @@
const xAxis = d3.axisBottom() const xAxis = d3.axisBottom()
.scale(axisXScale) .scale(axisXScale)
.ticks(this.graphWidth / 120)
.tickFormat(timeScaleFormat); .tickFormat(timeScaleFormat);
const yAxis = d3.axisLeft() const yAxis = d3.axisLeft()
......
...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; ...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores'; import store from '../notes/stores';
export default function initMrNotes() { export default function initMrNotes() {
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions', el: '#js-vue-mr-discussions',
components: { components: {
notesApp, notesApp,
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
...@@ -28,7 +30,8 @@ export default function initMrNotes() { ...@@ -28,7 +30,8 @@ export default function initMrNotes() {
}, },
}); });
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter', el: '#js-vue-discussion-counter',
components: { components: {
discussionCounter, discussionCounter,
......
This diff is collapsed.
<script> <script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
ClipboardButton, ClipboardButton,
Icon, Icon,
},
props: {
diffFile: {
type: Object,
required: true,
}, },
props: { },
diffFile: { computed: {
type: Object, titleTag() {
required: true, return this.diffFile.discussionPath ? 'a' : 'span';
},
}, },
computed: { },
titleTag() { };
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index'; import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
},
props: {
discussion: {
type: Object,
required: true,
}, },
props: { },
discussion: { computed: {
type: Object, isImageDiff() {
required: true, return !this.diffFile.text;
},
}, },
computed: { diffFileClass() {
isImageDiff() { const { text } = this.diffFile;
return !this.diffFile.text; return text ? 'text-file' : 'js-image-file';
},
diffFileClass() {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
diffRows() {
return $(this.discussion.truncatedDiffLines);
},
diffFile() {
return convertObjectPropsToCamelCase(this.discussion.diffFile);
},
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
}, },
mounted() { diffRows() {
if (this.isImageDiff) { return $(this.discussion.truncatedDiffLines);
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
syntaxHighlight(fileHolder);
});
}
}, },
methods: { diffFile() {
rowTag(html) { return convertObjectPropsToCamelCase(this.discussion.diffFile);
return html.outerHTML ? 'tr' : 'template';
},
}, },
}; imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
},
mounted() {
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(
this.$refs.fileHolder,
canCreateNote,
renderCommentBadge,
);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
syntaxHighlight(fileHolder);
});
}
},
methods: {
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg'; import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg'; import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg'; import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg'; import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils'; import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
computed: {
...mapGetters([
'getUserData',
'getNoteableData',
'discussionCount',
'unresolvedDiscussions',
'resolvedDiscussionCount',
]),
isLoggedIn() {
return this.getUserData.id;
}, },
computed: { hasNextButton() {
...mapGetters([ return this.isLoggedIn && !this.allResolved;
'getUserData', },
'getNoteableData', countText() {
'discussionCount', return pluralize('discussion', this.discussionCount);
'unresolvedDiscussions', },
'resolvedDiscussionCount', allResolved() {
]), return this.resolvedDiscussionCount === this.discussionCount;
isLoggedIn() {
return this.getUserData.id;
},
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
countText() {
return pluralize('discussion', this.discussionCount);
},
allResolved() {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
firstUnresolvedDiscussionId() {
const item = this.unresolvedDiscussions[0] || {};
return item.id;
},
}, },
created() { resolveAllDiscussionsIssuePath() {
this.resolveSvg = resolveSvg; return this.getNoteableData.create_issue_to_resolve_discussions_path;
this.resolvedSvg = resolvedSvg; },
this.mrIssueSvg = mrIssueSvg; firstUnresolvedDiscussionId() {
this.nextDiscussionSvg = nextDiscussionSvg; const item = this.unresolvedDiscussions[0] || {};
return item.id;
}, },
methods: { },
jumpToFirstDiscussion() { created() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); this.resolveSvg = resolveSvg;
const activeTab = window.mrTabs.currentAction; this.resolvedSvg = resolvedSvg;
this.mrIssueSvg = mrIssueSvg;
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: {
jumpToFirstDiscussion() {
const el = document.querySelector(
`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') { if (activeTab === 'commits' || activeTab === 'pipelines') {
window.mrTabs.activateTab('show'); window.mrTabs.activateTab('show');
} }
if (el) { if (el) {
scrollToElement(el); scrollToElement(el);
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable'; import Issuable from '~/vue_shared/mixins/issuable';
export default { export default {
components: { components: {
Icon, Icon,
}, },
mixins: [ mixins: [Issuable],
Issuable, };
],
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg'; import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg';
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'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
authorId: { authorId: {
type: Number, type: Number,
required: true, required: true,
}, },
noteId: { noteId: {
type: Number, type: Number,
required: true, required: true,
}, },
accessLevel: { accessLevel: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
reportAbusePath: { reportAbusePath: {
type: String, type: String,
required: true, required: true,
}, },
canEdit: { canEdit: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
canDelete: { canDelete: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
resolvable: { resolvable: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
isResolved: { isResolved: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
isResolving: { isResolving: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
resolvedBy: { resolvedBy: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
canReportAsAbuse: { canReportAsAbuse: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['getUserDataByProp']),
'getUserDataByProp', shouldShowActionsDropdown() {
]), return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
shouldShowActionsDropdown() { },
return this.currentUserId && (this.canEdit || this.canReportAsAbuse); canAddAwardEmoji() {
}, return this.currentUserId;
canAddAwardEmoji() { },
return this.currentUserId; isAuthoredByCurrentUser() {
}, return this.authorId === this.currentUserId;
isAuthoredByCurrentUser() { },
return this.authorId === this.currentUserId; currentUserId() {
}, return this.getUserDataByProp('id');
currentUserId() { },
return this.getUserDataByProp('id'); resolveButtonTitle() {
}, let title = 'Mark as resolved';
resolveButtonTitle() {
let title = 'Mark as resolved';
if (this.resolvedBy) { if (this.resolvedBy) {
title = `Resolved by ${this.resolvedBy.name}`; title = `Resolved by ${this.resolvedBy.name}`;
} }
return title; return title;
}, },
}, },
created() { created() {
this.emojiSmiling = emojiSmiling; this.emojiSmiling = emojiSmiling;
this.emojiSmile = emojiSmile; this.emojiSmile = emojiSmile;
this.emojiSmiley = emojiSmiley; this.emojiSmiley = emojiSmiley;
this.editSvg = editSvg; this.editSvg = editSvg;
this.ellipsisSvg = ellipsisSvg; this.ellipsisSvg = ellipsisSvg;
this.resolveDiscussionSvg = resolveDiscussionSvg; this.resolveDiscussionSvg = resolveDiscussionSvg;
this.resolvedDiscussionSvg = resolvedDiscussionSvg; this.resolvedDiscussionSvg = resolvedDiscussionSvg;
}, },
methods: { methods: {
onEdit() { onEdit() {
this.$emit('handleEdit'); this.$emit('handleEdit');
}, },
onDelete() { onDelete() {
this.$emit('handleDelete'); this.$emit('handleDelete');
}, },
onResolve() { onResolve() {
this.$emit('handleResolve'); this.$emit('handleResolve');
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
name: 'NoteAttachment', name: 'NoteAttachment',
props: { props: {
attachment: { attachment: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import noteEditedText from './note_edited_text.vue'; import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue'; import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue'; import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue'; import noteForm from './note_form.vue';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
components: { components: {
noteEditedText, noteEditedText,
noteAwardsList, noteAwardsList,
noteAttachment, noteAttachment,
noteForm, noteForm,
},
mixins: [autosave],
props: {
note: {
type: Object,
required: true,
}, },
mixins: [ canEdit: {
autosave, type: Boolean,
], required: true,
props: {
note: {
type: Object,
required: true,
},
canEdit: {
type: Boolean,
required: true,
},
isEditing: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { isEditing: {
noteBody() { type: Boolean,
return this.note.note; required: false,
}, default: false,
}, },
mounted() { },
this.renderGFM(); computed: {
this.initTaskList(); noteBody() {
return this.note.note;
},
},
mounted() {
this.renderGFM();
this.initTaskList();
if (this.isEditing) {
this.initAutoSave(this.note.noteable_type);
}
},
updated() {
this.initTaskList();
this.renderGFM();
if (this.isEditing) { if (this.isEditing) {
if (!this.autosave) {
this.initAutoSave(this.note.noteable_type); this.initAutoSave(this.note.noteable_type);
} else {
this.setAutoSave();
} }
}
},
methods: {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
}, },
updated() { initTaskList() {
this.initTaskList(); if (this.canEdit) {
this.renderGFM(); this.taskList = new TaskList({
dataType: 'note',
if (this.isEditing) { fieldName: 'note',
if (!this.autosave) { selector: '.notes',
this.initAutoSave(this.note.noteable_type); });
} else {
this.setAutoSave();
}
} }
}, },
methods: { handleFormUpdate(note, parentElement, callback) {
renderGFM() { this.$emit('handleFormUpdate', note, parentElement, callback);
$(this.$refs['note-body']).renderGFM(); },
}, formCancelHandler(shouldConfirm, isDirty) {
initTaskList() { this.$emit('cancelFormEdition', shouldConfirm, isDirty);
if (this.canEdit) {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes',
});
}
},
handleFormUpdate(note, parentElement, callback) {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
name: 'EditedNoteText', name: 'EditedNoteText',
components: { components: {
timeAgoTooltip, timeAgoTooltip,
},
props: {
actionText: {
type: String,
required: true,
}, },
props: { editedAt: {
actionText: { type: String,
type: String, required: true,
required: true,
},
editedAt: {
type: String,
required: true,
},
editedBy: {
type: Object,
required: false,
default: () => ({}),
},
className: {
type: String,
required: false,
default: 'edited-text',
},
}, },
}; editedBy: {
type: Object,
required: false,
default: () => ({}),
},
className: {
type: String,
required: false,
default: 'edited-text',
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
name: 'IssueNoteForm', name: 'IssueNoteForm',
components: { components: {
issueWarning, issueWarning,
markdownField, markdownField,
},
mixins: [issuableStateMixin, resolvable],
props: {
noteBody: {
type: String,
required: false,
default: '',
}, },
mixins: [ noteId: {
issuableStateMixin, type: Number,
resolvable, required: false,
], default: 0,
props: {
noteBody: {
type: String,
required: false,
default: '',
},
noteId: {
type: Number,
required: false,
default: 0,
},
saveButtonTitle: {
type: String,
required: false,
default: 'Save comment',
},
note: {
type: Object,
required: false,
default: () => ({}),
},
isEditing: {
type: Boolean,
required: true,
},
}, },
data() { saveButtonTitle: {
return { type: String,
updatedNoteBody: this.noteBody, required: false,
conflictWhileEditing: false, default: 'Save comment',
isSubmitting: false,
isResolving: false,
resolveAsThread: true,
};
}, },
computed: { note: {
...mapGetters([ type: Object,
'getDiscussionLastNote', required: false,
'getNoteableData', default: () => ({}),
'getNoteableDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
]),
noteHash() {
return `#note_${this.noteId}`;
},
markdownPreviewPath() {
return this.getNoteableDataByProp('preview_note_path');
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
},
isDisabled() {
return !this.updatedNoteBody.length || this.isSubmitting;
},
}, },
watch: { isEditing: {
noteBody() { type: Boolean,
if (this.updatedNoteBody === this.noteBody) { required: true,
this.updatedNoteBody = this.noteBody; },
} else { },
this.conflictWhileEditing = true; data() {
} return {
}, updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
isResolving: false,
resolveAsThread: true,
};
},
computed: {
...mapGetters([
'getDiscussionLastNote',
'getNoteableData',
'getNoteableDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
]),
noteHash() {
return `#note_${this.noteId}`;
},
markdownPreviewPath() {
return this.getNoteableDataByProp('preview_note_path');
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
return !this.isEditing
? this.getNotesDataByProp('quickActionsDocsPath')
: undefined;
}, },
mounted() { currentUserId() {
this.$refs.textarea.focus(); return this.getUserDataByProp('id');
}, },
methods: { isDisabled() {
...mapActions([ return !this.updatedNoteBody.length || this.isSubmitting;
'toggleResolveNote', },
]), },
handleUpdate(shouldResolve) { watch: {
const beforeSubmitDiscussionState = this.discussionResolved; noteBody() {
this.isSubmitting = true; if (this.updatedNoteBody === this.noteBody) {
this.updatedNoteBody = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
},
},
mounted() {
this.$refs.textarea.focus();
},
methods: {
...mapActions(['toggleResolveNote']),
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { this.$emit(
'handleFormUpdate',
this.updatedNoteBody,
this.$refs.editNoteForm,
() => {
this.isSubmitting = false; this.isSubmitting = false;
if (shouldResolve) { if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState); this.resolveHandler(beforeSubmitDiscussionState);
} }
}); },
}, );
editMyLastNote() { },
if (this.updatedNoteBody === '') { editMyLastNote() {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(
this.updatedNoteBody,
);
if (lastNoteInDiscussion) { if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', { eventHub.$emit('enterEditMode', {
noteId: lastNoteInDiscussion.id, noteId: lastNoteInDiscussion.id,
}); });
}
} }
}, }
cancelHandler(shouldConfirm = false) { },
// Sends information about confirm message and if the textarea has changed cancelHandler(shouldConfirm = false) {
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody); // Sends information about confirm message and if the textarea has changed
}, this.$emit(
'cancelFormEdition',
shouldConfirm,
this.noteBody !== this.updatedNoteBody,
);
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
timeAgoTooltip, timeAgoTooltip,
},
props: {
author: {
type: Object,
required: true,
}, },
props: { createdAt: {
author: { type: String,
type: Object, required: true,
required: true,
},
createdAt: {
type: String,
required: true,
},
actionText: {
type: String,
required: false,
default: '',
},
actionTextHtml: {
type: String,
required: false,
default: '',
},
noteId: {
type: Number,
required: true,
},
includeToggle: {
type: Boolean,
required: false,
default: false,
},
expanded: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { actionText: {
toggleChevronClass() { type: String,
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; required: false,
}, default: '',
noteTimestampLink() {
return `#note_${this.noteId}`;
},
}, },
methods: { actionTextHtml: {
...mapActions([ type: String,
'setTargetNoteHash', required: false,
]), default: '',
handleToggle() {
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
this.setTargetNoteHash(this.noteTimestampLink);
},
}, },
}; noteId: {
type: Number,
required: true,
},
includeToggle: {
type: Boolean,
required: false,
default: false,
},
expanded: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
toggleChevronClass() {
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
},
noteTimestampLink() {
return `#note_${this.noteId}`;
},
},
methods: {
...mapActions(['setTargetNoteHash']),
handleToggle() {
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
this.setTargetNoteHash(this.noteTimestampLink);
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: { computed: {
...mapGetters([ ...mapGetters(['getNotesDataByProp']),
'getNotesDataByProp', registerLink() {
]), return this.getNotesDataByProp('registerPath');
registerLink() {
return this.getNotesDataByProp('registerPath');
},
signInLink() {
return this.getNotesDataByProp('newSessionPath');
},
}, },
}; signInLink() {
return this.getNotesDataByProp('newSessionPath');
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore'; import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue'; import noteActions from './note_actions.vue';
import noteBody from './note_body.vue'; import noteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
components: { components: {
userAvatarLink, userAvatarLink,
noteHeader, noteHeader,
noteActions, noteActions,
noteBody, noteBody,
},
mixins: [noteable, resolvable],
props: {
note: {
type: Object,
required: true,
}, },
mixins: [ },
noteable, data() {
resolvable, return {
], isEditing: false,
props: { isDeleting: false,
note: { isRequesting: false,
type: Object, isResolving: false,
required: true, };
}, },
computed: {
...mapGetters(['targetNoteHash', 'getUserData']),
author() {
return this.note.author;
}, },
data() { classNameBindings() {
return { return {
isEditing: false, 'is-editing': this.isEditing && !this.isRequesting,
isDeleting: false, 'is-requesting being-posted': this.isRequesting,
isRequesting: false, 'disabled-content': this.isDeleting,
isResolving: false, target: this.targetNoteHash === this.noteAnchorId,
}; };
}, },
computed: { canReportAsAbuse() {
...mapGetters([ return (
'targetNoteHash', this.note.report_abuse_path && this.author.id !== this.getUserData.id
'getUserData', );
]),
author() {
return this.note.author;
},
classNameBindings() {
return {
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
target: this.targetNoteHash === this.noteAnchorId,
};
},
canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
},
}, },
noteAnchorId() {
created() { return `note_${this.note.id}`;
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
this.isEditing = true;
this.scrollToNoteIfNeeded($(this.$el));
}
});
}, },
},
methods: { created() {
...mapActions([ eventHub.$on('enterEditMode', ({ noteId }) => {
'deleteNote', if (noteId === this.note.id) {
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
]),
editHandler() {
this.isEditing = true; this.isEditing = true;
}, this.scrollToNoteIfNeeded($(this.$el));
deleteHandler() { }
// eslint-disable-next-line no-alert });
if (confirm('Are you sure you want to delete this comment?')) { },
this.isDeleting = true;
this.deleteNote(this.note) methods: {
.then(() => { ...mapActions([
this.isDeleting = false; 'deleteNote',
}) 'updateNote',
.catch(() => { 'toggleResolveNote',
Flash('Something went wrong while deleting your note. Please try again.'); 'scrollToNoteIfNeeded',
this.isDeleting = false; ]),
}); editHandler() {
} this.isEditing = true;
}, },
formUpdateHandler(noteText, parentElement, callback) { deleteHandler() {
const data = { // eslint-disable-next-line no-alert
endpoint: this.note.path, if (confirm('Are you sure you want to delete this comment?')) {
note: { this.isDeleting = true;
target_type: this.noteableType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
this.note.note_html = escape(noteText);
this.updateNote(data) this.deleteNote(this.note)
.then(() => { .then(() => {
this.isEditing = false; this.isDeleting = false;
this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave();
callback();
}) })
.catch(() => { .catch(() => {
this.isRequesting = false; Flash(
this.isEditing = true; 'Something went wrong while deleting your note. Please try again.',
this.$nextTick(() => { );
const msg = 'Something went wrong while editing your comment. Please try again.'; this.isDeleting = false;
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
}); });
}, }
formCancelHandler(shouldConfirm, isDirty) { },
if (shouldConfirm && isDirty) { formUpdateHandler(noteText, parentElement, callback) {
// eslint-disable-next-line no-alert const data = {
if (!confirm('Are you sure you want to cancel editing this comment?')) return; endpoint: this.note.path,
} note: {
this.$refs.noteBody.resetAutoSave(); target_type: this.noteableType,
if (this.oldContent) { target_id: this.note.noteable_id,
this.note.note_html = this.oldContent; note: { note: noteText },
},
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
this.note.note_html = escape(noteText);
this.updateNote(data)
.then(() => {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null; this.oldContent = null;
} $(this.$refs.noteBody.$el).renderGFM();
this.isEditing = false; this.$refs.noteBody.resetAutoSave();
}, callback();
recoverNoteContent(noteText) { })
// we need to do this to prevent noteForm inconsistent content warning .catch(() => {
// this is something we intentionally do so we need to recover the content this.isRequesting = false;
this.note.note = noteText; this.isEditing = true;
this.$refs.noteBody.$refs.noteForm.note.note = noteText; this.$nextTick(() => {
}, const msg =
'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
});
},
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel editing this comment?'))
return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
this.note.note_html = this.oldContent;
this.oldContent = null;
}
this.isEditing = false;
},
recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
this.$refs.noteBody.$refs.noteForm.note.note = noteText;
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility'; import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import store from '../stores/'; import store from '../stores/';
import * as constants from '../constants'; import * as constants from '../constants';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue'; import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue'; import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
components: { components: {
noteableNote, noteableNote,
noteableDiscussion, noteableDiscussion,
systemNote, systemNote,
commentForm, commentForm,
loadingIcon, loadingIcon,
placeholderNote, placeholderNote,
placeholderSystemNote, placeholderSystemNote,
},
props: {
noteableData: {
type: Object,
required: true,
}, },
props: { notesData: {
noteableData: { type: Object,
type: Object, required: true,
required: true,
},
notesData: {
type: Object,
required: true,
},
userData: {
type: Object,
required: false,
default: () => ({}),
},
}, },
store, userData: {
data() { type: Object,
return { required: false,
isLoading: true, default: () => ({}),
};
}, },
computed: { },
...mapGetters([ store,
'notes', data() {
'getNotesDataByProp', return {
'discussionCount', isLoading: true,
]), };
noteableType() { },
// FIXME -- @fatihacet Get this from JSON data. computed: {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; return this.noteableData.merge_params
}, ? MERGE_REQUEST_NOTEABLE_TYPE
allNotes() { : ISSUE_NOTEABLE_TYPE;
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
return new Array(totalNotes).fill({
isSkeletonNote: true,
});
}
return this.notes;
},
},
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
}, },
mounted() { allNotes() {
this.fetchNotes(); if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
const parentElement = this.$el.parentElement; return new Array(totalNotes).fill({
isSkeletonNote: true,
if (parentElement &&
parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
}); });
} }
document.addEventListener('refreshVueNotes', this.fetchNotes); return this.notes;
},
beforeDestroy() {
document.removeEventListener('refreshVueNotes', this.fetchNotes);
}, },
methods: { },
...mapActions({ created() {
actionFetchNotes: 'fetchNotes', this.setNotesData(this.notesData);
poll: 'poll', this.setNoteableData(this.noteableData);
actionToggleAward: 'toggleAward', this.setUserData(this.userData);
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', },
setNotesData: 'setNotesData', mounted() {
setNoteableData: 'setNoteableData', this.fetchNotes();
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt', const parentElement = this.$el.parentElement;
setTargetNoteHash: 'setTargetNoteHash',
}),
getComponentName(note) {
if (note.isSkeletonNote) {
return skeletonLoadingContainer;
}
if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? systemNote : noteableNote;
}
return noteableDiscussion; if (
}, parentElement &&
getComponentData(note) { parentElement.classList.contains('js-vue-notes-event')
return note.individual_note ? note.notes[0] : note; ) {
}, parentElement.addEventListener('toggleAward', event => {
fetchNotes() { const { awardName, noteId } = event.detail;
return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) this.actionToggleAward({ awardName, noteId });
.then(() => this.initPolling()) });
.then(() => { }
this.isLoading = false; document.addEventListener('refreshVueNotes', this.fetchNotes);
}) },
.then(() => this.$nextTick()) beforeDestroy() {
.then(() => this.checkLocationHash()) document.removeEventListener('refreshVueNotes', this.fetchNotes);
.catch(() => { },
this.isLoading = false; methods: {
Flash('Something went wrong while fetching comments. Please try again.'); ...mapActions({
}); actionFetchNotes: 'fetchNotes',
}, poll: 'poll',
initPolling() { actionToggleAward: 'toggleAward',
if (this.isPollingInitialized) { scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
return; setNotesData: 'setNotesData',
setNoteableData: 'setNoteableData',
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
}),
getComponentName(note) {
if (note.isSkeletonNote) {
return skeletonLoadingContainer;
}
if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
} }
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? systemNote : noteableNote;
}
this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); return noteableDiscussion;
},
getComponentData(note) {
return note.individual_note ? note.notes[0] : note;
},
fetchNotes() {
return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
.then(() => this.initPolling())
.then(() => {
this.isLoading = false;
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
Flash(
'Something went wrong while fetching comments. Please try again.',
);
});
},
initPolling() {
if (this.isPollingInitialized) {
return;
}
this.poll(); this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
this.isPollingInitialized = true;
},
checkLocationHash() {
const hash = getLocationHash();
const element = document.getElementById(hash);
if (hash && element) { this.poll();
this.setTargetNoteHash(hash); this.isPollingInitialized = true;
this.scrollToNoteIfNeeded($(element)); },
} checkLocationHash() {
}, const hash = getLocationHash();
const element = document.getElementById(hash);
if (hash && element) {
this.setTargetNoteHash(hash);
this.scrollToNoteIfNeeded($(element));
}
}, },
}; },
};
</script> </script>
<template> <template>
......
import Vue from 'vue'; import Vue from 'vue';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener(
el: '#js-vue-notes', 'DOMContentLoaded',
components: { () =>
notesApp, new Vue({
}, el: '#js-vue-notes',
data() { components: {
const notesDataset = document.getElementById('js-vue-notes').dataset; notesApp,
const parsedUserData = JSON.parse(notesDataset.currentUserData); },
const currentUserData = parsedUserData ? { data() {
id: parsedUserData.id, const notesDataset = document.getElementById('js-vue-notes').dataset;
name: parsedUserData.name, const parsedUserData = JSON.parse(notesDataset.currentUserData);
username: parsedUserData.username, let currentUserData = {};
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path, if (parsedUserData) {
} : {}; currentUserData = {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
};
}
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData, currentUserData,
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
}, },
render(createElement) { render(createElement) {
return createElement('notes-app', { return createElement('notes-app', {
props: { props: {
noteableData: this.noteableData, noteableData: this.noteableData,
notesData: this.notesData, notesData: this.notesData,
userData: this.currentUserData, userData: this.currentUserData,
},
});
}, },
}); }),
}, );
}));
...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; ...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default { export default {
methods: { methods: {
initAutoSave(noteableType) { initAutoSave(noteableType) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]); this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
capitalizeFirstCharacter(noteableType),
this.note.id,
]);
}, },
resetAutoSave() { resetAutoSave() {
this.autosave.reset(); this.autosave.reset();
......
...@@ -12,7 +12,8 @@ export default { ...@@ -12,7 +12,8 @@ export default {
discussionResolved() { discussionResolved() {
const { notes, resolved } = this.note; const { notes, resolved } = this.note;
if (notes) { // Decide resolved state using store. Only valid for discussions. if (notes) {
// Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system); return notes.every(note => note.resolved && !note.system);
} }
...@@ -26,7 +27,9 @@ export default { ...@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion'); return __('Comment and resolve discussion');
} }
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); return this.discussionResolved
? __('Unresolve discussion')
: __('Resolve discussion');
}, },
}, },
methods: { methods: {
...@@ -42,7 +45,9 @@ export default { ...@@ -42,7 +45,9 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isResolving = false; this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.'); const msg = __(
'Something went wrong while resolving this discussion. Please try again.',
);
Flash(msg, 'alert', this.$el); Flash(msg, 'alert', this.$el);
}); });
}, },
......
...@@ -22,7 +22,9 @@ export default { ...@@ -22,7 +22,9 @@ export default {
}, },
toggleResolveNote(endpoint, isResolved) { toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; const method = isResolved
? UNRESOLVE_NOTE_METHOD_NAME
: RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint); return Vue.http[method](endpoint);
}, },
......
...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop]; ...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state; export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop =>
state.userData && state.userData[prop];
export const notesById = state => state.notes.reduce((acc, note) => { export const notesById = state =>
note.notes.every(n => Object.assign(acc, { [n.id]: n })); state.notes.reduce((acc, note) => {
return acc; note.notes.every(n => Object.assign(acc, { [n.id]: n }));
}, {}); return acc;
}, {});
const reverseNotes = array => array.slice(0).reverse(); const reverseNotes = array => array.slice(0).reverse();
const isLastNote = (note, state) => !note.system && const isLastNote = (note, state) =>
state.userData && note.author && !note.system &&
state.userData &&
note.author &&
note.author.id === state.userData.id; note.author.id === state.userData.id;
export const getCurrentUserLastNote = state => _.flatten( export const getCurrentUserLastNote = state =>
reverseNotes(state.notes) _.flatten(
.map(note => reverseNotes(note.notes)), reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state)); ).find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) export const getDiscussionLastNote = state => discussion =>
.find(el => isLastNote(el, state)); reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = (state) => { export const discussionCount = state => {
const discussions = state.notes.filter(n => !n.individual_note); const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length; return discussions.length;
...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => { ...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
}; };
export const resolvedDiscussionsById = (state) => { export const resolvedDiscussionsById = state => {
const map = {}; const map = {};
state.notes.forEach((n) => { state.notes.forEach(n => {
if (n.notes) { if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system); const resolved = n.notes.every(note => note.resolved && !note.system);
......
...@@ -12,6 +12,9 @@ export default new Vuex.Store({ ...@@ -12,6 +12,9 @@ export default new Vuex.Store({
targetNoteHash: null, targetNoteHash: null,
lastFetchedAt: null, lastFetchedAt: null,
// View layer
isToggleStateButtonLoading: false,
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: {}, notesData: {},
userData: {}, userData: {},
......
...@@ -17,3 +17,4 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; ...@@ -17,3 +17,4 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
// Issue // Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) { [types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note; const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id); const [exists] = state.notes.filter(n => n.id === note.discussion_id);
const isDiscussion = (type === constants.DISCUSSION_NOTE); const isDiscussion = type === constants.DISCUSSION_NOTE;
if (!exists) { if (!exists) {
const noteData = { const noteData = {
...@@ -63,13 +63,15 @@ export default { ...@@ -63,13 +63,15 @@ export default {
const note = notes[i]; const note = notes[i];
const children = note.notes; const children = note.notes;
if (children.length && !note.individual_note) { // remove placeholder from discussions if (children.length && !note.individual_note) {
// remove placeholder from discussions
for (let j = children.length - 1; j >= 0; j -= 1) { for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) { if (children[j].isPlaceholderNote) {
children.splice(j, 1); children.splice(j, 1);
} }
} }
} else if (note.isPlaceholderNote) { // remove placeholders from state root } else if (note.isPlaceholderNote) {
// remove placeholders from state root
notes.splice(i, 1); notes.splice(i, 1);
} }
} }
...@@ -89,10 +91,10 @@ export default { ...@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) { [types.SET_INITIAL_NOTES](state, notesData) {
const notes = []; const notes = [];
notesData.forEach((note) => { notesData.forEach(note => {
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) { if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => { note.notes.forEach(n => {
notes.push({ notes.push({
...note, ...note,
notes: [n], // override notes array to only have one item to mimick individual_note notes: [n], // override notes array to only have one item to mimick individual_note
...@@ -103,7 +105,7 @@ export default { ...@@ -103,7 +105,7 @@ export default {
notes.push({ notes.push({
...note, ...note,
expanded: (oldNote ? oldNote.expanded : note.expanded), expanded: oldNote ? oldNote.expanded : note.expanded,
}); });
} }
}); });
...@@ -128,7 +130,9 @@ export default { ...@@ -128,7 +130,9 @@ export default {
notesArr.push({ notesArr.push({
individual_note: true, individual_note: true,
isPlaceholderNote: true, isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, placeholderType: data.isSystemNote
? constants.SYSTEM_NOTE
: constants.NOTE,
notes: [ notes: [
{ {
body: data.noteBody, body: data.noteBody,
...@@ -141,12 +145,16 @@ export default { ...@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data; const { awardName, note } = data;
const { id, name, username } = state.userData; const { id, name, username } = state.userData;
const hasEmojiAwardedByCurrentUser = note.award_emoji const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
.filter(emoji => emoji.name === data.awardName && emoji.user.id === id); emoji => emoji.name === data.awardName && emoji.user.id === id,
);
if (hasEmojiAwardedByCurrentUser.length) { if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it. // If current user has awarded this emoji, remove it.
note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1); note.award_emoji.splice(
note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
1,
);
} else { } else {
note.award_emoji.push({ note.award_emoji.push({
name: awardName, name: awardName,
...@@ -199,4 +207,8 @@ export default { ...@@ -199,4 +207,8 @@ export default {
[types.REOPEN_ISSUE](state) { [types.REOPEN_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.REOPENED }); Object.assign(state.noteableData, { state: constants.REOPENED });
}, },
[types.TOGGLE_STATE_BUTTON_LOADING](state, value) {
Object.assign(state, { isToggleStateButtonLoading: value });
},
}; };
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
---
title: Adds the option to the project export API to override the project description and display GitLab export description once imported
merge_request: 17744
author:
type: added
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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