Merge remote-tracking branch 'origin/master' into ide
This commit is contained in:
commit
6448368b5b
|
@ -11,6 +11,7 @@
|
|||
"gon": false,
|
||||
"localStorage": false
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"filenames",
|
||||
"import",
|
||||
|
|
|
@ -3,3 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb
|
|||
lib/gitlab/diff/position_tracer.rb
|
||||
app/policies/project_policy.rb
|
||||
app/models/concerns/relative_positioning.rb
|
||||
lib/gitlab/redis/*.rb
|
||||
|
|
|
@ -21,6 +21,7 @@ eslint-report.html
|
|||
/.yarn-cache
|
||||
/.byebug_history
|
||||
/Vagrantfile
|
||||
/app/assets/javascripts/locale/**/app.js
|
||||
/backups/*
|
||||
/config/aws.yml
|
||||
/config/database.yml
|
||||
|
@ -30,6 +31,9 @@ eslint-report.html
|
|||
/config/initializers/smtp_settings.rb
|
||||
/config/initializers/relative_url.rb
|
||||
/config/resque.yml
|
||||
/config/redis.cache.yml
|
||||
/config/redis.queues.yml
|
||||
/config/redis.shared_state.yml
|
||||
/config/unicorn.rb
|
||||
/config/secrets.yml
|
||||
/config/sidekiq.yml
|
||||
|
@ -59,3 +63,4 @@ eslint-report.html
|
|||
/.gitlab_workhorse_secret
|
||||
/webpack-report/
|
||||
/locale/**/LC_MESSAGES
|
||||
/.rspec
|
||||
|
|
230
.gitlab-ci.yml
230
.gitlab-ci.yml
|
@ -1,10 +1,20 @@
|
|||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-postgresql-9.6"
|
||||
|
||||
cache:
|
||||
.default-cache: &default-cache
|
||||
key: "ruby-233-with-yarn"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
|
||||
.push-cache: &push-cache
|
||||
cache:
|
||||
<<: *default-cache
|
||||
policy: push
|
||||
|
||||
.pull-cache: &pull-cache
|
||||
cache:
|
||||
<<: *default-cache
|
||||
policy: pull
|
||||
|
||||
variables:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
|
||||
|
@ -24,11 +34,11 @@ before_script:
|
|||
- source scripts/prepare_build.sh
|
||||
|
||||
stages:
|
||||
- build
|
||||
- prepare
|
||||
- test
|
||||
- post-test
|
||||
- pages
|
||||
- build
|
||||
- prepare
|
||||
- test
|
||||
- post-test
|
||||
- pages
|
||||
|
||||
# Predefined scopes
|
||||
.dedicated-runner: &dedicated-runner
|
||||
|
@ -41,10 +51,6 @@ stages:
|
|||
SETUP_DB: "false"
|
||||
USE_BUNDLE_INSTALL: "false"
|
||||
KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
|
||||
cache:
|
||||
key: "knapsack"
|
||||
paths:
|
||||
- knapsack/
|
||||
artifacts:
|
||||
expire_in: 31d
|
||||
paths:
|
||||
|
@ -63,7 +69,7 @@ stages:
|
|||
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
|
||||
only:
|
||||
- /mysql/
|
||||
- /-stable$/
|
||||
- /-stable/
|
||||
- master@gitlab-org/gitlab-ce
|
||||
- master@gitlab/gitlabhq
|
||||
- tags@gitlab-org/gitlab-ce
|
||||
|
@ -79,8 +85,9 @@ stages:
|
|||
- /(^docs[\/-].*|.*-docs$)/
|
||||
|
||||
.rspec-knapsack: &rspec-knapsack
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
script:
|
||||
- JOB_NAME=( $CI_JOB_NAME )
|
||||
- export CI_NODE_INDEX=${JOB_NAME[-2]}
|
||||
|
@ -110,8 +117,9 @@ stages:
|
|||
<<: *except-docs
|
||||
|
||||
.spinach-knapsack: &spinach-knapsack
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
script:
|
||||
- JOB_NAME=( $CI_JOB_NAME )
|
||||
- export CI_NODE_INDEX=${JOB_NAME[-2]}
|
||||
|
@ -157,9 +165,13 @@ build-package:
|
|||
SETUP_DB: "false"
|
||||
USE_BUNDLE_INSTALL: "false"
|
||||
stage: build
|
||||
cache: {}
|
||||
when: manual
|
||||
script:
|
||||
- scripts/trigger-build
|
||||
only:
|
||||
- //@gitlab-org/gitlab-ce
|
||||
- //@gitlab-org/gitlab-ee
|
||||
|
||||
# Prepare and merge knapsack tests
|
||||
knapsack:
|
||||
|
@ -167,6 +179,11 @@ knapsack:
|
|||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
stage: prepare
|
||||
cache:
|
||||
key: knapsack
|
||||
paths:
|
||||
- knapsack/
|
||||
policy: pull
|
||||
script:
|
||||
- mkdir -p knapsack/${CI_PROJECT_NAME}/
|
||||
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
|
||||
|
@ -179,7 +196,13 @@ update-knapsack:
|
|||
<<: *dedicated-runner
|
||||
<<: *only-canonical-masters
|
||||
stage: post-test
|
||||
cache:
|
||||
key: knapsack
|
||||
paths:
|
||||
- knapsack/
|
||||
policy: push
|
||||
script:
|
||||
- retry gem install fog-aws mime-types
|
||||
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
|
||||
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
|
||||
- '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
|
||||
|
@ -190,9 +213,12 @@ setup-test-env:
|
|||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
stage: prepare
|
||||
cache:
|
||||
<<: *default-cache
|
||||
script:
|
||||
- node --version
|
||||
- yarn install --pure-lockfile --cache-folder .yarn-cache
|
||||
- bundle exec rake gettext:po_to_json
|
||||
- bundle exec rake gitlab:assets:compile
|
||||
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
|
||||
artifacts:
|
||||
|
@ -202,72 +228,73 @@ setup-test-env:
|
|||
- public/assets
|
||||
- tmp/tests
|
||||
|
||||
rspec-pg 0 20: *rspec-knapsack-pg
|
||||
rspec-pg 1 20: *rspec-knapsack-pg
|
||||
rspec-pg 2 20: *rspec-knapsack-pg
|
||||
rspec-pg 3 20: *rspec-knapsack-pg
|
||||
rspec-pg 4 20: *rspec-knapsack-pg
|
||||
rspec-pg 5 20: *rspec-knapsack-pg
|
||||
rspec-pg 6 20: *rspec-knapsack-pg
|
||||
rspec-pg 7 20: *rspec-knapsack-pg
|
||||
rspec-pg 8 20: *rspec-knapsack-pg
|
||||
rspec-pg 9 20: *rspec-knapsack-pg
|
||||
rspec-pg 10 20: *rspec-knapsack-pg
|
||||
rspec-pg 11 20: *rspec-knapsack-pg
|
||||
rspec-pg 12 20: *rspec-knapsack-pg
|
||||
rspec-pg 13 20: *rspec-knapsack-pg
|
||||
rspec-pg 14 20: *rspec-knapsack-pg
|
||||
rspec-pg 15 20: *rspec-knapsack-pg
|
||||
rspec-pg 16 20: *rspec-knapsack-pg
|
||||
rspec-pg 17 20: *rspec-knapsack-pg
|
||||
rspec-pg 18 20: *rspec-knapsack-pg
|
||||
rspec-pg 19 20: *rspec-knapsack-pg
|
||||
rspec-pg 0 25: *rspec-knapsack-pg
|
||||
rspec-pg 1 25: *rspec-knapsack-pg
|
||||
rspec-pg 2 25: *rspec-knapsack-pg
|
||||
rspec-pg 3 25: *rspec-knapsack-pg
|
||||
rspec-pg 4 25: *rspec-knapsack-pg
|
||||
rspec-pg 5 25: *rspec-knapsack-pg
|
||||
rspec-pg 6 25: *rspec-knapsack-pg
|
||||
rspec-pg 7 25: *rspec-knapsack-pg
|
||||
rspec-pg 8 25: *rspec-knapsack-pg
|
||||
rspec-pg 9 25: *rspec-knapsack-pg
|
||||
rspec-pg 10 25: *rspec-knapsack-pg
|
||||
rspec-pg 11 25: *rspec-knapsack-pg
|
||||
rspec-pg 12 25: *rspec-knapsack-pg
|
||||
rspec-pg 13 25: *rspec-knapsack-pg
|
||||
rspec-pg 14 25: *rspec-knapsack-pg
|
||||
rspec-pg 15 25: *rspec-knapsack-pg
|
||||
rspec-pg 16 25: *rspec-knapsack-pg
|
||||
rspec-pg 17 25: *rspec-knapsack-pg
|
||||
rspec-pg 18 25: *rspec-knapsack-pg
|
||||
rspec-pg 19 25: *rspec-knapsack-pg
|
||||
rspec-pg 20 25: *rspec-knapsack-pg
|
||||
rspec-pg 21 25: *rspec-knapsack-pg
|
||||
rspec-pg 22 25: *rspec-knapsack-pg
|
||||
rspec-pg 23 25: *rspec-knapsack-pg
|
||||
rspec-pg 24 25: *rspec-knapsack-pg
|
||||
|
||||
rspec-mysql 0 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 1 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 2 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 3 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 4 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 5 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 6 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 7 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 8 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 9 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 10 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 11 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 12 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 13 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 14 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 15 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 16 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 17 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 18 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 19 20: *rspec-knapsack-mysql
|
||||
rspec-mysql 0 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 1 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 2 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 3 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 4 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 5 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 6 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 7 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 8 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 9 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 10 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 11 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 12 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 13 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 14 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 15 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 16 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 17 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 18 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 19 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 20 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 21 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 22 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 23 25: *rspec-knapsack-mysql
|
||||
rspec-mysql 24 25: *rspec-knapsack-mysql
|
||||
|
||||
spinach-pg 0 10: *spinach-knapsack-pg
|
||||
spinach-pg 1 10: *spinach-knapsack-pg
|
||||
spinach-pg 2 10: *spinach-knapsack-pg
|
||||
spinach-pg 3 10: *spinach-knapsack-pg
|
||||
spinach-pg 4 10: *spinach-knapsack-pg
|
||||
spinach-pg 5 10: *spinach-knapsack-pg
|
||||
spinach-pg 6 10: *spinach-knapsack-pg
|
||||
spinach-pg 7 10: *spinach-knapsack-pg
|
||||
spinach-pg 8 10: *spinach-knapsack-pg
|
||||
spinach-pg 9 10: *spinach-knapsack-pg
|
||||
spinach-pg 0 5: *spinach-knapsack-pg
|
||||
spinach-pg 1 5: *spinach-knapsack-pg
|
||||
spinach-pg 2 5: *spinach-knapsack-pg
|
||||
spinach-pg 3 5: *spinach-knapsack-pg
|
||||
spinach-pg 4 5: *spinach-knapsack-pg
|
||||
|
||||
spinach-mysql 0 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 1 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 2 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 3 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 4 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 5 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 6 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 7 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 8 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 9 10: *spinach-knapsack-mysql
|
||||
spinach-mysql 0 5: *spinach-knapsack-mysql
|
||||
spinach-mysql 1 5: *spinach-knapsack-mysql
|
||||
spinach-mysql 2 5: *spinach-knapsack-mysql
|
||||
spinach-mysql 3 5: *spinach-knapsack-mysql
|
||||
spinach-mysql 4 5: *spinach-knapsack-mysql
|
||||
|
||||
# Static analysis jobs
|
||||
.ruby-static-analysis: &ruby-static-analysis
|
||||
<<: *pull-cache
|
||||
variables:
|
||||
SIMPLECOV: "false"
|
||||
SETUP_DB: "false"
|
||||
|
@ -276,6 +303,7 @@ spinach-mysql 9 10: *spinach-knapsack-mysql
|
|||
<<: *ruby-static-analysis
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake $CI_JOB_NAME
|
||||
|
@ -292,9 +320,9 @@ static-analysis:
|
|||
# - Check validity of relative links
|
||||
# - Make sure cURL examples in API docs use the full switches
|
||||
docs lint:
|
||||
<<: *dedicated-runner
|
||||
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
cache: {}
|
||||
dependencies: []
|
||||
before_script: []
|
||||
|
@ -337,9 +365,10 @@ ee_compat_check:
|
|||
|
||||
# DB migration, rollback, and seed jobs
|
||||
.db-migrate-reset: &db-migrate-reset
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake db:migrate:reset
|
||||
|
||||
|
@ -352,15 +381,17 @@ db:migrate:reset-mysql:
|
|||
<<: *use-mysql
|
||||
|
||||
.migration-paths: &migration-paths
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *only-canonical-masters
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
<<: *only-canonical-masters
|
||||
script:
|
||||
- git fetch origin v8.14.10
|
||||
- git checkout -f FETCH_HEAD
|
||||
- bundle install $BUNDLE_INSTALL_FLAGS
|
||||
- cp config/gitlab.yml.example config/gitlab.yml
|
||||
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
|
||||
- git checkout $CI_COMMIT_SHA
|
||||
- bundle install $BUNDLE_INSTALL_FLAGS
|
||||
|
@ -376,9 +407,10 @@ migration:path-mysql:
|
|||
<<: *use-mysql
|
||||
|
||||
.db-rollback: &db-rollback
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
script:
|
||||
- bundle exec rake db:rollback STEP=120
|
||||
- bundle exec rake db:migrate
|
||||
|
@ -392,9 +424,10 @@ db:rollback-mysql:
|
|||
<<: *use-mysql
|
||||
|
||||
.db-seed_fu: &db-seed_fu
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
variables:
|
||||
SIZE: "1"
|
||||
SETUP_DB: "false"
|
||||
|
@ -419,9 +452,10 @@ db:seed_fu-mysql:
|
|||
|
||||
# Frontend-related jobs
|
||||
gitlab:assets:compile:
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: test
|
||||
dependencies: []
|
||||
variables:
|
||||
NODE_ENV: "production"
|
||||
|
@ -433,23 +467,26 @@ gitlab:assets:compile:
|
|||
NO_COMPRESSION: "true"
|
||||
script:
|
||||
- yarn install --pure-lockfile --production --cache-folder .yarn-cache
|
||||
- bundle exec rake gettext:po_to_json
|
||||
- bundle exec rake gitlab:assets:compile
|
||||
artifacts:
|
||||
name: webpack-report
|
||||
expire_in: 31d
|
||||
paths:
|
||||
- webpack-report/
|
||||
- webpack-report/
|
||||
|
||||
karma:
|
||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6"
|
||||
stage: test
|
||||
<<: *use-pg
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6"
|
||||
stage: test
|
||||
variables:
|
||||
BABEL_ENV: "coverage"
|
||||
CHROME_LOG_FILE: "chrome_debug.log"
|
||||
script:
|
||||
- bundle exec rake gettext:po_to_json
|
||||
- bundle exec rake karma
|
||||
coverage: '/^Statements *: (\d+\.\d+%)/'
|
||||
artifacts:
|
||||
|
@ -462,6 +499,7 @@ karma:
|
|||
|
||||
codeclimate:
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
before_script: []
|
||||
image: docker:latest
|
||||
stage: test
|
||||
|
@ -471,16 +509,17 @@ codeclimate:
|
|||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- docker pull codeclimate/codeclimate
|
||||
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
|
||||
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
|
||||
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
|
||||
artifacts:
|
||||
paths: [codeclimate.json]
|
||||
|
||||
coverage:
|
||||
stage: post-test
|
||||
services: []
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: post-test
|
||||
services: []
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
USE_BUNDLE_INSTALL: "true"
|
||||
|
@ -497,6 +536,7 @@ coverage:
|
|||
lint:javascript:report:
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
<<: *pull-cache
|
||||
stage: post-test
|
||||
before_script: []
|
||||
script:
|
||||
|
@ -509,9 +549,10 @@ lint:javascript:report:
|
|||
- eslint-report.html
|
||||
|
||||
pages:
|
||||
<<: *dedicated-runner
|
||||
<<: *pull-cache
|
||||
before_script: []
|
||||
stage: pages
|
||||
<<: *dedicated-runner
|
||||
dependencies:
|
||||
- coverage
|
||||
- karma
|
||||
|
@ -535,6 +576,7 @@ pages:
|
|||
# rubygems.org in the future.
|
||||
cache gems:
|
||||
<<: *dedicated-runner
|
||||
<<: *pull-cache
|
||||
only:
|
||||
- tags
|
||||
variables:
|
||||
|
@ -547,3 +589,11 @@ cache gems:
|
|||
only:
|
||||
- master@gitlab-org/gitlab-ce
|
||||
- master@gitlab-org/gitlab-ee
|
||||
|
||||
gitlab_git_test:
|
||||
<<: *pull-cache
|
||||
<<: *except-docs
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
script:
|
||||
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
|
||||
|
|
10
.rubocop.yml
10
.rubocop.yml
|
@ -965,6 +965,10 @@ RSpec/AnyInstance:
|
|||
RSpec/BeEql:
|
||||
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
|
||||
# module.
|
||||
RSpec/DescribeClass:
|
||||
|
@ -1024,6 +1028,12 @@ RSpec/FilePath:
|
|||
RSpec/Focus:
|
||||
Enabled: true
|
||||
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: is_expected, should
|
||||
RSpec/ImplicitExpect:
|
||||
Enabled: true
|
||||
EnforcedStyle: is_expected
|
||||
|
||||
# Checks for the usage of instance variables.
|
||||
RSpec/InstanceVariable:
|
||||
Enabled: false
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 54
|
||||
RSpec/BeforeAfterAll:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 233
|
||||
RSpec/EmptyLineAfterFinalLet:
|
||||
Enabled: false
|
||||
|
@ -24,12 +20,6 @@ RSpec/EmptyLineAfterSubject:
|
|||
RSpec/HookArgument:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 12
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: is_expected, should
|
||||
RSpec/ImplicitExpect:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 11
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: it_behaves_like, it_should_behave_like
|
||||
|
|
|
@ -10,7 +10,7 @@ linters:
|
|||
# Reports when you use improper spacing around ! (the "bang") in !default,
|
||||
# !global, !important, and !optional flags.
|
||||
BangFormat:
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
# Whether or not to prefer `border: 0` over `border: none`.
|
||||
BorderZero:
|
||||
|
@ -43,10 +43,11 @@ linters:
|
|||
# Rule sets should be ordered as follows:
|
||||
# - @extend declarations
|
||||
# - @include declarations without inner @content
|
||||
# - properties, @include declarations with inner @content
|
||||
# - properties
|
||||
# - @include declarations with inner @content
|
||||
# - nested rule sets.
|
||||
DeclarationOrder:
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
# `scss-lint:disable` control comments should be preceded by a comment
|
||||
# explaining why these linters are being disabled for this file.
|
||||
|
@ -93,7 +94,7 @@ linters:
|
|||
# The basenames of @imported SCSS partials should not begin with an
|
||||
# underscore and should not include the filename extension.
|
||||
ImportPath:
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
# Avoid using !important in properties. It is usually indicative of a
|
||||
# misunderstanding of CSS specificity and can lead to brittle code.
|
||||
|
@ -133,7 +134,7 @@ linters:
|
|||
# Reports when you use an unknown or disabled CSS property
|
||||
# (ignoring vendor-prefixed properties).
|
||||
PropertySpelling:
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
# Configure which units are allowed for property values.
|
||||
PropertyUnits:
|
||||
|
@ -176,6 +177,10 @@ linters:
|
|||
|
||||
# Commas in lists should be followed by a space.
|
||||
SpaceAfterComma:
|
||||
enabled: true
|
||||
|
||||
# Comment literals should be followed by a space.
|
||||
SpaceAfterComment:
|
||||
enabled: false
|
||||
|
||||
# Properties should be formatted with a single space separating the colon
|
||||
|
@ -240,7 +245,7 @@ linters:
|
|||
# Do not use parent selector references (&) when they would otherwise
|
||||
# be unnecessary.
|
||||
UnnecessaryParentReference:
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
# URLs should be valid and not contain protocols or domain names.
|
||||
UrlFormat:
|
||||
|
|
293
CHANGELOG.md
293
CHANGELOG.md
|
@ -2,6 +2,281 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 9.3.8 (2017-07-19)
|
||||
|
||||
- Improve support for external issue references. !12485
|
||||
- Renders 404 if given project is not readable by the user on Todos dashboard.
|
||||
- Use uploads/system directory for personal snippets.
|
||||
- Remove uploads/appearance symlink. A leftover from a previous migration.
|
||||
|
||||
## 9.3.7 (2017-07-18)
|
||||
|
||||
- Prevent bad data being added to application settings when Redis is unavailable. !12750
|
||||
- Return `is_admin` attribute in the GET /user endpoint for admins. !12811
|
||||
|
||||
## 9.3.6 (2017-07-12)
|
||||
|
||||
- Fix API Scoping. !12300
|
||||
- Username and password are no longer stripped from import url on mirror update. !12725
|
||||
- Fix issues with non-UTF8 filenames by always fixing the encoding of tree and blob paths.
|
||||
- Fixed GFM references not being included when updating issues inline.
|
||||
|
||||
## 9.3.5 (2017-07-05)
|
||||
|
||||
- Remove "Remove from board" button from backlog and closed list. !12430
|
||||
- Do not delete protected branches when deleting all merged branches. !12624
|
||||
- Set default for Remove source branch to false.
|
||||
- Prevent accidental deletion of protected MR source branch by repeating checks before actual deletion.
|
||||
- Expires full_path cache after a repository is renamed/transferred.
|
||||
|
||||
## 9.3.4 (2017-07-03)
|
||||
|
||||
- Update gitlab-shell to 5.1.1 !12615
|
||||
|
||||
## 9.3.3 (2017-06-30)
|
||||
|
||||
- Fix head pipeline stored in merge request for external pipelines. !12478
|
||||
- Bring back branches badge to main project page. !12548
|
||||
- Fix diff of requirements.txt file by not matching newlines as part of package names.
|
||||
- Perform housekeeping only when an import of a fresh project is completed.
|
||||
- Fixed issue boards closed list not showing all closed issues.
|
||||
- Fixed multi-line markdown tooltip buttons in issue edit form.
|
||||
|
||||
## 9.3.2 (2017-06-27)
|
||||
|
||||
- API: Fix optional arugments for POST :id/variables. !12474
|
||||
- Bump premailer-rails gem to 1.9.7 and its dependencies to prevent network retrieval of assets.
|
||||
|
||||
## 9.3.1 (2017-06-26)
|
||||
|
||||
- Fix reversed breadcrumb order for nested groups. !12322
|
||||
- Fix 500 when failing to create private group. !12394
|
||||
- Fix linking to line number on side-by-side diff creating empty discussion box.
|
||||
- Don't match tilde and exclamation mark as part of requirements.txt package name.
|
||||
- Perform project housekeeping after importing projects.
|
||||
- Fixed ctrl+enter not submit issue edit form.
|
||||
|
||||
## 9.3.0 (2017-06-22)
|
||||
|
||||
- Refactored gitlab:app:check into SystemCheck liberary and improve some checks. !9173
|
||||
- Add an ability to cancel attaching file and redesign attaching files UI. !9431 (blackst0ne)
|
||||
- Add Aliyun OSS as the backup storage provider. !9721 (Yuanfei Zhu)
|
||||
- Add suport for find_local_branches GRPC from Gitaly. !10059
|
||||
- Allow manual bypass of auto_sign_in_with_provider with a new param. !10187 (Maxime Besson)
|
||||
- Redirect to user's keys index instead of user's index after a key is deleted in the admin. !10227 (Cyril Jouve)
|
||||
- Changed Blame to Annotate in the UI to promote blameless culture. !10378 (Ilya Vassilevsky)
|
||||
- Implement ability to update deploy keys. !10383 (Alexander Randa)
|
||||
- Allow numeric values in gitlab-ci.yml. !10607 (blackst0ne)
|
||||
- Add a feature test for Unicode trace. !10736 (dosuken123)
|
||||
- Notes: Warning message should go away once resolved. !10823 (Jacopo Beschi @jacopo-beschi)
|
||||
- Project authorizations are calculated much faster when using PostgreSQL, and nested groups support for MySQL has been removed
|
||||
. !10885
|
||||
- Fix long urls in the title of commit. !10938 (Alexander Randa)
|
||||
- Update gem sidekiq-cron from 0.4.4 to 0.6.0 and rufus-scheduler from 3.1.10 to 3.4.0. !10976 (dosuken123)
|
||||
- Use relative paths for group/project/user avatars. !11001 (blackst0ne)
|
||||
- Enable cancelling non-HEAD pending pipelines by default for all projects. !11023
|
||||
- Implement web hook logging. !11027 (Alexander Randa)
|
||||
- Add indices for auto_canceled_by_id for ci_pipelines and ci_builds on PostgreSQL. !11034
|
||||
- Add post-deploy migration to clean up projects in `pending_delete` state. !11044
|
||||
- Limit User's trackable attributes, like `current_sign_in_at`, to update at most once/hour. !11053
|
||||
- Disallow multiple selections for Milestone dropdown. !11084
|
||||
- Link to commit author user page from pipelines. !11100
|
||||
- Fix the last coverage in trace log should be extracted. !11128 (dosuken123)
|
||||
- Remove redirect for old issue url containing id instead of iid. !11135 (blackst0ne)
|
||||
- Backported new SystemHook event: `repository_update`. !11140
|
||||
- Keep input data after creating a tag that already exists. !11155
|
||||
- Fix support for external CI services. !11176
|
||||
- Translate backend for Project & Repository pages. !11183
|
||||
- Fix LaTeX formatting for AsciiDoc wiki. !11212
|
||||
- Add foreign key for pipeline schedule owner. !11233
|
||||
- Print Go version in rake gitlab:env:info. !11241
|
||||
- Include the blob content when printing a blob page. !11247
|
||||
- Sync email address from specified omniauth provider. !11268 (Robin Bobbitt)
|
||||
- Disable reference prefixes in notes for Snippets. !11278
|
||||
- Rename build_events to job_events. !11287
|
||||
- Add API support for pipeline schedule. !11307 (dosuken123)
|
||||
- Use route.cache_key for project list cache key. !11325
|
||||
- Make environment table realtime. !11333
|
||||
- Cache npm modules between pipelines with yarn to speed up setup-test-env. !11343
|
||||
- Allow GitLab instance to start when InfluxDB hostname cannot be resolved. !11356
|
||||
- Add ConvDev Index page to admin area. !11377
|
||||
- Fix Git-over-HTTP error statuses and improve error messages. !11398
|
||||
- Renamed users 'Audit Log'' to 'Authentication Log'. !11400
|
||||
- Style people in issuable search bar. !11402
|
||||
- Change /builds in the URL to /-/jobs. Backward URLs were also added. !11407
|
||||
- Update password field label while editing service settings. !11431
|
||||
- Add an optional performance bar to view performance metrics for the current page. !11439
|
||||
- Update task_list to version 2.0.0. !11525 (Jared Deckard <jared.deckard@gmail.com>)
|
||||
- Avoid resource intensive login checks if password is not provided. !11537 (Horatiu Eugen Vlad)
|
||||
- Allow numeric pages domain. !11550
|
||||
- Exclude manual actions when checking if pipeline can be canceled. !11562
|
||||
- Add server uptime to System Info page in admin dashboard. !11590 (Justin Boltz)
|
||||
- Simplify testing and saving service integrations. !11599
|
||||
- Fixed handling of the `can_push` attribute in the v3 deploy_keys api. !11607 (Richard Clamp)
|
||||
- Improve user experience around slash commands in instant comments. !11612
|
||||
- Show current user immediately in issuable filters. !11630
|
||||
- Add extra context-sensitive functionality for the top right menu button. !11632
|
||||
- Reorder Issue action buttons in order of usability. !11642
|
||||
- Expose atom links with an RSS token instead of using the private token. !11647 (Alexis Reigel)
|
||||
- Respect merge, instead of push, permissions for protected actions. !11648
|
||||
- Job details page update real time. !11651
|
||||
- Improve performance of ProjectFinder used in /projects API endpoint. !11666
|
||||
- Remove redundant data-turbolink attributes from links. !11672 (blackst0ne)
|
||||
- Minimum postgresql version is now 9.2. !11677
|
||||
- Add protected variables which would only be passed to protected branches or protected tags. !11688
|
||||
- Introduce optimistic locking support via optional parameter last_commit_sha on File Update API. !11694 (electroma)
|
||||
- Add $CI_ENVIRONMENT_URL to predefined variables for pipelines. !11695
|
||||
- Simplify project repository settings page. !11698
|
||||
- Fix pipeline_schedules pages throwing error 500. !11706 (dosuken123)
|
||||
- Add performance deltas between app deployments on Merge Request widget. !11730
|
||||
- Add feature toggles and API endpoints for admins. !11747
|
||||
- Replace 'starred_projects.feature' spinach test with an rspec analog. !11752 (blackst0ne)
|
||||
- Introduce an Events API. !11755
|
||||
- Display Shared Runner status in Admin Dashboard. !11783 (Ivan Chernov)
|
||||
- Persist pipeline stages in the database. !11790
|
||||
- Revert the feature that would include the current user's username in the HTTP clone URL. !11792
|
||||
- Enable Gitaly by default in installations from source. !11796
|
||||
- Use zopfli compression for frontend assets. !11798
|
||||
- Add tag_list param to project api. !11799 (Ivan Chernov)
|
||||
- Add changelog for improved Registry description. !11816
|
||||
- Automatically adjust project settings to match changes in project visibility. !11831
|
||||
- Add slugify project path to CI enviroment variables. !11838 (Ivan Chernov)
|
||||
- Add all pipeline sources as special keywords to 'only' and 'except'. !11844 (Filip Krakowski)
|
||||
- Allow pulling of container images using personal access tokens. !11845
|
||||
- Expose import_status in Projects API. !11851 (Robin Bobbitt)
|
||||
- Allow admins to delete users from the admin users page. !11852
|
||||
- Allow users to be hard-deleted from the API. !11853
|
||||
- Fix hard-deleting users when they have authored issues. !11855
|
||||
- Fix missing optional path parameter in "Create project for user" API. !11868
|
||||
- Allow users to be hard-deleted from the admin panel. !11874
|
||||
- Add a Rake task to aid in rotating otp_key_base. !11881
|
||||
- Fix submodule link to then project under subgroup. !11906
|
||||
- Fix binary encoding error on MR diffs. !11929
|
||||
- Limit non-administrators to adding 100 members at a time to groups and projects. !11940
|
||||
- add bulgarian translation of cycle analytics page to I18N. !11958 (Lyubomir Vasilev)
|
||||
- Make backup task to continue on corrupt repositories. !11962
|
||||
- Fix incorrect ETag cache key when relative instance URL is used. !11964
|
||||
- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm)
|
||||
- Fix edit button for deploy keys available from other projects. !12301 (Alexander Randa)
|
||||
- Fix passing CI_ENVIRONMENT_NAME and CI_ENVIRONMENT_SLUG for CI_ENVIRONMENT_URL. !12344
|
||||
- Disable environment list refresh due to bug https://gitlab.com/gitlab-org/gitlab-ee/issues/2677. !12347
|
||||
- Standardize timeline note margins across different viewport sizes. !12364
|
||||
- Fix Ordered Task List Items. !31483 (Jared Deckard <jared.deckard@gmail.com>)
|
||||
- Upgrade dependency to Go 1.8.3. !31943
|
||||
- Add prometheus metrics on pipeline creation.
|
||||
- Fix etag route not being a match for environments.
|
||||
- Sort folder for environments.
|
||||
- Support descriptions for snippets.
|
||||
- Hide clone panel and file list when user is only a guest. (James Clark)
|
||||
- Don’t create comment on JIRA if it already exists for the entity.
|
||||
- Update Dashboard Groups UI with better support for subgroups.
|
||||
- Confirm Project forking behaviour via the API.
|
||||
- Add prometheus based metrics collection to gitlab webapp.
|
||||
- Fix: Wiki is not searchable with Guest permissions.
|
||||
- Center all empty states.
|
||||
- Remove 'New issue' button when issues search returns no results.
|
||||
- Add API URL to JIRA settings.
|
||||
- animate adding issue to boards.
|
||||
- Update session cookie key name to be unique to instance in development.
|
||||
- Single click on filter to open filtered search dropdown.
|
||||
- Makes header information of pipeline show page realtine.
|
||||
- Creates a mediator for pipeline details vue in order to mount several vue apps with the same data.
|
||||
- Scope issue/merge request recent searches to project.
|
||||
- Increase individual diff collapse limit to 100 KB, and render limit to 200 KB.
|
||||
- Fix Pipelines table empty state - only render empty state if we receive 0 pipelines.
|
||||
- Make New environment empty state btn lowercase.
|
||||
- Removes duplicate environment variable in documentation.
|
||||
- Change links in issuable meta to black.
|
||||
- Fix border-bottom for project activity tab.
|
||||
- Adds new icon for CI skipped status.
|
||||
- Create equal padding for emoji.
|
||||
- Use briefcase icon for company in profile page.
|
||||
- Remove overflow from comment form for confidential issues and vertically aligns confidential issue icon.
|
||||
- Keep trailing newline when resolving conflicts by picking sides.
|
||||
- Fix /unsubscribe slash command creating extra todos when you were already mentioned in an issue.
|
||||
- Fix math rendering on blob pages.
|
||||
- Allow group reporters to manage group labels.
|
||||
- Use pre-wrap for commit messages to keep lists indented.
|
||||
- Count badges depend on translucent color to better adjust to different background colors and permission badges now feature a pill shaped design similar to labels.
|
||||
- Allow reporters to promote project labels to group labels.
|
||||
- Enabled keyboard shortcuts on artifacts pages.
|
||||
- Perform filtered search when state tab is changed.
|
||||
- Remove duplication for sharing projects with groups in project settings.
|
||||
- Change order of commits ahead and behind on divergence graph for branch list view.
|
||||
- Creates CI Header component for Pipelines and Jobs details pages.
|
||||
- Invalidate cache for issue and MR counters more granularly.
|
||||
- disable blocked manual actions.
|
||||
- Load tree readme asynchronously.
|
||||
- Display extra info about files on .gitlab-ci.yml, .gitlab/route-map.yml and LICENSE blob pages.
|
||||
- Fix replying to a commit discussion displayed in the context of an MR.
|
||||
- Consistently use monospace font for commit SHAs and branch and tag names.
|
||||
- Consistently display last push event widget.
|
||||
- Don't copy empty elements that were not selected on purpose as GFM.
|
||||
- Copy as GFM even when parts of other elements are selected.
|
||||
- Autolink package names in Gemfile.
|
||||
- Resolve N+1 query issue with discussions.
|
||||
- Don't match email addresses or foo@bar as user references.
|
||||
- Fix title of discussion jump button at top of page.
|
||||
- Don't return nil for missing objects from parser cache.
|
||||
- Make .gitmodules parsing more resilient to syntax errors.
|
||||
- Add username parameter to gravatar URL.
|
||||
- Autolink package names in more dependency files.
|
||||
- Return nil when looking up config for unknown LDAP provider.
|
||||
- Add system note with link to diff comparison when MR discussion becomes outdated.
|
||||
- Don't wrap pasted code when it's already inside code tags.
|
||||
- Revert 'New file from interface on existing branch'.
|
||||
- Show last commit for current tree on tree page.
|
||||
- Add documentation about adding foreign keys.
|
||||
- add username field to push webhook. (David Turner)
|
||||
- Rename CI/CD Pipelines to Pipelines in the project settings.
|
||||
- Make environment tables responsive.
|
||||
- Expand/collapse backlog & closed lists in issue boards.
|
||||
- Fix GitHub importer performance on branch existence check.
|
||||
- Fix counter cache for acts as taggable.
|
||||
- Github - Fix token interpolation when cloning wiki repository.
|
||||
- Fix token interpolation when setting the Github remote.
|
||||
- Fix N+1 queries for non-members in comment threads.
|
||||
- Fix terminals support for Kubernetes Service.
|
||||
- Fix: A diff comment on a change at last line of a file shows as two comments in discussion.
|
||||
- Instrument MergeRequestDiff#load_commits.
|
||||
- Introduce source to Pipeline entity.
|
||||
- Fixed create new label form in issue form not working for sub-group projects.
|
||||
- Fixed style on unsubscribe page. (Gustav Ernberg)
|
||||
- Enables inline editing for an issues title & description.
|
||||
- Ask for an example project for bug reports.
|
||||
- Add summary lines for collapsed details in the bug report template.
|
||||
- Prevent commits from upstream repositories to be re-processed by forks.
|
||||
- Avoid repeated queries for pipeline builds on merge requests.
|
||||
- Preloads head pipeline for merge request collection.
|
||||
- Handle head pipeline when creating merge requests.
|
||||
- Migrate artifacts to a new path.
|
||||
- Rescue OpenSSL::SSL::SSLError in JiraService & IssueTrackerService.
|
||||
- Repository browser: handle in-repository submodule urls. (David Turner)
|
||||
- Prevent project transfers if a new group is not selected.
|
||||
- Allow 'no one' as an option for allowed to merge on a procted branch.
|
||||
- Reduce time spent waiting for certain Sidekiq jobs to complete.
|
||||
- Refactor ProjectsFinder#init_collection to produce more efficient queries for retrieving projects.
|
||||
- Remove unused code and uses underscore.
|
||||
- Restricts search projects dropdown to group projects when group is selected.
|
||||
- Properly handle container registry redirects to fix metadata stored on a S3 backend.
|
||||
- Fix LFS timeouts when trying to save large files.
|
||||
- Set artifact working directory to be in the destination store to prevent unnecessary I/O.
|
||||
- Strip trailing whitespaces in submodule URLs.
|
||||
- Make sure reCAPTCHA configuration is loaded when spam checks are initiated.
|
||||
- Fix up arrow not editing last discussion comment.
|
||||
- Added application readiness endpoints to the monitoring health check admin view.
|
||||
- Use wait_for_requests for both ajax and Vue requests.
|
||||
- Cleanup ci_variables schema and table.
|
||||
- Remove foreigh key on ci_trigger_schedules only if it exists.
|
||||
- Allow translation of Pipeline Schedules.
|
||||
|
||||
## 9.2.8 (2017-07-19)
|
||||
|
||||
- Improve support for external issue references. !12485
|
||||
- Renders 404 if given project is not readable by the user on Todos dashboard.
|
||||
- Fix incorrect project authorizations.
|
||||
- Remove uploads/appearance symlink. A leftover from a previous migration.
|
||||
|
||||
## 9.2.7 (2017-06-21)
|
||||
|
||||
- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm)
|
||||
|
@ -246,6 +521,13 @@ entry.
|
|||
- Fix preemptive scroll bar on user activity calendar.
|
||||
- Pipeline chat notifications convert seconds to minutes and hours.
|
||||
|
||||
## 9.1.8 (2017-07-19)
|
||||
|
||||
- Improve support for external issue references. !12485
|
||||
- Renders 404 if given project is not readable by the user on Todos dashboard.
|
||||
- Fix incorrect project authorizations.
|
||||
- Remove uploads/appearance symlink. A leftover from a previous migration.
|
||||
|
||||
## 9.1.7 (2017-06-07)
|
||||
|
||||
- No changes.
|
||||
|
@ -558,6 +840,12 @@ entry.
|
|||
- Only send chat notifications for the default branch.
|
||||
- Don't fill in the default kubernetes namespace.
|
||||
|
||||
## 9.0.11 (2017-07-19)
|
||||
|
||||
- Renders 404 if given project is not readable by the user on Todos dashboard.
|
||||
- Fix incorrect project authorizations.
|
||||
- Remove uploads/appearance symlink. A leftover from a previous migration.
|
||||
|
||||
## 9.0.10 (2017-06-07)
|
||||
|
||||
- No changes.
|
||||
|
@ -928,6 +1216,11 @@ entry.
|
|||
- Change development tanuki favicon colors to match logo color order.
|
||||
- API issues - support filtering by iids.
|
||||
|
||||
## 8.17.7 (2017-07-19)
|
||||
|
||||
- Renders 404 if given project is not readable by the user on Todos dashboard.
|
||||
- Fix incorrect project authorizations.
|
||||
|
||||
## 8.17.6 (2017-05-05)
|
||||
|
||||
- Enforce project features when searching blobs and wikis.
|
||||
|
|
|
@ -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
|
||||
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
|
||||
source edition, and GitLab Enterprise Edition (EE) which is our commercial
|
||||
edition. Throughout this guide you will see references to CE and EE for
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.12.0
|
||||
0.22.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.4.3
|
||||
0.5.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
5.0.5
|
||||
5.3.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.1.1
|
||||
3.0.0
|
||||
|
|
31
Gemfile
31
Gemfile
|
@ -2,7 +2,6 @@ source 'https://rubygems.org'
|
|||
|
||||
gem 'rails', '4.2.8'
|
||||
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
|
||||
gem 'bootsnap', '~> 1.0.0'
|
||||
|
||||
# Responders respond_to and respond_with
|
||||
gem 'responders', '~> 2.0'
|
||||
|
@ -13,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
|
|||
gem 'default_value_for', '~> 3.0.0'
|
||||
|
||||
# Supported DBs
|
||||
gem 'mysql2', '~> 0.3.16', group: :mysql
|
||||
gem 'mysql2', '~> 0.4.5', group: :mysql
|
||||
gem 'pg', '~> 0.18.2', group: :postgres
|
||||
|
||||
gem 'rugged', '~> 0.25.1.1'
|
||||
|
@ -38,7 +37,7 @@ gem 'omniauth-saml', '~> 1.7.0'
|
|||
gem 'omniauth-shibboleth', '~> 1.2.0'
|
||||
gem 'omniauth-twitter', '~> 1.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 'jwt', '~> 1.5.6'
|
||||
|
||||
|
@ -92,7 +91,7 @@ gem 'carrierwave', '~> 1.1'
|
|||
gem 'dropzonejs-rails', '~> 0.7.1'
|
||||
|
||||
# for backups
|
||||
gem 'fog-aws', '~> 0.9'
|
||||
gem 'fog-aws', '~> 1.4'
|
||||
gem 'fog-core', '~> 1.44'
|
||||
gem 'fog-google', '~> 0.5'
|
||||
gem 'fog-local', '~> 0.3'
|
||||
|
@ -123,6 +122,7 @@ gem 'asciidoctor', '~> 1.5.2'
|
|||
gem 'asciidoctor-plantuml', '0.0.7'
|
||||
gem 'rouge', '~> 2.0'
|
||||
gem 'truncato', '~> 0.7.8'
|
||||
gem 'bootstrap_form', '~> 2.7.0'
|
||||
|
||||
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
|
||||
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
|
||||
|
@ -163,6 +163,9 @@ gem 'rainbow', '~> 2.2'
|
|||
# GitLab settings
|
||||
gem 'settingslogic', '~> 2.0.9'
|
||||
|
||||
# Linear-time regex library for untrusted regular expressions
|
||||
gem 're2', '~> 1.0.0'
|
||||
|
||||
# Misc
|
||||
|
||||
gem 'version_sorter', '~> 2.1.0'
|
||||
|
@ -237,7 +240,6 @@ gem 'webpack-rails', '~> 0.9.10'
|
|||
gem 'rack-proxy', '~> 0.6.0'
|
||||
|
||||
gem 'sass-rails', '~> 5.0.6'
|
||||
gem 'coffee-rails', '~> 4.1.0'
|
||||
gem 'uglifier', '~> 2.7.2'
|
||||
|
||||
gem 'addressable', '~> 2.3.8'
|
||||
|
@ -250,13 +252,12 @@ gem 'jquery-rails', '~> 4.1.0'
|
|||
gem 'request_store', '~> 1.3'
|
||||
gem 'select2-rails', '~> 3.5.9'
|
||||
gem 'virtus', '~> 1.0.1'
|
||||
gem 'net-ssh', '~> 3.0.1'
|
||||
gem 'base32', '~> 0.3.0'
|
||||
|
||||
# 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
|
||||
gem 'ruby_parser', '~> 3.8', require: false
|
||||
|
@ -270,7 +271,7 @@ gem 'peek', '~> 1.0.1'
|
|||
gem 'peek-gc', '~> 0.0.2'
|
||||
gem 'peek-host', '~> 1.0.0'
|
||||
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-rblineprof', '~> 0.2.0'
|
||||
gem 'peek-redis', '~> 1.2.0'
|
||||
|
@ -283,7 +284,8 @@ group :metrics do
|
|||
gem 'influxdb', '~> 0.2', require: false
|
||||
|
||||
# Prometheus
|
||||
gem 'prometheus-client-mmap', '~>0.7.0.beta5'
|
||||
gem 'prometheus-client-mmap', '~>0.7.0.beta9'
|
||||
gem 'raindrops', '~> 0.18'
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
@ -334,7 +336,7 @@ group :development, :test do
|
|||
|
||||
gem 'rubocop', '~> 0.47.1', 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 'simplecov', '~> 0.14.0', require: false
|
||||
gem 'flay', '~> 2.8.0', require: false
|
||||
|
@ -354,7 +356,7 @@ group :test do
|
|||
gem 'shoulda-matchers', '~> 2.8.0', require: false
|
||||
gem 'email_spec', '~> 1.6.0'
|
||||
gem 'json-schema', '~> 2.6.2'
|
||||
gem 'webmock', '~> 1.24.0'
|
||||
gem 'webmock', '~> 2.3.2'
|
||||
gem 'test_after_commit', '~> 1.1'
|
||||
gem 'sham_rack', '~> 1.3.6'
|
||||
gem 'timecop', '~> 0.8.0'
|
||||
|
@ -384,10 +386,13 @@ gem 'vmstat', '~> 2.3.0'
|
|||
gem 'sys-filesystem', '~> 1.1.6'
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly', '~> 0.9.0'
|
||||
gem 'gitaly', '~> 0.17.0'
|
||||
|
||||
gem 'toml-rb', '~> 0.3.15', require: false
|
||||
|
||||
# Feature toggles
|
||||
gem 'flipper', '~> 0.10.2'
|
||||
gem 'flipper-active_record', '~> 0.10.2'
|
||||
|
||||
# Structured logging
|
||||
gem 'lograge', '~> 0.5'
|
||||
|
|
99
Gemfile.lock
99
Gemfile.lock
|
@ -83,11 +83,10 @@ GEM
|
|||
bindata (2.3.5)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.0.0)
|
||||
msgpack (~> 1.0)
|
||||
bootstrap-sass (3.3.6)
|
||||
autoprefixer-rails (>= 5.2.1)
|
||||
sass (>= 3.3.4)
|
||||
bootstrap_form (2.7.0)
|
||||
brakeman (3.6.1)
|
||||
browser (2.2.0)
|
||||
builder (3.2.3)
|
||||
|
@ -123,13 +122,6 @@ GEM
|
|||
coderay (1.1.1)
|
||||
coercible (1.0.0)
|
||||
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)
|
||||
concurrent-ruby (1.0.5)
|
||||
concurrent-ruby-ext (1.0.5)
|
||||
|
@ -138,7 +130,7 @@ GEM
|
|||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
creole (0.5.0)
|
||||
css_parser (1.4.1)
|
||||
css_parser (1.5.0)
|
||||
addressable
|
||||
d3_rails (3.5.11)
|
||||
railties (>= 3.1.0)
|
||||
|
@ -187,7 +179,7 @@ GEM
|
|||
et-orbi (1.0.3)
|
||||
tzinfo
|
||||
eventmachine (1.0.8)
|
||||
excon (0.55.0)
|
||||
excon (0.57.1)
|
||||
execjs (2.6.0)
|
||||
expression_parser (0.9.0)
|
||||
extlib (0.9.16)
|
||||
|
@ -223,26 +215,26 @@ GEM
|
|||
fog-json (~> 1.0)
|
||||
ipaddress (~> 0.8)
|
||||
xml-simple (~> 1.1)
|
||||
fog-aws (0.13.0)
|
||||
fog-aws (1.4.0)
|
||||
fog-core (~> 1.38)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
ipaddress (~> 0.8)
|
||||
fog-core (1.44.1)
|
||||
fog-core (1.44.3)
|
||||
builder
|
||||
excon (~> 0.49)
|
||||
formatador (~> 0.2)
|
||||
fog-google (0.5.0)
|
||||
fog-google (0.5.3)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-json (1.0.2)
|
||||
fog-core (~> 1.0)
|
||||
multi_json (~> 1.10)
|
||||
fog-local (0.3.0)
|
||||
fog-local (0.3.1)
|
||||
fog-core (~> 1.27)
|
||||
fog-openstack (0.1.6)
|
||||
fog-core (>= 1.39)
|
||||
fog-openstack (0.1.21)
|
||||
fog-core (>= 1.40)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
fog-rackspace (0.1.1)
|
||||
|
@ -277,7 +269,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly (0.9.0)
|
||||
gitaly (0.17.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (4.7.6)
|
||||
|
@ -353,7 +345,7 @@ GEM
|
|||
grape-entity (0.6.0)
|
||||
activesupport
|
||||
multi_json (>= 1.3.2)
|
||||
grpc (1.2.5)
|
||||
grpc (1.4.0)
|
||||
google-protobuf (~> 3.1)
|
||||
googleauth (~> 0.5.1)
|
||||
haml (4.0.7)
|
||||
|
@ -367,7 +359,7 @@ GEM
|
|||
temple (~> 0.7.6)
|
||||
thor
|
||||
tilt
|
||||
hashdiff (0.3.2)
|
||||
hashdiff (0.3.4)
|
||||
hashie (3.5.5)
|
||||
hashie-forbidden_attributes (0.1.1)
|
||||
hashie (>= 3.0)
|
||||
|
@ -451,6 +443,10 @@ GEM
|
|||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
lograge (0.5.1)
|
||||
actionpack (>= 4, < 5.2)
|
||||
activesupport (>= 4, < 5.2)
|
||||
railties (>= 4, < 5.2)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.5)
|
||||
|
@ -462,9 +458,8 @@ GEM
|
|||
mimemagic (0.3.0)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.7.0)
|
||||
mmap2 (2.2.6)
|
||||
mmap2 (2.2.7)
|
||||
mousetrap-rails (1.4.6)
|
||||
msgpack (1.1.0)
|
||||
multi_json (1.12.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
|
@ -472,9 +467,8 @@ GEM
|
|||
tool (~> 0.2)
|
||||
mustermann-grape (0.4.0)
|
||||
mustermann (= 0.4.0)
|
||||
mysql2 (0.3.20)
|
||||
mysql2 (0.4.5)
|
||||
net-ldap (0.12.1)
|
||||
net-ssh (3.0.1)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.6.8.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
|
@ -494,7 +488,7 @@ GEM
|
|||
rack (>= 1.0, < 3)
|
||||
omniauth-auth0 (1.4.1)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-authentiq (0.3.0)
|
||||
omniauth-authentiq (0.3.1)
|
||||
omniauth-oauth2 (~> 1.3, >= 1.3.1)
|
||||
omniauth-azure-oauth2 (0.0.6)
|
||||
jwt (~> 1.0)
|
||||
|
@ -563,7 +557,7 @@ GEM
|
|||
atomic (>= 1.0.0)
|
||||
mysql2
|
||||
peek
|
||||
peek-performance_bar (1.2.1)
|
||||
peek-performance_bar (1.3.0)
|
||||
peek (>= 0.1.0)
|
||||
peek-pg (1.3.0)
|
||||
concurrent-ruby
|
||||
|
@ -591,14 +585,15 @@ GEM
|
|||
websocket-driver (>= 0.2.0)
|
||||
posix-spawn (0.3.11)
|
||||
powerpack (0.1.1)
|
||||
premailer (1.8.6)
|
||||
css_parser (>= 1.3.6)
|
||||
premailer (1.10.4)
|
||||
addressable
|
||||
css_parser (>= 1.4.10)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.9.2)
|
||||
premailer-rails (1.9.7)
|
||||
actionmailer (>= 3, < 6)
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
prometheus-client-mmap (0.7.0.beta5)
|
||||
mmap2 (~> 2.2.6)
|
||||
prometheus-client-mmap (0.7.0.beta9)
|
||||
mmap2 (~> 2.2, >= 2.2.7)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
|
@ -656,12 +651,13 @@ GEM
|
|||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.17.0)
|
||||
raindrops (0.18.0)
|
||||
rake (10.5.0)
|
||||
rblineprof (0.3.6)
|
||||
debugger-ruby_core_source (~> 1.3)
|
||||
rdoc (4.2.2)
|
||||
json (~> 1.4)
|
||||
re2 (1.0.0)
|
||||
recaptcha (3.0.0)
|
||||
json
|
||||
recursive-open-struct (1.0.0)
|
||||
|
@ -764,16 +760,16 @@ GEM
|
|||
sawyer (0.8.1)
|
||||
addressable (>= 2.3.5, < 2.6)
|
||||
faraday (~> 0.8, < 1.0)
|
||||
scss_lint (0.47.1)
|
||||
rake (>= 0.9, < 11)
|
||||
sass (~> 3.4.15)
|
||||
scss_lint (0.54.0)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.4.20)
|
||||
securecompare (1.0.0)
|
||||
seed-fu (2.3.6)
|
||||
activerecord (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
select2-rails (3.5.9.3)
|
||||
thor (~> 0.14)
|
||||
sentry-raven (2.4.0)
|
||||
sentry-raven (2.5.3)
|
||||
faraday (>= 0.7.6, < 1.0)
|
||||
settingslogic (2.0.9)
|
||||
sexp_processor (4.9.0)
|
||||
|
@ -781,7 +777,7 @@ GEM
|
|||
rack
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (5.0.0)
|
||||
sidekiq (5.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
|
@ -889,7 +885,7 @@ GEM
|
|||
vmstat (2.3.0)
|
||||
warden (1.2.6)
|
||||
rack (>= 1.0)
|
||||
webmock (1.24.6)
|
||||
webmock (2.3.2)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
|
@ -928,8 +924,8 @@ DEPENDENCIES
|
|||
benchmark-ips (~> 2.3.0)
|
||||
better_errors (~> 2.1.0)
|
||||
binding_of_caller (~> 0.7.2)
|
||||
bootsnap (~> 1.0.0)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
bootstrap_form (~> 2.7.0)
|
||||
brakeman (~> 3.6.0)
|
||||
browser (~> 2.2)
|
||||
bullet (~> 5.5.0)
|
||||
|
@ -940,7 +936,6 @@ DEPENDENCIES
|
|||
charlock_holmes (~> 0.7.3)
|
||||
chronic (~> 0.10.2)
|
||||
chronic_duration (~> 0.10.6)
|
||||
coffee-rails (~> 4.1.0)
|
||||
concurrent-ruby (~> 1.0.5)
|
||||
connection_pool (~> 2.0)
|
||||
creole (~> 0.5.0)
|
||||
|
@ -963,7 +958,7 @@ DEPENDENCIES
|
|||
flipper (~> 0.10.2)
|
||||
flipper-active_record (~> 0.10.2)
|
||||
fog-aliyun (~> 0.1.0)
|
||||
fog-aws (~> 0.9)
|
||||
fog-aws (~> 1.4)
|
||||
fog-core (~> 1.44)
|
||||
fog-google (~> 0.5)
|
||||
fog-local (~> 0.3)
|
||||
|
@ -977,7 +972,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.2.0)
|
||||
gitaly (~> 0.9.0)
|
||||
gitaly (~> 0.17.0)
|
||||
github-linguist (~> 4.7.0)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.5.1)
|
||||
|
@ -1008,20 +1003,20 @@ DEPENDENCIES
|
|||
letter_opener_web (~> 1.3.0)
|
||||
license_finder (~> 2.1.0)
|
||||
licensee (~> 8.7.0)
|
||||
lograge (~> 0.5)
|
||||
loofah (~> 2.0.3)
|
||||
mail_room (~> 0.9.1)
|
||||
method_source (~> 0.8)
|
||||
minitest (~> 5.7.0)
|
||||
mousetrap-rails (~> 1.4.6)
|
||||
mysql2 (~> 0.3.16)
|
||||
net-ssh (~> 3.0.1)
|
||||
mysql2 (~> 0.4.5)
|
||||
nokogiri (~> 1.6.7, >= 1.6.7.2)
|
||||
oauth2 (~> 1.4)
|
||||
octokit (~> 4.6.2)
|
||||
oj (~> 2.17.4)
|
||||
omniauth (~> 1.4.2)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth-authentiq (~> 0.3.0)
|
||||
omniauth-authentiq (~> 0.3.1)
|
||||
omniauth-azure-oauth2 (~> 0.0.6)
|
||||
omniauth-cas3 (~> 1.1.2)
|
||||
omniauth-facebook (~> 4.0.0)
|
||||
|
@ -1040,15 +1035,15 @@ DEPENDENCIES
|
|||
peek-gc (~> 0.0.2)
|
||||
peek-host (~> 1.0.0)
|
||||
peek-mysql2 (~> 1.1.0)
|
||||
peek-performance_bar (~> 1.2.1)
|
||||
peek-performance_bar (~> 1.3.0)
|
||||
peek-pg (~> 1.3.0)
|
||||
peek-rblineprof (~> 0.2.0)
|
||||
peek-redis (~> 1.2.0)
|
||||
peek-sidekiq (~> 1.0.3)
|
||||
pg (~> 0.18.2)
|
||||
poltergeist (~> 1.9.0)
|
||||
premailer-rails (~> 1.9.0)
|
||||
prometheus-client-mmap (~> 0.7.0.beta5)
|
||||
premailer-rails (~> 1.9.7)
|
||||
prometheus-client-mmap (~> 0.7.0.beta9)
|
||||
pry-byebug (~> 3.4.1)
|
||||
pry-rails (~> 0.3.4)
|
||||
rack-attack (~> 4.4.1)
|
||||
|
@ -1059,8 +1054,10 @@ DEPENDENCIES
|
|||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rails-i18n (~> 4.0.9)
|
||||
rainbow (~> 2.2)
|
||||
raindrops (~> 0.18)
|
||||
rblineprof (~> 0.3.6)
|
||||
rdoc (~> 4.2)
|
||||
re2 (~> 1.0.0)
|
||||
recaptcha (~> 3.0)
|
||||
redcarpet (~> 3.4)
|
||||
redis (~> 3.2)
|
||||
|
@ -1083,10 +1080,10 @@ DEPENDENCIES
|
|||
rugged (~> 0.25.1.1)
|
||||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.6)
|
||||
scss_lint (~> 0.47.0)
|
||||
scss_lint (~> 0.54.0)
|
||||
seed-fu (~> 2.3.5)
|
||||
select2-rails (~> 3.5.9)
|
||||
sentry-raven (~> 2.4.0)
|
||||
sentry-raven (~> 2.5.3)
|
||||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 2.8.0)
|
||||
|
@ -1119,7 +1116,7 @@ DEPENDENCIES
|
|||
version_sorter (~> 2.1.0)
|
||||
virtus (~> 1.0.1)
|
||||
vmstat (~> 2.3.0)
|
||||
webmock (~> 1.24.0)
|
||||
webmock (~> 2.3.2)
|
||||
webpack-rails (~> 0.9.10)
|
||||
wikicloth (= 0.8.1)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
## 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
|
||||
- [![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
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -1,12 +1,8 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
/* global Flash */
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import emojiMap from 'emojis/digests.json';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import { glEmojiTag } from './behaviors/gl_emoji';
|
||||
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
|
||||
|
||||
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
|
||||
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
|
||||
const requestAnimationFrame = window.requestAnimationFrame ||
|
||||
|
@ -16,8 +12,6 @@ const requestAnimationFrame = window.requestAnimationFrame ||
|
|||
|
||||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
|
||||
|
||||
let categoryMap = null;
|
||||
|
||||
const categoryLabelMap = {
|
||||
activity: 'Activity',
|
||||
people: 'People',
|
||||
|
@ -29,186 +23,144 @@ const categoryLabelMap = {
|
|||
flags: 'Flags',
|
||||
};
|
||||
|
||||
function buildCategoryMap() {
|
||||
return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => {
|
||||
const emojiInfo = emojiMap[emojiNameKey];
|
||||
if (currentCategoryMap[emojiInfo.category]) {
|
||||
currentCategoryMap[emojiInfo.category].push(emojiNameKey);
|
||||
class AwardsHandler {
|
||||
constructor(emoji) {
|
||||
this.emoji = emoji;
|
||||
this.eventListeners = [];
|
||||
// If the user shows intent let's pre-build the menu
|
||||
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
|
||||
const $menu = $('.emoji-menu');
|
||||
if ($menu.length === 0) {
|
||||
requestAnimationFrame(() => {
|
||||
this.createEmojiMenu();
|
||||
});
|
||||
}
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.showEmojiMenu($(e.currentTarget));
|
||||
});
|
||||
|
||||
this.registerEventListener('on', $('html'), 'click', (e) => {
|
||||
const $target = $(e.target);
|
||||
if (!$target.closest('.emoji-menu-content').length) {
|
||||
$('.js-awards-block.current').removeClass('current');
|
||||
}
|
||||
if (!$target.closest('.emoji-menu').length) {
|
||||
if ($('.emoji-menu').is(':visible')) {
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
}
|
||||
}
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
const $glEmojiElement = $target.find('gl-emoji');
|
||||
const $spriteIconElement = $target.find('.icon');
|
||||
const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
|
||||
|
||||
$target.closest('.js-awards-block').addClass('current');
|
||||
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
|
||||
});
|
||||
}
|
||||
|
||||
registerEventListener(method = 'on', element, ...args) {
|
||||
element[method].call(element, ...args);
|
||||
this.eventListeners.push({
|
||||
element,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
showEmojiMenu($addBtn) {
|
||||
if ($addBtn.hasClass('js-note-emoji')) {
|
||||
$addBtn.closest('.note').find('.js-awards-block').addClass('current');
|
||||
} else {
|
||||
$addBtn.closest('.js-awards-block').addClass('current');
|
||||
}
|
||||
|
||||
return currentCategoryMap;
|
||||
}, {
|
||||
activity: [],
|
||||
people: [],
|
||||
nature: [],
|
||||
food: [],
|
||||
travel: [],
|
||||
objects: [],
|
||||
symbols: [],
|
||||
flags: [],
|
||||
});
|
||||
}
|
||||
|
||||
function renderCategory(name, emojiList, opts = {}) {
|
||||
return `
|
||||
<h5 class="emoji-menu-title">
|
||||
${name}
|
||||
</h5>
|
||||
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
|
||||
${emojiList.map(emojiName => `
|
||||
<li class="emoji-menu-list-item">
|
||||
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
|
||||
${glEmojiTag(emojiName, {
|
||||
sprite: true,
|
||||
})}
|
||||
</button>
|
||||
</li>
|
||||
`).join('\n')}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
function AwardsHandler() {
|
||||
this.eventListeners = [];
|
||||
this.aliases = emojiAliases;
|
||||
// If the user shows intent let's pre-build the menu
|
||||
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
|
||||
const $menu = $('.emoji-menu');
|
||||
if ($menu.length === 0) {
|
||||
requestAnimationFrame(() => {
|
||||
this.createEmojiMenu();
|
||||
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
|
||||
const $userAuthored = this.isUserAuthored($addBtn);
|
||||
if ($menu.length) {
|
||||
if ($menu.is('.is-visible')) {
|
||||
$addBtn.removeClass('is-active');
|
||||
$menu.removeClass('is-visible');
|
||||
$('.js-emoji-menu-search').blur();
|
||||
} else {
|
||||
$addBtn.addClass('is-active');
|
||||
this.positionMenu($menu, $addBtn);
|
||||
$menu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}
|
||||
} else {
|
||||
$addBtn.addClass('is-loading is-active');
|
||||
this.createEmojiMenu(() => {
|
||||
const $createdMenu = $('.emoji-menu');
|
||||
$addBtn.removeClass('is-loading');
|
||||
this.positionMenu($createdMenu, $addBtn);
|
||||
return setTimeout(() => {
|
||||
$createdMenu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
// Prebuild the categoryMap
|
||||
categoryMap = categoryMap || buildCategoryMap();
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.showEmojiMenu($(e.currentTarget));
|
||||
});
|
||||
|
||||
this.registerEventListener('on', $('html'), 'click', (e) => {
|
||||
const $target = $(e.target);
|
||||
if (!$target.closest('.emoji-menu-content').length) {
|
||||
$('.js-awards-block.current').removeClass('current');
|
||||
$thumbsBtn.toggleClass('disabled', $userAuthored);
|
||||
}
|
||||
|
||||
// Create the emoji menu with the first category of emojis.
|
||||
// Then render the remaining categories of emojis one by one to avoid jank.
|
||||
createEmojiMenu(callback) {
|
||||
if (this.isCreatingEmojiMenu) {
|
||||
return;
|
||||
}
|
||||
if (!$target.closest('.emoji-menu').length) {
|
||||
if ($('.emoji-menu').is(':visible')) {
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
}
|
||||
this.isCreatingEmojiMenu = true;
|
||||
|
||||
// Render the first category
|
||||
const categoryMap = this.emoji.getEmojiCategoryMap();
|
||||
const categoryNameKey = Object.keys(categoryMap)[0];
|
||||
const emojisInCategory = categoryMap[categoryNameKey];
|
||||
const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
|
||||
|
||||
// Render the frequently used
|
||||
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
|
||||
let frequentlyUsedCatgegory = '';
|
||||
if (frequentlyUsedEmojis.length > 0) {
|
||||
frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
|
||||
menuListClass: 'frequent-emojis',
|
||||
});
|
||||
}
|
||||
});
|
||||
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
const $glEmojiElement = $target.find('gl-emoji');
|
||||
const $spriteIconElement = $target.find('.icon');
|
||||
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
|
||||
|
||||
$target.closest('.js-awards-block').addClass('current');
|
||||
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
|
||||
});
|
||||
}
|
||||
const emojiMenuMarkup = `
|
||||
<div class="emoji-menu">
|
||||
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
|
||||
|
||||
AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) {
|
||||
element[method].call(element, ...args);
|
||||
this.eventListeners.push({
|
||||
element,
|
||||
args,
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
|
||||
if ($addBtn.hasClass('js-note-emoji')) {
|
||||
$addBtn.closest('.note').find('.js-awards-block').addClass('current');
|
||||
} else {
|
||||
$addBtn.closest('.js-awards-block').addClass('current');
|
||||
}
|
||||
|
||||
const $menu = $('.emoji-menu');
|
||||
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
|
||||
const $userAuthored = this.isUserAuthored($addBtn);
|
||||
if ($menu.length) {
|
||||
if ($menu.is('.is-visible')) {
|
||||
$addBtn.removeClass('is-active');
|
||||
$menu.removeClass('is-visible');
|
||||
$('.js-emoji-menu-search').blur();
|
||||
} else {
|
||||
$addBtn.addClass('is-active');
|
||||
this.positionMenu($menu, $addBtn);
|
||||
$menu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}
|
||||
} else {
|
||||
$addBtn.addClass('is-loading is-active');
|
||||
this.createEmojiMenu(() => {
|
||||
const $createdMenu = $('.emoji-menu');
|
||||
$addBtn.removeClass('is-loading');
|
||||
this.positionMenu($createdMenu, $addBtn);
|
||||
return setTimeout(() => {
|
||||
$createdMenu.addClass('is-visible');
|
||||
$('.js-emoji-menu-search').focus();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
$thumbsBtn.toggleClass('disabled', $userAuthored);
|
||||
};
|
||||
|
||||
// Create the emoji menu with the first category of emojis.
|
||||
// Then render the remaining categories of emojis one by one to avoid jank.
|
||||
AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
|
||||
if (this.isCreatingEmojiMenu) {
|
||||
return;
|
||||
}
|
||||
this.isCreatingEmojiMenu = true;
|
||||
|
||||
// Render the first category
|
||||
categoryMap = categoryMap || buildCategoryMap();
|
||||
const categoryNameKey = Object.keys(categoryMap)[0];
|
||||
const emojisInCategory = categoryMap[categoryNameKey];
|
||||
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
|
||||
|
||||
// Render the frequently used
|
||||
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
|
||||
let frequentlyUsedCatgegory = '';
|
||||
if (frequentlyUsedEmojis.length > 0) {
|
||||
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, {
|
||||
menuListClass: 'frequent-emojis',
|
||||
});
|
||||
}
|
||||
|
||||
const emojiMenuMarkup = `
|
||||
<div class="emoji-menu">
|
||||
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
|
||||
|
||||
<div class="emoji-menu-content">
|
||||
${frequentlyUsedCatgegory}
|
||||
${firstCategory}
|
||||
<div class="emoji-menu-content">
|
||||
${frequentlyUsedCatgegory}
|
||||
${firstCategory}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
|
||||
this.addRemainingEmojiMenuCategories();
|
||||
this.setupSearch();
|
||||
if (callback) {
|
||||
callback();
|
||||
this.addRemainingEmojiMenuCategories();
|
||||
this.setupSearch();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler
|
||||
.prototype
|
||||
.addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() {
|
||||
addRemainingEmojiMenuCategories() {
|
||||
if (this.isAddingRemainingEmojiMenuCategories) {
|
||||
return;
|
||||
}
|
||||
this.isAddingRemainingEmojiMenuCategories = true;
|
||||
|
||||
categoryMap = categoryMap || buildCategoryMap();
|
||||
const categoryMap = this.emoji.getEmojiCategoryMap();
|
||||
|
||||
// Avoid the jank and render the remaining categories separately
|
||||
// This will take more time, but makes UI more responsive
|
||||
|
@ -220,7 +172,7 @@ AwardsHandler
|
|||
promiseChain.then(() =>
|
||||
new Promise((resolve) => {
|
||||
const emojisInCategory = categoryMap[categoryNameKey];
|
||||
const categoryMarkup = renderCategory(
|
||||
const categoryMarkup = this.renderCategory(
|
||||
categoryLabelMap[categoryNameKey],
|
||||
emojisInCategory,
|
||||
);
|
||||
|
@ -243,179 +195,186 @@ AwardsHandler
|
|||
emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
|
||||
throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) {
|
||||
const position = $addBtn.data('position');
|
||||
// The menu could potentially be off-screen or in a hidden overflow element
|
||||
// So we position the element absolute in the body
|
||||
const css = {
|
||||
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
|
||||
};
|
||||
if (position === 'right') {
|
||||
css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
|
||||
$menu.addClass('is-aligned-right');
|
||||
} else {
|
||||
css.left = `${$addBtn.offset().left}px`;
|
||||
$menu.removeClass('is-aligned-right');
|
||||
}
|
||||
return $menu.css(css);
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addAward = function addAward(
|
||||
votesBlock,
|
||||
awardUrl,
|
||||
emoji,
|
||||
checkMutuality,
|
||||
callback,
|
||||
) {
|
||||
const normalizedEmoji = this.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
|
||||
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
|
||||
return typeof callback === 'function' ? callback() : undefined;
|
||||
});
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
|
||||
votesBlock,
|
||||
emoji,
|
||||
checkForMutuality,
|
||||
) {
|
||||
if (checkForMutuality || checkForMutuality === null) {
|
||||
this.checkMutuality(votesBlock, emoji);
|
||||
renderCategory(name, emojiList, opts = {}) {
|
||||
return `
|
||||
<h5 class="emoji-menu-title">
|
||||
${name}
|
||||
</h5>
|
||||
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
|
||||
${emojiList.map(emojiName => `
|
||||
<li class="emoji-menu-list-item">
|
||||
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
|
||||
${this.emoji.glEmojiTag(emojiName, {
|
||||
sprite: true,
|
||||
})}
|
||||
</button>
|
||||
</li>
|
||||
`).join('\n')}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
this.addEmojiToFrequentlyUsedList(emoji);
|
||||
const normalizedEmoji = this.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
if ($emojiButton.length > 0) {
|
||||
if (this.isActive($emojiButton)) {
|
||||
this.decrementCounter($emojiButton, normalizedEmoji);
|
||||
|
||||
positionMenu($menu, $addBtn) {
|
||||
const position = $addBtn.data('position');
|
||||
// The menu could potentially be off-screen or in a hidden overflow element
|
||||
// So we position the element absolute in the body
|
||||
const css = {
|
||||
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
|
||||
};
|
||||
if (position === 'right') {
|
||||
css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
|
||||
$menu.addClass('is-aligned-right');
|
||||
} else {
|
||||
const counter = $emojiButton.find('.js-counter');
|
||||
counter.text(parseInt(counter.text(), 10) + 1);
|
||||
$emojiButton.addClass('active');
|
||||
this.addYouToUserList(votesBlock, normalizedEmoji);
|
||||
this.animateEmoji($emojiButton);
|
||||
css.left = `${$addBtn.offset().left}px`;
|
||||
$menu.removeClass('is-aligned-right');
|
||||
}
|
||||
} else {
|
||||
votesBlock.removeClass('hidden');
|
||||
this.createEmoji(votesBlock, normalizedEmoji);
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getVotesBlock = function getVotesBlock() {
|
||||
const currentBlock = $('.js-awards-block.current');
|
||||
let resultantVotesBlock = currentBlock;
|
||||
if (currentBlock.length === 0) {
|
||||
resultantVotesBlock = $('.js-awards-block').eq(0);
|
||||
return $menu.css(css);
|
||||
}
|
||||
|
||||
return resultantVotesBlock;
|
||||
};
|
||||
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
|
||||
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
|
||||
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
|
||||
return typeof callback === 'function' ? callback() : undefined;
|
||||
});
|
||||
$('.emoji-menu').removeClass('is-visible');
|
||||
$('.js-add-award.is-active').removeClass('is-active');
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.getAwardUrl = function getAwardUrl() {
|
||||
return this.getVotesBlock().data('award-url');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) {
|
||||
const awardUrl = this.getAwardUrl();
|
||||
if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
|
||||
const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent();
|
||||
const isAlreadyVoted = $emojiButton.hasClass('active');
|
||||
if (isAlreadyVoted) {
|
||||
this.addAward(votesBlock, awardUrl, mutualVote, false);
|
||||
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
|
||||
if (checkForMutuality || checkForMutuality === null) {
|
||||
this.checkMutuality(votesBlock, emoji);
|
||||
}
|
||||
this.addEmojiToFrequentlyUsedList(emoji);
|
||||
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
|
||||
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
|
||||
if ($emojiButton.length > 0) {
|
||||
if (this.isActive($emojiButton)) {
|
||||
this.decrementCounter($emojiButton, normalizedEmoji);
|
||||
} else {
|
||||
const counter = $emojiButton.find('.js-counter');
|
||||
counter.text(parseInt(counter.text(), 10) + 1);
|
||||
$emojiButton.addClass('active');
|
||||
this.addYouToUserList(votesBlock, normalizedEmoji);
|
||||
this.animateEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
votesBlock.removeClass('hidden');
|
||||
this.createEmoji(votesBlock, normalizedEmoji);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.isActive = function isActive($emojiButton) {
|
||||
return $emojiButton.hasClass('active');
|
||||
};
|
||||
getVotesBlock() {
|
||||
const currentBlock = $('.js-awards-block.current');
|
||||
let resultantVotesBlock = currentBlock;
|
||||
if (currentBlock.length === 0) {
|
||||
resultantVotesBlock = $('.js-awards-block').eq(0);
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) {
|
||||
return $button.hasClass('js-user-authored');
|
||||
};
|
||||
return resultantVotesBlock;
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
|
||||
const counter = $('.js-counter', $emojiButton);
|
||||
const counterNumber = parseInt(counter.text(), 10);
|
||||
if (counterNumber > 1) {
|
||||
counter.text(counterNumber - 1);
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
$emojiButton.tooltip('destroy');
|
||||
counter.text('0');
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
if ($emojiButton.parents('.note').length) {
|
||||
getAwardUrl() {
|
||||
return this.getVotesBlock().data('award-url');
|
||||
}
|
||||
|
||||
checkMutuality(votesBlock, emoji) {
|
||||
const awardUrl = this.getAwardUrl();
|
||||
if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
|
||||
const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent();
|
||||
const isAlreadyVoted = $emojiButton.hasClass('active');
|
||||
if (isAlreadyVoted) {
|
||||
this.addAward(votesBlock, awardUrl, mutualVote, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isActive($emojiButton) {
|
||||
return $emojiButton.hasClass('active');
|
||||
}
|
||||
|
||||
isUserAuthored($button) {
|
||||
return $button.hasClass('js-user-authored');
|
||||
}
|
||||
|
||||
decrementCounter($emojiButton, emoji) {
|
||||
const counter = $('.js-counter', $emojiButton);
|
||||
const counterNumber = parseInt(counter.text(), 10);
|
||||
if (counterNumber > 1) {
|
||||
counter.text(counterNumber - 1);
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
$emojiButton.tooltip('destroy');
|
||||
counter.text('0');
|
||||
this.removeYouFromUserList($emojiButton);
|
||||
if ($emojiButton.parents('.note').length) {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
return $emojiButton.removeClass('active');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) {
|
||||
$emojiButton.tooltip('destroy');
|
||||
$emojiButton.remove();
|
||||
const $votesBlock = this.getVotesBlock();
|
||||
if ($votesBlock.find('.js-emoji-btn').length === 0) {
|
||||
$votesBlock.addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) {
|
||||
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.toSentence = function toSentence(list) {
|
||||
let sentence;
|
||||
if (list.length <= 2) {
|
||||
sentence = list.join(' and ');
|
||||
} else {
|
||||
sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
|
||||
return $emojiButton.removeClass('active');
|
||||
}
|
||||
|
||||
return sentence;
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) {
|
||||
const awardBlock = $emojiButton;
|
||||
const originalTitle = this.getAwardTooltip(awardBlock);
|
||||
const authors = originalTitle.split(FROM_SENTENCE_REGEX);
|
||||
authors.splice(authors.indexOf('You'), 1);
|
||||
return awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.removeData('title')
|
||||
.removeAttr('data-title')
|
||||
.removeAttr('data-original-title')
|
||||
.attr('title', this.toSentence(authors))
|
||||
.tooltip('fixTitle');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) {
|
||||
const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
|
||||
const origTitle = this.getAwardTooltip(awardBlock);
|
||||
let users = [];
|
||||
if (origTitle) {
|
||||
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
|
||||
removeEmoji($emojiButton) {
|
||||
$emojiButton.tooltip('destroy');
|
||||
$emojiButton.remove();
|
||||
const $votesBlock = this.getVotesBlock();
|
||||
if ($votesBlock.find('.js-emoji-btn').length === 0) {
|
||||
$votesBlock.addClass('hidden');
|
||||
}
|
||||
}
|
||||
users.unshift('You');
|
||||
return awardBlock
|
||||
.attr('title', this.toSentence(users))
|
||||
.tooltip('fixTitle');
|
||||
};
|
||||
|
||||
AwardsHandler
|
||||
.prototype
|
||||
.createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) {
|
||||
getAwardTooltip($awardBlock) {
|
||||
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
|
||||
}
|
||||
|
||||
toSentence(list) {
|
||||
let sentence;
|
||||
if (list.length <= 2) {
|
||||
sentence = list.join(' and ');
|
||||
} else {
|
||||
sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`;
|
||||
}
|
||||
|
||||
return sentence;
|
||||
}
|
||||
|
||||
removeYouFromUserList($emojiButton) {
|
||||
const awardBlock = $emojiButton;
|
||||
const originalTitle = this.getAwardTooltip(awardBlock);
|
||||
const authors = originalTitle.split(FROM_SENTENCE_REGEX);
|
||||
authors.splice(authors.indexOf('You'), 1);
|
||||
return awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.removeData('title')
|
||||
.removeAttr('data-title')
|
||||
.removeAttr('data-original-title')
|
||||
.attr('title', this.toSentence(authors))
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
|
||||
addYouToUserList(votesBlock, emoji) {
|
||||
const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
|
||||
const origTitle = this.getAwardTooltip(awardBlock);
|
||||
let users = [];
|
||||
if (origTitle) {
|
||||
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
|
||||
}
|
||||
users.unshift('You');
|
||||
return awardBlock
|
||||
.attr('title', this.toSentence(users))
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
|
||||
createAwardButtonForVotesBlock(votesBlock, emojiName) {
|
||||
const buttonHtml = `
|
||||
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
|
||||
${glEmojiTag(emojiName)}
|
||||
${this.emoji.glEmojiTag(emojiName)}
|
||||
<span class="award-control-text js-counter">1</span>
|
||||
</button>
|
||||
`;
|
||||
|
@ -424,144 +383,136 @@ AwardsHandler
|
|||
this.animateEmoji($emojiButton);
|
||||
$('.award-control').tooltip();
|
||||
votesBlock.removeClass('current');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) {
|
||||
const className = 'pulse animated once short';
|
||||
$emoji.addClass(className);
|
||||
|
||||
this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
|
||||
$(e.currentTarget).removeClass(className);
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
|
||||
if ($('.emoji-menu').length) {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
}
|
||||
this.createEmojiMenu(() => {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) {
|
||||
if (this.isUserAuthored($emojiButton)) {
|
||||
this.userAuthored($emojiButton);
|
||||
} else {
|
||||
$.post(awardUrl, {
|
||||
name: emoji,
|
||||
}, (data) => {
|
||||
if (data.ok) {
|
||||
callback();
|
||||
}
|
||||
}).fail(() => new Flash('Something went wrong on our end.'));
|
||||
animateEmoji($emoji) {
|
||||
const className = 'pulse animated once short';
|
||||
$emoji.addClass(className);
|
||||
|
||||
this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
|
||||
$(e.currentTarget).removeClass(className);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
|
||||
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
|
||||
};
|
||||
createEmoji(votesBlock, emoji) {
|
||||
if ($('.emoji-menu').length) {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
}
|
||||
this.createEmojiMenu(() => {
|
||||
this.createAwardButtonForVotesBlock(votesBlock, emoji);
|
||||
});
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) {
|
||||
const oldTitle = this.getAwardTooltip($emojiButton);
|
||||
const newTitle = 'You cannot vote on your own issue, MR and note';
|
||||
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
|
||||
// Restore tooltip back to award list
|
||||
return setTimeout(() => {
|
||||
$emojiButton.tooltip('hide');
|
||||
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
|
||||
}, 2800);
|
||||
};
|
||||
postEmoji($emojiButton, awardUrl, emoji, callback) {
|
||||
if (this.isUserAuthored($emojiButton)) {
|
||||
this.userAuthored($emojiButton);
|
||||
} else {
|
||||
$.post(awardUrl, {
|
||||
name: emoji,
|
||||
}, (data) => {
|
||||
if (data.ok) {
|
||||
callback();
|
||||
}
|
||||
}).fail(() => new Flash('Something went wrong on our end.'));
|
||||
}
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
|
||||
const options = {
|
||||
scrollTop: $('.awards').offset().top - 110,
|
||||
};
|
||||
return $('body, html').animate(options, 200);
|
||||
};
|
||||
findEmojiIcon(votesBlock, emoji) {
|
||||
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) {
|
||||
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
|
||||
};
|
||||
userAuthored($emojiButton) {
|
||||
const oldTitle = this.getAwardTooltip($emojiButton);
|
||||
const newTitle = 'You cannot vote on your own issue, MR and note';
|
||||
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
|
||||
// Restore tooltip back to award list
|
||||
return setTimeout(() => {
|
||||
$emojiButton.tooltip('hide');
|
||||
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
|
||||
}, 2800);
|
||||
}
|
||||
|
||||
AwardsHandler
|
||||
.prototype
|
||||
.addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) {
|
||||
if (isEmojiNameValid(emoji)) {
|
||||
scrollToAwards() {
|
||||
const options = {
|
||||
scrollTop: $('.awards').offset().top - 110,
|
||||
};
|
||||
return $('body, html').animate(options, 200);
|
||||
}
|
||||
|
||||
addEmojiToFrequentlyUsedList(emoji) {
|
||||
if (this.emoji.isEmojiNameValid(emoji)) {
|
||||
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
|
||||
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
|
||||
return this.frequentlyUsedEmojis || (() => {
|
||||
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
|
||||
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
|
||||
inputName => isEmojiNameValid(inputName),
|
||||
);
|
||||
|
||||
return this.frequentlyUsedEmojis;
|
||||
})();
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.setupSearch = function setupSearch() {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
|
||||
this.registerEventListener('on', $search, 'input', (e) => {
|
||||
const term = $(e.target).val().trim();
|
||||
this.searchEmojis(term);
|
||||
});
|
||||
|
||||
const $menu = $('.emoji-menu');
|
||||
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
// Clear the search
|
||||
this.searchEmojis('');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
$search.val(term);
|
||||
|
||||
// Clean previous search results
|
||||
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
|
||||
if (term.length > 0) {
|
||||
// Generate a search result block
|
||||
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
|
||||
const foundEmojis = this.findMatchingEmojiElements(term).show();
|
||||
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||
$('.emoji-menu-content').append(h5).append(ul);
|
||||
} else {
|
||||
$('.emoji-menu-content').children().show();
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) {
|
||||
const safeTerm = term.toLowerCase();
|
||||
getFrequentlyUsedEmojis() {
|
||||
return this.frequentlyUsedEmojis || (() => {
|
||||
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
|
||||
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
|
||||
inputName => this.emoji.isEmojiNameValid(inputName),
|
||||
);
|
||||
|
||||
const namesMatchingAlias = [];
|
||||
Object.keys(emojiAliases).forEach((alias) => {
|
||||
if (alias.indexOf(safeTerm) >= 0) {
|
||||
namesMatchingAlias.push(emojiAliases[alias]);
|
||||
return this.frequentlyUsedEmojis;
|
||||
})();
|
||||
}
|
||||
|
||||
setupSearch() {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
|
||||
this.registerEventListener('on', $search, 'input', (e) => {
|
||||
const term = $(e.target).val().trim();
|
||||
this.searchEmojis(term);
|
||||
});
|
||||
|
||||
const $menu = $('.emoji-menu');
|
||||
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
// Clear the search
|
||||
this.searchEmojis('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchEmojis(term) {
|
||||
const $search = $('.js-emoji-menu-search');
|
||||
$search.val(term);
|
||||
|
||||
// Clean previous search results
|
||||
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
|
||||
if (term.length > 0) {
|
||||
// Generate a search result block
|
||||
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
|
||||
const foundEmojis = this.findMatchingEmojiElements(term).show();
|
||||
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||
$('.emoji-menu-content').append(h5).append(ul);
|
||||
} else {
|
||||
$('.emoji-menu-content').children().show();
|
||||
}
|
||||
});
|
||||
const $matchingElements = namesMatchingAlias.concat(safeTerm)
|
||||
.reduce(
|
||||
($result, searchTerm) =>
|
||||
$result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
|
||||
$([]),
|
||||
);
|
||||
return $matchingElements.closest('li').clone();
|
||||
};
|
||||
}
|
||||
|
||||
AwardsHandler.prototype.destroy = function destroy() {
|
||||
this.eventListeners.forEach((entry) => {
|
||||
entry.element.off.call(entry.element, ...entry.args);
|
||||
});
|
||||
$('.emoji-menu').remove();
|
||||
};
|
||||
findMatchingEmojiElements(query) {
|
||||
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
|
||||
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
|
||||
const $matchingElements = $emojiElements
|
||||
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
|
||||
return $matchingElements.closest('li').clone();
|
||||
}
|
||||
|
||||
export default AwardsHandler;
|
||||
destroy() {
|
||||
this.eventListeners.forEach((entry) => {
|
||||
entry.element.off.call(entry.element, ...entry.args);
|
||||
});
|
||||
$('.emoji-menu').remove();
|
||||
}
|
||||
}
|
||||
|
||||
let awardsHandlerPromise = null;
|
||||
export default function loadAwardsHandler(reload = false) {
|
||||
if (!awardsHandlerPromise || reload) {
|
||||
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
|
||||
.then(Emoji => new AwardsHandler(Emoji));
|
||||
}
|
||||
return awardsHandlerPromise;
|
||||
}
|
||||
|
|
|
@ -1,23 +1,8 @@
|
|||
import autosize from 'vendor/autosize';
|
||||
|
||||
$(() => {
|
||||
const $fields = $('.js-autosize');
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const autosizeEls = document.querySelectorAll('.js-autosize');
|
||||
|
||||
$fields.on('autosize:resized', function resized() {
|
||||
const $field = $(this);
|
||||
$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');
|
||||
autosize(autosizeEls);
|
||||
autosize.update(autosizeEls);
|
||||
});
|
||||
|
|
|
@ -1,75 +1,9 @@
|
|||
import installCustomElements from 'document-register-element';
|
||||
import emojiMap from 'emojis/digests.json';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
|
||||
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
|
||||
import isEmojiUnicodeSupported from '../emoji/support';
|
||||
|
||||
installCustomElements(window);
|
||||
|
||||
const generatedUnicodeSupportMap = getUnicodeSupportMap();
|
||||
|
||||
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() {
|
||||
export default function installGlEmojiElement() {
|
||||
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
|
||||
GlEmojiElementProto.createdCallback = function createdCallback() {
|
||||
const emojiUnicode = this.textContent.trim();
|
||||
|
@ -90,18 +24,26 @@ function installGlEmojiElement() {
|
|||
if (
|
||||
emojiUnicode &&
|
||||
isEmojiUnicode &&
|
||||
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
|
||||
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
|
||||
) {
|
||||
// CSS sprite fallback takes precedence over image fallback
|
||||
if (hasCssSpriteFalback) {
|
||||
// IE 11 doesn't like adding multiple at once :(
|
||||
this.classList.add('emoji-icon');
|
||||
this.classList.add(fallbackSpriteClass);
|
||||
} else if (hasImageFallback) {
|
||||
this.innerHTML = emojiImageTag(name, fallbackSrc);
|
||||
} else {
|
||||
const src = assembleFallbackImageSrc(name);
|
||||
this.innerHTML = emojiImageTag(name, src);
|
||||
import(/* webpackChunkName: 'emoji' */ '../emoji')
|
||||
.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() {
|
|||
prototype: GlEmojiElementProto,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
installGlEmojiElement,
|
||||
glEmojiTag,
|
||||
emojiImageTag,
|
||||
};
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
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;
|
|
@ -1,7 +1,7 @@
|
|||
import './autosize';
|
||||
import './bind_in_out';
|
||||
import './details_behavior';
|
||||
import { installGlEmojiElement } from './gl_emoji';
|
||||
import installGlEmojiElement from './gl_emoji';
|
||||
import './quick_submit';
|
||||
import './requires_input';
|
||||
import './toggler_behavior';
|
||||
|
|
|
@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
|
|||
|
||||
e.preventDefault();
|
||||
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')) {
|
||||
$submitButton.trigger('click', [e]);
|
||||
|
|
|
@ -51,8 +51,9 @@ export default () => {
|
|||
methods: {
|
||||
loadFile() {
|
||||
this.$http.get(el.dataset.endpoint)
|
||||
.then(response => response.json())
|
||||
.then((res) => {
|
||||
this.json = res.json();
|
||||
this.json = res;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
@ -81,8 +81,9 @@ $(() => {
|
|||
mounted () {
|
||||
Store.disabled = this.disabled;
|
||||
gl.boardService.all()
|
||||
.then(response => response.json())
|
||||
.then((resp) => {
|
||||
resp.json().forEach((board) => {
|
||||
resp.forEach((board) => {
|
||||
const list = Store.addList(board, this.defaultAvatar);
|
||||
|
||||
if (list.type === 'closed') {
|
||||
|
@ -97,7 +98,8 @@ $(() => {
|
|||
|
||||
Store.addBlankState();
|
||||
this.loading = false;
|
||||
}).catch(() => new Flash('An error occurred. Please try again.'));
|
||||
})
|
||||
.catch(() => new Flash('An error occurred. Please try again.'));
|
||||
},
|
||||
methods: {
|
||||
updateTokens() {
|
||||
|
|
|
@ -64,8 +64,9 @@ export default {
|
|||
|
||||
// Save the labels
|
||||
gl.boardService.generateDefaultLists()
|
||||
.then((resp) => {
|
||||
resp.json().forEach((listObj) => {
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
data.forEach((listObj) => {
|
||||
const list = Store.findList('title', listObj.title);
|
||||
|
||||
list.id = listObj.id;
|
||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
|||
methods: {
|
||||
submit(e) {
|
||||
e.preventDefault();
|
||||
if (this.title.trim() === '') return;
|
||||
if (this.title.trim() === '') return Promise.resolve();
|
||||
|
||||
this.error = false;
|
||||
|
||||
|
@ -29,7 +29,10 @@ export default {
|
|||
assignees: [],
|
||||
});
|
||||
|
||||
this.list.newIssue(issue)
|
||||
eventHub.$emit(`scroll-board-list-${this.list.id}`);
|
||||
this.cancel();
|
||||
|
||||
return this.list.newIssue(issue)
|
||||
.then(() => {
|
||||
// Need this because our jQuery very kindly disables buttons on ALL form submissions
|
||||
$(this.$refs.submitButton).enable();
|
||||
|
@ -47,9 +50,6 @@ export default {
|
|||
// Show error message
|
||||
this.error = true;
|
||||
});
|
||||
|
||||
eventHub.$emit(`scroll-board-list-${this.list.id}`);
|
||||
this.cancel();
|
||||
},
|
||||
cancel() {
|
||||
this.title = '';
|
||||
|
|
|
@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
|
|||
},
|
||||
milestoneTitle() {
|
||||
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
|
||||
}
|
||||
},
|
||||
canRemove() {
|
||||
return !this.list.preset;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
detail: {
|
||||
|
|
|
@ -88,9 +88,9 @@ gl.issueBoards.IssuesModal = Vue.extend({
|
|||
return gl.boardService.getBacklog(queryData(this.filter.path, {
|
||||
page: this.page,
|
||||
per: this.perPage,
|
||||
})).then((res) => {
|
||||
const data = res.json();
|
||||
|
||||
}))
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
if (clearIssues) {
|
||||
this.issues = [];
|
||||
}
|
||||
|
|
|
@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
|||
},
|
||||
template: `
|
||||
<div
|
||||
class="block list"
|
||||
v-if="list.type !== 'closed'">
|
||||
class="block list">
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
|
|
|
@ -40,9 +40,8 @@ class List {
|
|||
|
||||
save () {
|
||||
return gl.boardService.createList(this.label.id)
|
||||
.then((resp) => {
|
||||
const data = resp.json();
|
||||
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
this.id = data.id;
|
||||
this.type = data.list_type;
|
||||
this.position = data.position;
|
||||
|
@ -91,8 +90,8 @@ class List {
|
|||
}
|
||||
|
||||
return gl.boardService.getIssuesForList(this.id, data)
|
||||
.then((resp) => {
|
||||
const data = resp.json();
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
this.loading = false;
|
||||
this.issuesSize = data.size;
|
||||
|
||||
|
@ -109,11 +108,10 @@ class List {
|
|||
this.issuesSize += 1;
|
||||
|
||||
return gl.boardService.newIssue(this.id, issue)
|
||||
.then((resp) => {
|
||||
const data = resp.json();
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
issue.id = data.iid;
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
if (this.issuesSize > 1) {
|
||||
const moveBeforeIid = this.issues[1].id;
|
||||
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
|
||||
|
|
|
@ -23,11 +23,6 @@ class BoardService {
|
|||
url: bulkUpdatePath,
|
||||
},
|
||||
});
|
||||
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
all () {
|
||||
|
|
|
@ -13,25 +13,21 @@ window.Build = (function () {
|
|||
this.options = options || $('.js-build-options').data();
|
||||
|
||||
this.pageUrl = this.options.pageUrl;
|
||||
this.buildUrl = this.options.buildUrl;
|
||||
this.buildStatus = this.options.buildStatus;
|
||||
this.state = this.options.logState;
|
||||
this.buildStage = this.options.buildStage;
|
||||
this.$document = $(document);
|
||||
this.logBytes = 0;
|
||||
this.scrollOffsetPadding = 30;
|
||||
this.hasBeenScrolled = false;
|
||||
|
||||
this.updateDropdown = this.updateDropdown.bind(this);
|
||||
this.getBuildTrace = this.getBuildTrace.bind(this);
|
||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
|
||||
this.$body = $('body');
|
||||
this.$buildTrace = $('#build-trace');
|
||||
this.$buildRefreshAnimation = $('.js-build-refresh');
|
||||
this.$truncatedInfo = $('.js-truncated-info');
|
||||
this.$buildTraceOutput = $('.js-build-output');
|
||||
this.$scrollContainer = $('.js-scroll-container');
|
||||
this.$topBar = $('.js-top-bar');
|
||||
|
||||
// Scroll controllers
|
||||
this.$scrollTopBtn = $('.js-scroll-up');
|
||||
|
@ -63,13 +59,22 @@ window.Build = (function () {
|
|||
.off('click')
|
||||
.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')
|
||||
.on('scroll', () => {
|
||||
this.hasBeenScrolled = true;
|
||||
scrollThrottled();
|
||||
const contentHeight = this.$buildTraceOutput.prop('scrollHeight');
|
||||
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)
|
||||
|
@ -77,60 +82,73 @@ window.Build = (function () {
|
|||
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
|
||||
|
||||
this.updateArtifactRemoveDate();
|
||||
this.initAffixTopArea();
|
||||
|
||||
// eslint-disable-next-line
|
||||
this.getBuildTrace()
|
||||
.then(() => this.toggleScroll())
|
||||
.then(() => {
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
this.verifyTopPosition();
|
||||
this.getBuildTrace();
|
||||
}
|
||||
|
||||
Build.prototype.canScroll = function () {
|
||||
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height();
|
||||
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 () {
|
||||
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 () {
|
||||
const currentPosition = this.$scrollContainer.scrollTop();
|
||||
const bottomScroll = currentPosition + this.$scrollContainer.innerHeight();
|
||||
const currentPosition = document.body.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
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.$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.$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 () {
|
||||
this.hasBeenScrolled = true;
|
||||
this.$scrollContainer.scrollTop(0);
|
||||
this.toggleScroll();
|
||||
Build.prototype.scrollDown = function () {
|
||||
document.body.scrollTop = document.body.scrollHeight;
|
||||
};
|
||||
|
||||
Build.prototype.scrollToBottom = function () {
|
||||
this.scrollDown();
|
||||
this.hasBeenScrolled = true;
|
||||
this.toggleScroll();
|
||||
};
|
||||
|
||||
Build.prototype.scrollToTop = function () {
|
||||
document.body.scrollTop = 0;
|
||||
this.hasBeenScrolled = true;
|
||||
this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
|
||||
this.toggleScroll();
|
||||
};
|
||||
|
||||
|
@ -143,47 +161,6 @@ window.Build = (function () {
|
|||
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 () {
|
||||
this.$sidebar = $('.js-build-sidebar');
|
||||
this.$sidebar.niceScroll();
|
||||
|
@ -196,10 +173,13 @@ window.Build = (function () {
|
|||
})
|
||||
.done((log) => {
|
||||
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
|
||||
|
||||
if (log.state) {
|
||||
this.state = log.state;
|
||||
}
|
||||
|
||||
this.windowSize = this.$buildTraceOutput.prop('scrollHeight');
|
||||
|
||||
if (log.append) {
|
||||
this.$buildTraceOutput.append(log.html);
|
||||
this.logBytes += log.size;
|
||||
|
@ -220,16 +200,14 @@ window.Build = (function () {
|
|||
}
|
||||
|
||||
if (!log.complete) {
|
||||
this.toggleScrollAnimation(true);
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.toggleScrollAnimation(true);
|
||||
} else {
|
||||
this.toggleScrollAnimation(false);
|
||||
}
|
||||
|
||||
Build.timeout = setTimeout(() => {
|
||||
//eslint-disable-next-line
|
||||
this.getBuildTrace()
|
||||
.then(() => {
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
this.getBuildTrace();
|
||||
}, 4000);
|
||||
} else {
|
||||
this.$buildRefreshAnimation.remove();
|
||||
|
@ -242,7 +220,13 @@ window.Build = (function () {
|
|||
})
|
||||
.fail(() => {
|
||||
this.$buildRefreshAnimation.remove();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (!this.hasBeenScrolled) {
|
||||
this.scrollDown();
|
||||
}
|
||||
})
|
||||
.then(() => this.toggleScroll());
|
||||
};
|
||||
|
||||
Build.prototype.shouldHideSidebarForViewport = function () {
|
||||
|
@ -254,14 +238,11 @@ window.Build = (function () {
|
|||
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
|
||||
const $toggleButton = $('.js-sidebar-build-toggle-header');
|
||||
|
||||
this.$buildTrace
|
||||
.toggleClass('sidebar-expanded', shouldShow)
|
||||
.toggleClass('sidebar-collapsed', shouldHide);
|
||||
this.$sidebar
|
||||
.toggleClass('right-sidebar-expanded', shouldShow)
|
||||
.toggleClass('right-sidebar-collapsed', shouldHide);
|
||||
|
||||
$('.js-build-page')
|
||||
this.$topBar
|
||||
.toggleClass('sidebar-expanded', shouldShow)
|
||||
.toggleClass('sidebar-collapsed', shouldHide);
|
||||
|
||||
|
@ -274,17 +255,10 @@ window.Build = (function () {
|
|||
|
||||
Build.prototype.sidebarOnResize = function () {
|
||||
this.toggleSidebar(this.shouldHideSidebarForViewport());
|
||||
|
||||
this.verifyTopPosition();
|
||||
|
||||
if (this.canScroll()) {
|
||||
this.toggleScroll();
|
||||
}
|
||||
};
|
||||
|
||||
Build.prototype.sidebarOnClick = function () {
|
||||
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
|
||||
this.verifyTopPosition();
|
||||
};
|
||||
|
||||
Build.prototype.updateArtifactRemoveDate = function () {
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
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;
|
|
@ -1,5 +1,8 @@
|
|||
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 {
|
||||
constructor(opts = {}) {
|
||||
|
|
|
@ -18,13 +18,26 @@ window.gl.CommitPipelinesTable = CommitPipelinesTable;
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
|
||||
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
|
||||
const table = new CommitPipelinesTable({
|
||||
propsData: {
|
||||
endpoint: pipelineTableViewEl.dataset.endpoint,
|
||||
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
|
||||
},
|
||||
}).$mount();
|
||||
pipelineTableViewEl.appendChild(table.$el);
|
||||
if (pipelineTableViewEl) {
|
||||
// Update MR and Commits tabs
|
||||
pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => {
|
||||
if (event.detail.pipelines &&
|
||||
event.detail.pipelines.count &&
|
||||
event.detail.pipelines.count.all) {
|
||||
const badge = document.querySelector('.js-pipelines-mr-count');
|
||||
|
||||
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 @@
|
|||
},
|
||||
methods: {
|
||||
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 pipelines = response.pipelines || response;
|
||||
this.setCommonData(pipelines);
|
||||
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
|
||||
detail: {
|
||||
pipelines: response,
|
||||
},
|
||||
});
|
||||
|
||||
// notifiy to update the count in tabs
|
||||
if (this.$el.parentElement) {
|
||||
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import './lib/utils/url_utility';
|
||||
import FilesCommentButton from './files_comment_button';
|
||||
import SingleFileDiff from './single_file_diff';
|
||||
|
||||
const UNFOLD_COUNT = 20;
|
||||
let isBound = false;
|
||||
|
@ -8,8 +10,14 @@ let isBound = false;
|
|||
class Diff {
|
||||
constructor() {
|
||||
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));
|
||||
|
||||
|
|
|
@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
|
|||
const notesCount = this.notesCount;
|
||||
|
||||
$(this.$el).closest('.js-avatar-container')
|
||||
.toggleClass('js-no-comment-btn', notesCount > 0)
|
||||
.toggleClass('no-comment-btn', notesCount > 0)
|
||||
.nextUntil('.js-avatar-container')
|
||||
.toggleClass('js-no-comment-btn', notesCount > 0);
|
||||
.toggleClass('no-comment-btn', notesCount > 0);
|
||||
},
|
||||
toggleDiscussionsToggleState() {
|
||||
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* 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 ResolveService */
|
||||
/* global Flash */
|
||||
|
@ -64,8 +64,6 @@ const ResolveBtn = Vue.extend({
|
|||
});
|
||||
},
|
||||
resolve: function () {
|
||||
const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.';
|
||||
|
||||
if (!this.canResolve) return;
|
||||
|
||||
let promise;
|
||||
|
@ -79,24 +77,20 @@ const ResolveBtn = Vue.extend({
|
|||
.resolve(this.noteId);
|
||||
}
|
||||
|
||||
promise.then((response) => {
|
||||
this.loading = false;
|
||||
promise
|
||||
.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;
|
||||
|
||||
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
|
||||
this.discussion.updateHeadline(data);
|
||||
gl.mrWidget.checkStatus();
|
||||
} else {
|
||||
new Flash(errorFlashMsg);
|
||||
}
|
||||
|
||||
this.updateTooltip();
|
||||
}).catch(() => {
|
||||
new Flash(errorFlashMsg);
|
||||
});
|
||||
this.updateTooltip();
|
||||
})
|
||||
.catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.'));
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
|
||||
/* global Flash */
|
||||
/* global CommentsStore */
|
||||
|
||||
|
@ -32,27 +31,22 @@ class ResolveServiceClass {
|
|||
promise = this.resolveAll(mergeRequestId, discussionId);
|
||||
}
|
||||
|
||||
promise.then((response) => {
|
||||
discussion.loading = false;
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.json();
|
||||
const resolved_by = data ? data.resolved_by : null;
|
||||
promise
|
||||
.then(resp => resp.json())
|
||||
.then((data) => {
|
||||
discussion.loading = false;
|
||||
const resolvedBy = data ? data.resolved_by : null;
|
||||
|
||||
if (isResolved) {
|
||||
discussion.unResolveAllNotes();
|
||||
} else {
|
||||
discussion.resolveAllNotes(resolved_by);
|
||||
discussion.resolveAllNotes(resolvedBy);
|
||||
}
|
||||
|
||||
gl.mrWidget.checkStatus();
|
||||
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) {
|
||||
|
@ -62,7 +56,7 @@ class ResolveServiceClass {
|
|||
|
||||
return this.discussionResource.save({
|
||||
mergeRequestId,
|
||||
discussionId
|
||||
discussionId,
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
@ -73,7 +67,7 @@ class ResolveServiceClass {
|
|||
|
||||
return this.discussionResource.delete({
|
||||
mergeRequestId,
|
||||
discussionId
|
||||
discussionId,
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
/* 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 ActiveTabMemoizer */
|
||||
/* global ProjectSelect */
|
||||
/* global ShortcutsNavigation */
|
||||
/* global IssuableIndex */
|
||||
/* global ShortcutsIssuable */
|
||||
/* global ZenMode */
|
||||
/* global Milestone */
|
||||
/* global IssuableForm */
|
||||
/* global LabelsSelect */
|
||||
|
@ -24,7 +22,6 @@
|
|||
/* global ProjectAvatar */
|
||||
/* global CompareAutocomplete */
|
||||
/* global ProjectNew */
|
||||
/* global Star */
|
||||
/* global ProjectShow */
|
||||
/* global Labels */
|
||||
/* global Shortcuts */
|
||||
|
@ -53,8 +50,20 @@ import UsersSelect from './users_select';
|
|||
import RefSelectDropdown from './ref_select_dropdown';
|
||||
import GfmAutoComplete from './gfm_auto_complete';
|
||||
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 ScrollHelper from './helpers/scroll_helper';
|
||||
import initExperimentalFlags from './experimental_flags';
|
||||
import OAuthRememberMe from './oauth_remember_me';
|
||||
import PerformanceBar from './performance_bar';
|
||||
|
||||
(function() {
|
||||
var Dispatcher;
|
||||
|
@ -123,9 +132,13 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
}
|
||||
|
||||
switch (page) {
|
||||
case 'profiles:preferences:show':
|
||||
initExperimentalFlags();
|
||||
break;
|
||||
case 'sessions:new':
|
||||
new UsernameValidator();
|
||||
new ActiveTabMemoizer();
|
||||
new SigninTabsMemoizer();
|
||||
new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents();
|
||||
break;
|
||||
case 'projects:boards:show':
|
||||
case 'projects:boards:index':
|
||||
|
@ -149,6 +162,9 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
shortcut_handler = new ShortcutsIssuable();
|
||||
new ZenMode();
|
||||
break;
|
||||
case 'dashboard:milestones:index':
|
||||
new ProjectSelect();
|
||||
break;
|
||||
case 'projects:milestones:show':
|
||||
case 'groups:milestones:show':
|
||||
case 'dashboard:milestones:show':
|
||||
|
@ -158,9 +174,10 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
case 'groups:issues':
|
||||
case 'groups:merge_requests':
|
||||
new UsersSelect();
|
||||
new ProjectSelect();
|
||||
break;
|
||||
case 'dashboard:todos:index':
|
||||
new gl.Todos();
|
||||
new Todos();
|
||||
break;
|
||||
case 'dashboard:projects:index':
|
||||
case 'dashboard:projects:starred':
|
||||
|
@ -208,8 +225,8 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
new MilestoneSelect();
|
||||
new gl.IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:merge_requests:new':
|
||||
case 'projects:merge_requests:new_diffs':
|
||||
case 'projects:merge_requests:creations:new':
|
||||
case 'projects:merge_requests:creations:diffs':
|
||||
case 'projects:merge_requests:edit':
|
||||
new gl.Diff();
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
|
@ -246,15 +263,12 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
shortcut_handler = new ShortcutsIssuable(true);
|
||||
new ZenMode();
|
||||
break;
|
||||
case "projects:merge_requests:diffs":
|
||||
new gl.Diff();
|
||||
new ZenMode();
|
||||
break;
|
||||
case 'dashboard:activity':
|
||||
new gl.Activities();
|
||||
break;
|
||||
case 'dashboard:issues':
|
||||
case 'dashboard:merge_requests':
|
||||
new ProjectSelect();
|
||||
new UsersSelect();
|
||||
break;
|
||||
case 'projects:commit:show':
|
||||
|
@ -312,7 +326,7 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
new gl.Members();
|
||||
new UsersSelect();
|
||||
break;
|
||||
case 'projects:members:show':
|
||||
case 'projects:project_members:index':
|
||||
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
|
||||
new GroupsSelect();
|
||||
new gl.MemberExpirationDate();
|
||||
|
@ -365,12 +379,12 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
new BlobViewer();
|
||||
break;
|
||||
case 'help:index':
|
||||
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
|
||||
VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
|
||||
break;
|
||||
case 'search:show':
|
||||
new Search();
|
||||
break;
|
||||
case 'projects:repository:show':
|
||||
case 'projects:settings:repository:show':
|
||||
// Initialize Protected Branch Settings
|
||||
new gl.ProtectedBranchCreate();
|
||||
new gl.ProtectedBranchEditList();
|
||||
|
@ -380,7 +394,8 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
// Initialize expandable settings panels
|
||||
initSettingsPanels();
|
||||
break;
|
||||
case 'projects:ci_cd:show':
|
||||
case 'projects:settings:ci_cd:show':
|
||||
case 'groups:settings:ci_cd:show':
|
||||
new gl.ProjectVariables();
|
||||
break;
|
||||
case 'ci:lints:create':
|
||||
|
@ -417,7 +432,7 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
new Admin();
|
||||
switch (path[1]) {
|
||||
case 'cohorts':
|
||||
new gl.UsagePing();
|
||||
new UsagePing();
|
||||
break;
|
||||
case 'groups':
|
||||
new UsersSelect();
|
||||
|
@ -469,7 +484,7 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
new NotificationsDropdown();
|
||||
break;
|
||||
case 'wikis':
|
||||
new gl.Wikis();
|
||||
new Wikis();
|
||||
shortcut_handler = new ShortcutsWiki();
|
||||
new ZenMode();
|
||||
new gl.GLForm($('.wiki-form'), true);
|
||||
|
@ -501,6 +516,10 @@ import ScrollHelper from './helpers/scroll_helper';
|
|||
if (!shortcut_handler) {
|
||||
new Shortcuts();
|
||||
}
|
||||
|
||||
if (document.querySelector('#peek')) {
|
||||
new PerformanceBar({ container: '#peek' });
|
||||
}
|
||||
};
|
||||
|
||||
Dispatcher.prototype.initSearch = function() {
|
||||
|
|
|
@ -5,21 +5,28 @@ import './preview_markdown';
|
|||
|
||||
window.DropzoneInput = (function() {
|
||||
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;
|
||||
divHover = '<div class="div-dropzone-hover"></div>';
|
||||
iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
||||
$attachButton = form.find('.button-attach-file');
|
||||
$attachingFileMessage = form.find('.attaching-file-message');
|
||||
$cancelButton = form.find('.button-cancel-uploading-files');
|
||||
$retryLink = form.find('.retry-uploading-link');
|
||||
$uploadProgress = form.find('.uploading-progress');
|
||||
$uploadingErrorContainer = form.find('.uploading-error-container');
|
||||
$uploadingErrorMessage = form.find('.uploading-error-message');
|
||||
$uploadingProgressContainer = form.find('.uploading-progress-container');
|
||||
uploadsPath = window.uploads_path || null;
|
||||
maxFileSize = gon.max_file_size || 10;
|
||||
formTextarea = form.find('.js-gfm-input');
|
||||
const divHover = '<div class="div-dropzone-hover"></div>';
|
||||
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
||||
const $attachButton = form.find('.button-attach-file');
|
||||
const $attachingFileMessage = form.find('.attaching-file-message');
|
||||
const $cancelButton = form.find('.button-cancel-uploading-files');
|
||||
const $retryLink = form.find('.retry-uploading-link');
|
||||
const $uploadProgress = form.find('.uploading-progress');
|
||||
const $uploadingErrorContainer = form.find('.uploading-error-container');
|
||||
const $uploadingErrorMessage = form.find('.uploading-error-message');
|
||||
const $uploadingProgressContainer = form.find('.uploading-progress-container');
|
||||
const uploadsPath = window.uploads_path || null;
|
||||
const maxFileSize = gon.max_file_size || 10;
|
||||
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.on('paste', (function(_this) {
|
||||
return function(event) {
|
||||
|
@ -28,16 +35,16 @@ window.DropzoneInput = (function() {
|
|||
})(this));
|
||||
|
||||
// Add dropzone area to the form.
|
||||
$mdArea = formTextarea.closest('.md-area');
|
||||
const $mdArea = formTextarea.closest('.md-area');
|
||||
form.setupMarkdownPreview();
|
||||
$formDropzone = form.find('.div-dropzone');
|
||||
const $formDropzone = form.find('.div-dropzone');
|
||||
$formDropzone.parent().addClass('div-dropzone-wrapper');
|
||||
$formDropzone.append(divHover);
|
||||
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
|
||||
|
||||
if (!uploadsPath) return;
|
||||
|
||||
dropzone = $formDropzone.dropzone({
|
||||
const dropzone = $formDropzone.dropzone({
|
||||
url: uploadsPath,
|
||||
dictDefaultMessage: '',
|
||||
clickable: true,
|
||||
|
@ -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)
|
||||
// and remove them from dropzone files queue.
|
||||
|
@ -214,6 +221,35 @@ window.DropzoneInput = (function() {
|
|||
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) {
|
||||
var formData;
|
||||
formData = new FormData();
|
||||
|
@ -262,35 +298,6 @@ window.DropzoneInput = (function() {
|
|||
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) {
|
||||
e.preventDefault();
|
||||
$(this).closest('.gfm-form').find('.div-dropzone').click();
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
/* global dateFormat */
|
||||
/* global Pikaday */
|
||||
|
||||
import DateFix from './lib/utils/datefix';
|
||||
|
||||
class DueDateSelect {
|
||||
constructor({ $dropdown, $loading } = {}) {
|
||||
const $dropdownParent = $dropdown.closest('.dropdown');
|
||||
|
@ -43,14 +45,13 @@ class DueDateSelect {
|
|||
|
||||
initDatePicker() {
|
||||
const $dueDateInput = $(`input[name='${this.fieldName}']`);
|
||||
|
||||
const dateFix = DateFix.dashedFix($dueDateInput.val());
|
||||
const calendar = new Pikaday({
|
||||
field: $dueDateInput.get(0),
|
||||
theme: 'gitlab-theme',
|
||||
format: 'yyyy-mm-dd',
|
||||
onSelect: (dateText) => {
|
||||
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
|
||||
|
||||
$dueDateInput.val(formattedDate);
|
||||
|
||||
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
|
||||
|
@ -62,7 +63,7 @@ class DueDateSelect {
|
|||
}
|
||||
});
|
||||
|
||||
calendar.setDate(new Date($dueDateInput.val()));
|
||||
calendar.setDate(dateFix);
|
||||
this.$datePicker.append(calendar.el);
|
||||
this.$datePicker.data('pikaday', calendar);
|
||||
}
|
||||
|
@ -168,6 +169,7 @@ class DueDateSelectors {
|
|||
initMilestoneDatePicker() {
|
||||
$('.datepicker').each(function() {
|
||||
const $datePicker = $(this);
|
||||
const dateFix = DateFix.dashedFix($datePicker.val());
|
||||
const calendar = new Pikaday({
|
||||
field: $datePicker.get(0),
|
||||
theme: 'gitlab-theme animate-picker',
|
||||
|
@ -177,7 +179,8 @@ class DueDateSelectors {
|
|||
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
|
||||
}
|
||||
});
|
||||
calendar.setDate(new Date($datePicker.val()));
|
||||
|
||||
calendar.setDate(dateFix);
|
||||
|
||||
$datePicker.data('pikaday', calendar);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
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>
|
||||
`;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
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);
|
||||
}
|
|
@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
|
|||
}
|
||||
|
||||
export {
|
||||
isEmojiUnicodeSupported,
|
||||
isEmojiUnicodeSupported as default,
|
||||
isFlagEmoji,
|
||||
isKeycapEmoji,
|
||||
isSkinToneComboEmoji,
|
|
@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) {
|
|||
return resultMap;
|
||||
}
|
||||
|
||||
function getUnicodeSupportMap() {
|
||||
export default function getUnicodeSupportMap() {
|
||||
let unicodeSupportMap;
|
||||
let userAgentFromCache;
|
||||
|
||||
|
@ -165,8 +165,3 @@ function getUnicodeSupportMap() {
|
|||
|
||||
return unicodeSupportMap;
|
||||
}
|
||||
|
||||
export {
|
||||
getUnicodeSupportMap,
|
||||
generateUnicodeSupportMap,
|
||||
};
|
|
@ -32,7 +32,6 @@ export default {
|
|||
state: store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
isLoadingFolderContent: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
|
@ -86,9 +85,6 @@ export default {
|
|||
errorCallback: this.errorCallback,
|
||||
notificationCallback: (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 {
|
|||
this.store.toggleFolder(folder);
|
||||
|
||||
if (!folder.isOpen) {
|
||||
this.fetchChildEnvironments(folder, folderUrl);
|
||||
this.fetchChildEnvironments(folder, folderUrl, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -147,19 +143,17 @@ export default {
|
|||
.catch(this.errorCallback);
|
||||
},
|
||||
|
||||
fetchChildEnvironments(folder, folderUrl) {
|
||||
this.isLoadingFolderContent = true;
|
||||
fetchChildEnvironments(folder, folderUrl, showLoader = false) {
|
||||
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
|
||||
|
||||
this.service.getFolderContent(folderUrl)
|
||||
.then(resp => resp.json())
|
||||
.then((response) => {
|
||||
this.store.setfolderContent(folder, response.environments);
|
||||
this.isLoadingFolderContent = false;
|
||||
})
|
||||
.then(response => this.store.setfolderContent(folder, response.environments))
|
||||
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
|
||||
.catch(() => {
|
||||
this.isLoadingFolderContent = false;
|
||||
// eslint-disable-next-line no-new
|
||||
new Flash('An error occurred while fetching the environments.');
|
||||
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -176,13 +170,13 @@ export default {
|
|||
successCallback(resp) {
|
||||
this.saveData(resp);
|
||||
|
||||
// If folders are open while polling we need to open them again
|
||||
if (this.openFolders.length) {
|
||||
this.openFolders.map((folder) => {
|
||||
// We need to verify if any folder is open to also update it
|
||||
const openFolders = this.store.getOpenFolders();
|
||||
if (openFolders.length) {
|
||||
openFolders.forEach((folder) => {
|
||||
// TODO - Move this to the backend
|
||||
const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
|
||||
|
||||
this.store.updateFolder(folder, 'isOpen', true);
|
||||
return this.fetchChildEnvironments(folder, folderUrl);
|
||||
});
|
||||
}
|
||||
|
@ -267,7 +261,7 @@ export default {
|
|||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:is-loading-folder-content="isLoadingFolderContent" />
|
||||
/>
|
||||
</div>
|
||||
|
||||
<table-pagination
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import playIconSvg from 'icons/_icon_play.svg';
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -12,6 +13,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
|
@ -33,8 +38,6 @@ export default {
|
|||
onClickAction(endpoint) {
|
||||
this.isLoading = true;
|
||||
|
||||
$(this.$refs.tooltip).tooltip('destroy');
|
||||
|
||||
eventHub.$emit('postAction', endpoint);
|
||||
},
|
||||
|
||||
|
@ -53,11 +56,11 @@ export default {
|
|||
class="btn-group"
|
||||
role="group">
|
||||
<button
|
||||
v-tooltip
|
||||
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-toggle="dropdown"
|
||||
ref="tooltip"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
:disabled="isLoading">
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<script>
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
/**
|
||||
* Renders the external url link in environments table.
|
||||
*/
|
||||
|
@ -10,6 +12,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return 'Open';
|
||||
|
@ -19,7 +25,8 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<a
|
||||
class="btn external-url has-tooltip"
|
||||
v-tooltip
|
||||
class="btn external-url"
|
||||
data-container="body"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
|
|
|
@ -498,9 +498,9 @@ export default {
|
|||
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
|
||||
<a
|
||||
v-if="shouldRenderBuildName"
|
||||
class="build-link"
|
||||
class="build-link flex-truncate-parent"
|
||||
:href="buildPath">
|
||||
{{buildName}}
|
||||
<span class="flex-truncate-child">{{buildName}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
/**
|
||||
* Renders the Monitoring (Metrics) link in environments table.
|
||||
*/
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
monitoringUrl: {
|
||||
|
@ -10,6 +12,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return 'Monitoring';
|
||||
|
@ -19,7 +25,8 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<a
|
||||
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
|
||||
v-tooltip
|
||||
class="btn monitoring-url hidden-xs hidden-sm"
|
||||
data-container="body"
|
||||
rel="noopener noreferrer nofollow"
|
||||
:href="monitoringUrl"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import eventHub from '../event_hub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -14,6 +15,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
|
@ -46,8 +51,9 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<button
|
||||
v-tooltip
|
||||
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"
|
||||
@click="onClick"
|
||||
:disabled="isLoading"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* Used in environments table.
|
||||
*/
|
||||
import terminalIconSvg from 'icons/_icon_terminal.svg';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -14,6 +15,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
terminalIconSvg,
|
||||
|
@ -29,7 +34,8 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<a
|
||||
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
|
||||
v-tooltip
|
||||
class="btn terminal-button hidden-xs hidden-sm"
|
||||
data-container="body"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
|
|
|
@ -29,12 +29,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
isLoadingFolderContent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -74,7 +68,7 @@ export default {
|
|||
/>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
export default {
|
||||
methods: {
|
||||
saveData(resp) {
|
||||
const response = {
|
||||
headers: resp.headers,
|
||||
body: resp.json(),
|
||||
};
|
||||
const headers = resp.headers;
|
||||
return resp.json().then((response) => {
|
||||
this.isLoading = false;
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
this.store.storeAvailableCount(response.body.available_count);
|
||||
this.store.storeStoppedCount(response.body.stopped_count);
|
||||
this.store.storeEnvironments(response.body.environments);
|
||||
this.store.setPagination(response.headers);
|
||||
this.store.storeAvailableCount(response.available_count);
|
||||
this.store.storeStoppedCount(response.stopped_count);
|
||||
this.store.storeEnvironments(response.environments);
|
||||
this.store.setPagination(headers);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -35,14 +35,18 @@ export default class EnvironmentsStore {
|
|||
*/
|
||||
storeEnvironments(environments = []) {
|
||||
const filteredEnvironments = environments.map((env) => {
|
||||
const oldEnvironmentState = this.state.environments
|
||||
.find(element => element.id === env.latest.id) || {};
|
||||
|
||||
let filtered = {};
|
||||
|
||||
if (env.size > 1) {
|
||||
filtered = Object.assign({}, env, {
|
||||
isFolder: true,
|
||||
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
|
||||
folderName: env.name,
|
||||
isOpen: false,
|
||||
children: [],
|
||||
isOpen: oldEnvironmentState.isOpen || false,
|
||||
children: oldEnvironmentState.children || [],
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -98,7 +102,7 @@ export default class EnvironmentsStore {
|
|||
* @return {Array}
|
||||
*/
|
||||
toggleFolder(folder) {
|
||||
return this.updateFolder(folder, 'isOpen', !folder.isOpen);
|
||||
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,23 +129,23 @@ export default class EnvironmentsStore {
|
|||
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|Boolean|Object|Array} newValue
|
||||
* @return {Array}
|
||||
*/
|
||||
updateFolder(folder, prop, newValue) {
|
||||
updateEnvironmentProp(environment, prop, newValue) {
|
||||
const environments = this.state.environments;
|
||||
|
||||
const updatedEnvironments = environments.map((env) => {
|
||||
const updateEnv = Object.assign({}, env);
|
||||
if (env.isFolder && env.id === folder.id) {
|
||||
if (env.id === environment.id) {
|
||||
updateEnv[prop] = newValue;
|
||||
}
|
||||
|
||||
|
@ -149,8 +153,6 @@ export default class EnvironmentsStore {
|
|||
});
|
||||
|
||||
this.state.environments = updatedEnvironments;
|
||||
|
||||
return updatedEnvironments;
|
||||
}
|
||||
|
||||
getOpenFolders() {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
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();
|
||||
});
|
||||
};
|
|
@ -1,150 +1,73 @@
|
|||
/* 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 */
|
||||
|
||||
let $commentButtonTemplate;
|
||||
/* Developer beware! Do not add logic to showButton or hideButton
|
||||
* that will force a reflow. Doing so will create a signficant performance
|
||||
* bottleneck for pages with large diffs. For a comprehensive list of what
|
||||
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
|
||||
*/
|
||||
|
||||
window.FilesCommentButton = (function() {
|
||||
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;
|
||||
const LINE_NUMBER_CLASS = 'diff-line-num';
|
||||
const UNFOLDABLE_LINE_CLASS = 'js-unfold';
|
||||
const NO_COMMENT_CLASS = 'no-comment-btn';
|
||||
const EMPTY_CELL_CLASS = 'empty-cell';
|
||||
const OLD_LINE_CLASS = 'old_line';
|
||||
const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
|
||||
const DIFF_CONTAINER_SELECTOR = '.files';
|
||||
const DIFF_EXPANDED_CLASS = 'diff-expanded';
|
||||
|
||||
COMMENT_BUTTON_CLASS = '.add-diff-note';
|
||||
export default {
|
||||
init($diffFile) {
|
||||
/* 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
|
||||
* will be true in all cases */
|
||||
|
||||
LINE_HOLDER_CLASS = '.line_holder';
|
||||
|
||||
LINE_NUMBER_CLASS = 'diff-line-num';
|
||||
|
||||
LINE_CONTENT_CLASS = 'line_content';
|
||||
|
||||
UNFOLDABLE_LINE_CLASS = 'js-unfold';
|
||||
|
||||
EMPTY_CELL_CLASS = 'empty-cell';
|
||||
|
||||
OLD_LINE_CLASS = 'old_line';
|
||||
|
||||
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
|
||||
|
||||
TEXT_FILE_SELECTOR = '.text-file';
|
||||
|
||||
function FilesCommentButton(filesContainerElement) {
|
||||
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;
|
||||
if (!this.userCanCreateNote) {
|
||||
// data-can-create-note is an empty string when true, otherwise undefined
|
||||
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
|
||||
}
|
||||
|
||||
textFileElement = this.getTextFileElement($currentTarget);
|
||||
buttonParentElement.append(this.buildButton({
|
||||
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 (typeof notes !== 'undefined' && !this.isParallelView) {
|
||||
this.isParallelView = notes.isParallelView && notes.isParallelView();
|
||||
}
|
||||
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.isParallelView) {
|
||||
if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
|
||||
return hoveredElement;
|
||||
if (this.userCanCreateNote) {
|
||||
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
|
||||
.on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
|
||||
}
|
||||
},
|
||||
|
||||
showButton(isParallelView, e) {
|
||||
const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
|
||||
|
||||
if (!this.validateButtonParent(buttonParentElement)) return;
|
||||
|
||||
buttonParentElement.classList.add('is-over');
|
||||
buttonParentElement.nextElementSibling.classList.add('is-over');
|
||||
},
|
||||
|
||||
hideButton(isParallelView, e) {
|
||||
const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
|
||||
|
||||
buttonParentElement.classList.remove('is-over');
|
||||
buttonParentElement.nextElementSibling.classList.remove('is-over');
|
||||
},
|
||||
|
||||
getButtonParent(hoveredElement, isParallelView) {
|
||||
if (isParallelView) {
|
||||
if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
|
||||
return hoveredElement.previousElementSibling;
|
||||
}
|
||||
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
|
||||
} else {
|
||||
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
|
||||
return hoveredElement;
|
||||
}
|
||||
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
|
||||
} else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
|
||||
return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
|
||||
}
|
||||
};
|
||||
return hoveredElement;
|
||||
},
|
||||
|
||||
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
|
||||
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
|
||||
};
|
||||
|
||||
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
|
||||
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== '';
|
||||
};
|
||||
|
||||
return FilesCommentButton;
|
||||
})();
|
||||
|
||||
$.fn.filesCommentButton = function() {
|
||||
$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>');
|
||||
|
||||
if (!(this && (this.parent().data('can-create-note') != null))) {
|
||||
return;
|
||||
}
|
||||
return this.each(function() {
|
||||
if (!$.data(this, 'filesCommentButton')) {
|
||||
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
|
||||
}
|
||||
});
|
||||
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 @@
|
|||
|
||||
import AjaxFilter from '~/droplab/plugins/ajax_filter';
|
||||
import './filtered_search_dropdown';
|
||||
import { addClassIfElementExists } from '../lib/utils/dom_utils';
|
||||
|
||||
class DropdownUser extends gl.FilteredSearchDropdown {
|
||||
constructor(droplab, dropdown, input, tokenKeys, filter) {
|
||||
|
@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
|
|||
}
|
||||
|
||||
hideCurrentUser() {
|
||||
const currenUserItem = this.dropdown.querySelector('.js-current-user');
|
||||
currenUserItem.classList.add('hidden');
|
||||
addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden');
|
||||
}
|
||||
|
||||
itemClicked(e) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root';
|
|||
import RecentSearchesStore from './stores/recent_searches_store';
|
||||
import RecentSearchesService from './services/recent_searches_service';
|
||||
import eventHub from './event_hub';
|
||||
import { addClassIfElementExists } from '../lib/utils/dom_utils';
|
||||
|
||||
class FilteredSearchManager {
|
||||
constructor(page) {
|
||||
|
@ -40,6 +41,10 @@ class FilteredSearchManager {
|
|||
return [];
|
||||
})
|
||||
.then((searches) => {
|
||||
if (!searches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put any searches that may have come in before
|
||||
// we fetched the saved searches ahead of the already saved ones
|
||||
const resultantSearches = this.recentSearchesStore.setRecentSearches(
|
||||
|
@ -223,11 +228,7 @@ class FilteredSearchManager {
|
|||
}
|
||||
|
||||
addInputContainerFocus() {
|
||||
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
||||
|
||||
if (inputContainer) {
|
||||
inputContainer.classList.add('focus');
|
||||
}
|
||||
addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
|
||||
}
|
||||
|
||||
removeInputContainerFocus(e) {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import emojiMap from 'emojis/digests.json';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import { glEmojiTag } from '~/behaviors/gl_emoji';
|
||||
import glRegexp from '~/lib/utils/regexp';
|
||||
import AjaxCache from '~/lib/utils/ajax_cache';
|
||||
import glRegexp from './lib/utils/regexp';
|
||||
import AjaxCache from './lib/utils/ajax_cache';
|
||||
|
||||
function sanitize(str) {
|
||||
return str.replace(/<(?:.|\n)*?>/gm, '');
|
||||
|
@ -33,6 +30,7 @@ class GfmAutoComplete {
|
|||
this.input.each((i, input) => {
|
||||
const $input = $(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
|
||||
// Needed for quick actions with suffixes (ex: /label ~)
|
||||
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
|
||||
|
@ -375,7 +373,12 @@ class GfmAutoComplete {
|
|||
if (this.cachedData[at]) {
|
||||
this.loadData($input, at, this.cachedData[at]);
|
||||
} 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 {
|
||||
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
|
||||
.then((data) => {
|
||||
|
@ -398,6 +401,13 @@ class GfmAutoComplete {
|
|||
this.cachedData = {};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.input.each((i, input) => {
|
||||
const $input = $(input);
|
||||
$input.atwho('destroy');
|
||||
});
|
||||
}
|
||||
|
||||
static isLoading(data) {
|
||||
let dataToInspect = data;
|
||||
if (data && data.length > 0) {
|
||||
|
@ -423,12 +433,14 @@ GfmAutoComplete.atTypeMap = {
|
|||
};
|
||||
|
||||
// Emoji
|
||||
GfmAutoComplete.glEmojiTag = null;
|
||||
GfmAutoComplete.Emoji = {
|
||||
templateFunction(name) {
|
||||
return `<li>
|
||||
${name} ${glEmojiTag(name)}
|
||||
</li>
|
||||
`;
|
||||
// glEmojiTag helper is loaded on-demand in fetchData()
|
||||
if (GfmAutoComplete.glEmojiTag) {
|
||||
return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
|
||||
}
|
||||
return `<li>${name}</li>`;
|
||||
},
|
||||
};
|
||||
// Team Members
|
||||
|
|
|
@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
|
|||
GLForm.prototype.destroy = function() {
|
||||
// Clean form listeners
|
||||
this.clearEventListeners();
|
||||
if (this.autoComplete) {
|
||||
this.autoComplete.destroy();
|
||||
}
|
||||
return this.form.data('gl-form', null);
|
||||
};
|
||||
|
||||
|
@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
|
|||
this.form.addClass('gfm-form');
|
||||
// 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'));
|
||||
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,
|
||||
members: this.enableGFM,
|
||||
issues: this.enableGFM,
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
|
||||
import Cookies from 'js-cookie';
|
||||
import _ from 'underscore';
|
||||
|
||||
export default class GroupName {
|
||||
constructor() {
|
||||
this.titleContainer = document.querySelector('.title-container');
|
||||
this.title = document.querySelector('.title');
|
||||
this.titleWidth = this.title.offsetWidth;
|
||||
this.groupTitle = document.querySelector('.group-title');
|
||||
this.groups = document.querySelectorAll('.group-path');
|
||||
this.toggle = null;
|
||||
this.isHidden = false;
|
||||
this.init();
|
||||
this.titleContainer = document.querySelector('.js-title-container');
|
||||
this.title = this.titleContainer.querySelector('.title');
|
||||
|
||||
if (this.title) {
|
||||
this.titleWidth = this.title.offsetWidth;
|
||||
this.groupTitle = this.titleContainer.querySelector('.group-title');
|
||||
this.groups = this.titleContainer.querySelectorAll('.group-path');
|
||||
this.toggle = null;
|
||||
this.isHidden = false;
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -33,11 +36,20 @@ export default class GroupName {
|
|||
|
||||
createToggle() {
|
||||
this.toggle = document.createElement('button');
|
||||
this.toggle.setAttribute('type', 'button');
|
||||
this.toggle.className = 'text-expander group-name-toggle';
|
||||
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.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();
|
||||
}
|
||||
|
||||
|
|
|
@ -99,8 +99,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
page: currentPath,
|
||||
}, document.title, currentPath);
|
||||
|
||||
this.updateGroups(response.json());
|
||||
this.updatePagination(response.headers);
|
||||
return response.json().then((data) => {
|
||||
this.updateGroups(data);
|
||||
this.updatePagination(response.headers);
|
||||
});
|
||||
})
|
||||
.catch(this.handleErrorResponse);
|
||||
},
|
||||
|
@ -114,18 +116,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
},
|
||||
leaveGroup(group, collection) {
|
||||
this.service.leaveGroup(group.leavePath)
|
||||
.then(resp => resp.json())
|
||||
.then((response) => {
|
||||
$.scrollTo(0);
|
||||
|
||||
this.store.removeGroup(group, collection);
|
||||
|
||||
// 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.';
|
||||
|
||||
if (response.status === 403) {
|
||||
if (error.status === 403) {
|
||||
message = 'Failed to leave the group. Please make sure you are not the only owner';
|
||||
}
|
||||
|
||||
|
|
|
@ -47,8 +47,8 @@ export default class GroupsStore {
|
|||
|
||||
// Map groups to an object
|
||||
groups.map((group) => {
|
||||
mappedGroups[group.id] = group;
|
||||
mappedGroups[group.id].subGroups = {};
|
||||
mappedGroups[`id${group.id}`] = group;
|
||||
mappedGroups[`id${group.id}`].subGroups = {};
|
||||
return group;
|
||||
});
|
||||
|
||||
|
@ -56,26 +56,27 @@ export default class GroupsStore {
|
|||
const currentGroup = mappedGroups[key];
|
||||
if (currentGroup.parentId) {
|
||||
// If the group is not at the root level, add it to its parent array of subGroups.
|
||||
const findParentGroup = mappedGroups[currentGroup.parentId];
|
||||
const findParentGroup = mappedGroups[`id${currentGroup.parentId}`];
|
||||
if (findParentGroup) {
|
||||
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
|
||||
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
|
||||
mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
|
||||
mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
|
||||
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
|
||||
tree[currentGroup.id] = currentGroup;
|
||||
tree[`id${currentGroup.id}`] = currentGroup;
|
||||
} else {
|
||||
// Means the groups hast no direct parent.
|
||||
// Save for later processing, we will add them to its corresponding base group
|
||||
// No parent found. We save it for later processing
|
||||
orphans.push(currentGroup);
|
||||
|
||||
// Add to tree to preserve original order
|
||||
tree[`id${currentGroup.id}`] = currentGroup;
|
||||
}
|
||||
} else {
|
||||
// If the group is at the root level, add it to first level elements array.
|
||||
tree[currentGroup.id] = currentGroup;
|
||||
// If the group is at the top level, add it to first level elements array.
|
||||
tree[`id${currentGroup.id}`] = currentGroup;
|
||||
}
|
||||
|
||||
return key;
|
||||
});
|
||||
|
||||
// Hopefully this array will be empty for most cases
|
||||
if (orphans.length) {
|
||||
orphans.map((orphan) => {
|
||||
let found = false;
|
||||
|
@ -83,11 +84,23 @@ export default class GroupsStore {
|
|||
|
||||
Object.keys(tree).map((key) => {
|
||||
const group = tree[key];
|
||||
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
|
||||
|
||||
if (
|
||||
group &&
|
||||
currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
|
||||
// Make sure the currently selected orphan is not the same as the group
|
||||
// we are checking here otherwise it will end up in an infinite loop
|
||||
currentOrphan.id !== group.id
|
||||
) {
|
||||
group.subGroups[currentOrphan.id] = currentOrphan;
|
||||
group.isOpen = true;
|
||||
currentOrphan.isOrphan = true;
|
||||
found = true;
|
||||
|
||||
// Delete if group was put at the top level. If not the group will be displayed twice.
|
||||
if (tree[`id${currentOrphan.id}`]) {
|
||||
delete tree[`id${currentOrphan.id}`];
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
|
@ -95,7 +108,8 @@ export default class GroupsStore {
|
|||
|
||||
if (!found) {
|
||||
currentOrphan.isOrphan = true;
|
||||
tree[currentOrphan.id] = currentOrphan;
|
||||
|
||||
tree[`id${currentOrphan.id}`] = currentOrphan;
|
||||
}
|
||||
|
||||
return orphan;
|
||||
|
@ -140,7 +154,7 @@ export default class GroupsStore {
|
|||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
removeGroup(group, collection) {
|
||||
Vue.delete(collection, group.id);
|
||||
Vue.delete(collection, `id${group.id}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
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;
|
|
@ -5,6 +5,7 @@
|
|||
/* global SubscriptionSelect */
|
||||
|
||||
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
||||
import SidebarHeightManager from './sidebar_height_manager';
|
||||
|
||||
const HIDDEN_CLASS = 'hidden';
|
||||
const DISABLED_CONTENT_CLASS = 'disabled-content';
|
||||
|
@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
|
|||
return navbarHeight + layoutNavHeight + subNavScroll;
|
||||
}
|
||||
|
||||
initSidebar() {
|
||||
if (!this.navHeight) {
|
||||
this.navHeight = this.getNavHeight();
|
||||
}
|
||||
|
||||
if (!this.sidebarInitialized) {
|
||||
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
|
||||
this.sidebarInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
setupBulkUpdateActions() {
|
||||
IssuableBulkUpdateActions.setOriginalDropdownData();
|
||||
}
|
||||
|
@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
|
|||
this.toggleCheckboxDisplay(enable);
|
||||
|
||||
if (enable) {
|
||||
this.initSidebar();
|
||||
SidebarHeightManager.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
|
|||
this.$bulkEditSubmitBtn.enable();
|
||||
}
|
||||
}
|
||||
// loosely based on method of the same name in right_sidebar.js
|
||||
setSidebarHeight() {
|
||||
const currentScrollDepth = window.pageYOffset || 0;
|
||||
const diff = this.navHeight - currentScrollDepth;
|
||||
|
||||
if (diff > 0) {
|
||||
this.$sidebar.outerHeight(window.innerHeight - diff);
|
||||
} else {
|
||||
this.$sidebar.outerHeight('100%');
|
||||
}
|
||||
}
|
||||
|
||||
static getCheckedIssueIds() {
|
||||
const $checkedIssues = $('.selected_issue:checked');
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* 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 ZenMode */
|
||||
/* global Autosave */
|
||||
/* global dateFormat */
|
||||
/* global Pikaday */
|
||||
|
||||
import UsersSelect from './users_select';
|
||||
import GfmAutoComplete from './gfm_auto_complete';
|
||||
import ZenMode from './zen_mode';
|
||||
|
||||
(function() {
|
||||
this.IssuableForm = (function() {
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
import 'vendor/jquery.waitforimages';
|
||||
import '~/lib/utils/text_utility';
|
||||
import './flash';
|
||||
import './task_list';
|
||||
import TaskList from './task_list';
|
||||
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
|
||||
import IssuablesHelper from './helpers/issuables_helper';
|
||||
|
||||
class Issue {
|
||||
constructor() {
|
||||
if ($('a.btn-close').length) {
|
||||
this.taskList = new gl.TaskList({
|
||||
this.taskList = new TaskList({
|
||||
dataType: 'issue',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
|
@ -28,6 +29,11 @@ class Issue {
|
|||
Issue.initMergeRequests();
|
||||
Issue.initRelatedBranches();
|
||||
|
||||
this.closeButtons = $('a.btn-close');
|
||||
this.reopenButtons = $('a.btn-reopen');
|
||||
|
||||
this.initCloseReopenReport();
|
||||
|
||||
if (Issue.createMrDropdownWrap) {
|
||||
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
|
||||
}
|
||||
|
@ -35,13 +41,8 @@ class Issue {
|
|||
|
||||
initIssueBtnEventListeners() {
|
||||
const issueFailMessage = 'Unable to update this issue at this time.';
|
||||
const closeButtons = $('a.btn-close');
|
||||
const isClosedBadge = $('div.status-box-closed');
|
||||
const isOpenBadge = $('div.status-box-open');
|
||||
const projectIssuesCounter = $('.issue_counter');
|
||||
const reopenButtons = $('a.btn-reopen');
|
||||
|
||||
return closeButtons.add(reopenButtons).on('click', (e) => {
|
||||
return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
|
||||
var $button, shouldSubmit, url;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
@ -50,7 +51,9 @@ class Issue {
|
|||
if (shouldSubmit) {
|
||||
Issue.submitNoteForm($button.closest('form'));
|
||||
}
|
||||
$button.prop('disabled', true);
|
||||
|
||||
this.disableCloseReopenButton($button);
|
||||
|
||||
url = $button.attr('href');
|
||||
return $.ajax({
|
||||
type: 'PUT',
|
||||
|
@ -58,15 +61,19 @@ class Issue {
|
|||
})
|
||||
.fail(() => new Flash(issueFailMessage))
|
||||
.done((data) => {
|
||||
const isClosedBadge = $('div.status-box-closed');
|
||||
const isOpenBadge = $('div.status-box-open');
|
||||
const projectIssuesCounter = $('.issue_counter');
|
||||
|
||||
if ('id' in data) {
|
||||
$(document).trigger('issuable:change');
|
||||
|
||||
const isClosed = $button.hasClass('btn-close');
|
||||
closeButtons.toggleClass('hidden', isClosed);
|
||||
reopenButtons.toggleClass('hidden', !isClosed);
|
||||
isClosedBadge.toggleClass('hidden', !isClosed);
|
||||
isOpenBadge.toggleClass('hidden', isClosed);
|
||||
|
||||
this.toggleCloseReopenButton(isClosed);
|
||||
|
||||
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
|
||||
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
|
||||
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
|
||||
|
@ -83,12 +90,34 @@ class Issue {
|
|||
} else {
|
||||
new Flash(issueFailMessage);
|
||||
}
|
||||
|
||||
$button.prop('disabled', false);
|
||||
})
|
||||
.then(() => {
|
||||
this.disableCloseReopenButton($button, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initCloseReopenReport() {
|
||||
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
|
||||
|
||||
if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button');
|
||||
if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button');
|
||||
}
|
||||
|
||||
disableCloseReopenButton($button, shouldDisable) {
|
||||
if (this.closeReopenReportToggle) {
|
||||
this.closeReopenReportToggle.setDisable(shouldDisable);
|
||||
} else {
|
||||
$button.prop('disabled', shouldDisable);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCloseReopenButton(isClosed) {
|
||||
if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed);
|
||||
this.closeButtons.toggleClass('hidden', isClosed);
|
||||
this.reopenButtons.toggleClass('hidden', !isClosed);
|
||||
}
|
||||
|
||||
static submitNoteForm(form) {
|
||||
var noteText;
|
||||
noteText = form.find("textarea.js-note-text").val();
|
||||
|
|
|
@ -202,16 +202,7 @@ export default {
|
|||
this.poll = new Poll({
|
||||
resource: this.service,
|
||||
method: 'getData',
|
||||
successCallback: (res) => {
|
||||
const data = res.json();
|
||||
const shouldUpdate = this.store.stateShouldUpdate(data);
|
||||
|
||||
this.store.updateState(data);
|
||||
|
||||
if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
|
||||
this.store.formState.lockedWarningVisible = true;
|
||||
}
|
||||
},
|
||||
successCallback: res => res.json().then(data => this.store.updateState(data)),
|
||||
errorCallback(err) {
|
||||
throw new Error(err);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import animateMixin from '../mixins/animate';
|
||||
import TaskList from '../../task_list';
|
||||
|
||||
export default {
|
||||
mixins: [animateMixin],
|
||||
|
@ -46,7 +47,7 @@
|
|||
|
||||
if (this.canUpdate) {
|
||||
// eslint-disable-next-line no-new
|
||||
new gl.TaskList({
|
||||
new TaskList({
|
||||
dataType: 'issue',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
ref="textarea"
|
||||
slot="textarea"
|
||||
placeholder="Write a comment or drag your files here..."
|
||||
@keydown.meta.enter="updateIssuable">
|
||||
@keydown.meta.enter="updateIssuable"
|
||||
@keydown.ctrl.enter="updateIssuable">
|
||||
</textarea>
|
||||
</markdown-field>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
tooltipMixin,
|
||||
],
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
formState: {
|
||||
type: Object,
|
||||
|
@ -71,9 +71,9 @@
|
|||
data-placeholder="Move to a different project" />
|
||||
</div>
|
||||
<span
|
||||
v-tooltip
|
||||
data-placement="auto top"
|
||||
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
|
||||
ref="tooltip">
|
||||
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
|
||||
<i
|
||||
class="fa fa-question-circle"
|
||||
aria-hidden="true">
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
placeholder="Issue title"
|
||||
aria-label="Issue title"
|
||||
v-model="formState.title"
|
||||
@keydown.meta.enter="updateIssuable" />
|
||||
@keydown.meta.enter="updateIssuable"
|
||||
@keydown.ctrl.enter="updateIssuable" />
|
||||
</fieldset>
|
||||
</template>
|
||||
|
|
|
@ -12,6 +12,10 @@ export default class Store {
|
|||
}
|
||||
|
||||
updateState(data) {
|
||||
if (this.stateShouldUpdate(data)) {
|
||||
this.formState.lockedWarningVisible = true;
|
||||
}
|
||||
|
||||
this.state.titleHtml = data.title;
|
||||
this.state.titleText = data.title_text;
|
||||
this.state.descriptionHtml = data.description;
|
||||
|
@ -23,10 +27,8 @@ export default class Store {
|
|||
}
|
||||
|
||||
stateShouldUpdate(data) {
|
||||
return {
|
||||
title: this.state.titleText !== data.title_text,
|
||||
description: this.state.descriptionText !== data.description_text,
|
||||
};
|
||||
return this.state.titleText !== data.title_text ||
|
||||
this.state.descriptionText !== data.description_text;
|
||||
}
|
||||
|
||||
setFormState(state) {
|
||||
|
|
|
@ -39,6 +39,17 @@
|
|||
runnerId() {
|
||||
return `#${this.job.runner.id}`;
|
||||
},
|
||||
renderBlock() {
|
||||
return this.job.merge_request ||
|
||||
this.job.duration ||
|
||||
this.job.finished_data ||
|
||||
this.job.erased_at ||
|
||||
this.job.queued ||
|
||||
this.job.runner ||
|
||||
this.job.coverage ||
|
||||
this.job.tags.length ||
|
||||
this.job.cancel_path;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -63,7 +74,7 @@
|
|||
Retry
|
||||
</a>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div :class="{block : renderBlock }">
|
||||
<p
|
||||
class="build-detail-row js-job-mr"
|
||||
v-if="job.merge_request">
|
||||
|
|
|
@ -26,14 +26,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
mounted() {
|
||||
this.mediator.initBuildClass();
|
||||
},
|
||||
updated() {
|
||||
// Wait for flash message to be appended
|
||||
Vue.nextTick(() => {
|
||||
if (this.mediator.build) {
|
||||
this.mediator.build.verifyTopPosition();
|
||||
}
|
||||
});
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('job-header', {
|
||||
props: {
|
||||
|
|
|
@ -54,9 +54,8 @@ export default class JobMediator {
|
|||
}
|
||||
|
||||
successCallback(response) {
|
||||
const data = response.json();
|
||||
this.state.isLoading = false;
|
||||
this.store.storeJob(data);
|
||||
return response.json().then(data => this.store.storeJob(data));
|
||||
}
|
||||
|
||||
errorCallback() {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
}
|
||||
|
||||
bindEvents() {
|
||||
this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
|
||||
return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
|
||||
}
|
||||
|
||||
|
@ -36,6 +37,11 @@
|
|||
_this.toggleEmptyState($label, $btn, action);
|
||||
}
|
||||
|
||||
onButtonActionClick(e) {
|
||||
e.stopPropagation();
|
||||
$(e.currentTarget).tooltip('hide');
|
||||
}
|
||||
|
||||
toggleEmptyState($label, $btn, action) {
|
||||
this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
|
||||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
import NewNavSidebar from './new_sidebar';
|
||||
|
||||
(function() {
|
||||
var hideEndFade;
|
||||
|
@ -53,6 +55,11 @@ import _ from 'underscore';
|
|||
}
|
||||
|
||||
$(() => {
|
||||
if (Cookies.get('new_nav') === 'true') {
|
||||
const newNavSidebar = new NewNavSidebar();
|
||||
newNavSidebar.bindEvents();
|
||||
}
|
||||
|
||||
$(window).on('scroll', _.throttle(applyScrollNavClass, 100));
|
||||
});
|
||||
}).call(window);
|
||||
|
|
|
@ -86,18 +86,25 @@
|
|||
// This is required to handle non-unicode characters in hash
|
||||
hash = decodeURIComponent(hash);
|
||||
|
||||
var fixedTabs = document.querySelector('.js-tabs-affix');
|
||||
var fixedNav = document.querySelector('.navbar-gitlab');
|
||||
|
||||
var adjustment = 0;
|
||||
if (fixedNav) adjustment -= fixedNav.offsetHeight;
|
||||
|
||||
// scroll to user-generated markdown anchor if we cannot find a match
|
||||
if (document.getElementById(hash) === null) {
|
||||
var target = document.getElementById('user-content-' + hash);
|
||||
if (target && target.scrollIntoView) {
|
||||
target.scrollIntoView(true);
|
||||
window.scrollBy(0, adjustment);
|
||||
}
|
||||
} else {
|
||||
// only adjust for fixedTabs when not targeting user-generated content
|
||||
var fixedTabs = document.querySelector('.js-tabs-affix');
|
||||
if (fixedTabs) {
|
||||
window.scrollBy(0, -fixedTabs.offsetHeight);
|
||||
adjustment -= fixedTabs.offsetHeight;
|
||||
}
|
||||
window.scrollBy(0, adjustment);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
const DateFix = {
|
||||
dashedFix(val) {
|
||||
const [y, m, d] = val.split('-');
|
||||
return new Date(y, m - 1, d);
|
||||
},
|
||||
};
|
||||
|
||||
export default DateFix;
|
|
@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
|
|||
return timefor;
|
||||
};
|
||||
|
||||
w.gl.utils.cachedTimeagoElements = [];
|
||||
w.gl.utils.renderTimeago = function($els) {
|
||||
if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
|
||||
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
|
||||
} else if ($els) {
|
||||
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
|
||||
}
|
||||
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
|
||||
|
||||
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
|
||||
};
|
||||
|
||||
w.gl.utils.updateTimeagoText = function(el) {
|
||||
const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang);
|
||||
|
||||
if (el.textContent !== formattedDate) {
|
||||
el.textContent = formattedDate;
|
||||
}
|
||||
};
|
||||
|
||||
w.gl.utils.initTimeagoTimeout = function() {
|
||||
gl.utils.renderTimeago();
|
||||
|
||||
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
|
||||
// timeago.js sets timeouts internally for each timeago value to be updated in real time
|
||||
gl.utils.getTimeago().render(timeagoEls, lang);
|
||||
};
|
||||
|
||||
w.gl.utils.getDayDifference = function(a, b) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export const addClassIfElementExists = (element, className) => {
|
||||
if (element) {
|
||||
element.classList.add(className);
|
||||
}
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
export default {
|
||||
ABORTED: 0,
|
||||
NO_CONTENT: 204,
|
||||
OK: 200,
|
||||
};
|
||||
|
|
|
@ -81,6 +81,9 @@ export default class Poll {
|
|||
})
|
||||
.catch((error) => {
|
||||
notificationCallback(false);
|
||||
if (error.status === httpStatusCodes.ABORTED) {
|
||||
return;
|
||||
}
|
||||
errorCallback(error);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -94,8 +94,8 @@ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
|
|||
|
||||
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
|
||||
|
||||
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
|
||||
if (blockTag != null) {
|
||||
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
|
||||
if (blockTag != null && blockTag !== '') {
|
||||
insertText = this.blockTagText(text, textArea, blockTag, selected);
|
||||
} else {
|
||||
insertText = selectedSplit.map(function(val) {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -70,7 +70,7 @@ import './ajax_loading_spinner';
|
|||
import './api';
|
||||
import './aside';
|
||||
import './autosave';
|
||||
import AwardsHandler from './awards_handler';
|
||||
import loadAwardsHandler from './awards_handler';
|
||||
import './breakpoints';
|
||||
import './broadcast_message';
|
||||
import './build';
|
||||
|
@ -143,25 +143,12 @@ import './render_math';
|
|||
import './right_sidebar';
|
||||
import './search';
|
||||
import './search_autocomplete';
|
||||
import './signin_tabs_memoizer';
|
||||
import './single_file_diff';
|
||||
import './smart_interval';
|
||||
import './snippets_list';
|
||||
import './star';
|
||||
import './subscription';
|
||||
import './subscription_select';
|
||||
import './syntax_highlight';
|
||||
import './task_list';
|
||||
import './todos';
|
||||
import './usage_ping';
|
||||
import './user';
|
||||
import './user_tabs';
|
||||
import './username_validator';
|
||||
import './users_select';
|
||||
import './version_check_image';
|
||||
import './visibility_select';
|
||||
import './wikis';
|
||||
import './zen_mode';
|
||||
|
||||
// eslint-disable-next-line global-require, import/no-commonjs
|
||||
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
|
||||
|
@ -298,9 +285,10 @@ $(function () {
|
|||
// Commit show suppressed diff
|
||||
});
|
||||
$('.navbar-toggle').on('click', function () {
|
||||
$('.header-content .title').toggle();
|
||||
$('.header-content .title, .header-content .navbar-sub-nav').toggle();
|
||||
$('.header-content .header-logo').toggle();
|
||||
$('.header-content .navbar-collapse').toggle();
|
||||
$('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle();
|
||||
return $('.navbar-toggle').toggleClass('active');
|
||||
});
|
||||
// Show/hide comments on diff
|
||||
|
@ -353,10 +341,10 @@ $(function () {
|
|||
$window.off('resize.app').on('resize.app', function () {
|
||||
return fitSidebarForSize();
|
||||
});
|
||||
gl.awardsHandler = new AwardsHandler();
|
||||
loadAwardsHandler();
|
||||
new Aside();
|
||||
|
||||
gl.utils.initTimeagoTimeout();
|
||||
gl.utils.renderTimeago();
|
||||
|
||||
$(document).trigger('init.scrolling-tabs');
|
||||
});
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
/* global MergeRequestTabs */
|
||||
|
||||
import 'vendor/jquery.waitforimages';
|
||||
import './task_list';
|
||||
import TaskList from './task_list';
|
||||
import './merge_request_tabs';
|
||||
import IssuablesHelper from './helpers/issuables_helper';
|
||||
|
||||
(function() {
|
||||
this.MergeRequest = (function() {
|
||||
|
@ -21,11 +22,14 @@ import './merge_request_tabs';
|
|||
return _this.showAllCommits();
|
||||
};
|
||||
})(this));
|
||||
|
||||
this.initTabs();
|
||||
this.initMRBtnListeners();
|
||||
this.initCommitMessageListeners();
|
||||
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
|
||||
|
||||
if ($("a.btn-close").length) {
|
||||
this.taskList = new gl.TaskList({
|
||||
this.taskList = new TaskList({
|
||||
dataType: 'merge_request',
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
|
@ -64,11 +68,15 @@ import './merge_request_tabs';
|
|||
if (shouldSubmit && $this.data('submitted')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
|
||||
|
||||
if (shouldSubmit) {
|
||||
if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
return _this.submitNoteForm($this.closest('form'), $this);
|
||||
|
||||
_this.submitNoteForm($this.closest('form'), $this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
this.resetViewContainer();
|
||||
this.mountPipelinesView();
|
||||
} else {
|
||||
this.expandView();
|
||||
if (Breakpoints.get().getBreakpointSize() !== 'xs') {
|
||||
this.expandView();
|
||||
}
|
||||
this.resetViewContainer();
|
||||
this.destroyPipelinesView();
|
||||
}
|
||||
|
@ -155,7 +157,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
|
||||
scrollToElement(container) {
|
||||
if (location.hash) {
|
||||
const offset = -$('.js-tabs-affix').outerHeight();
|
||||
const offset = 0 - (
|
||||
$('.navbar-gitlab').outerHeight() +
|
||||
$('.js-tabs-affix').outerHeight()
|
||||
);
|
||||
const $el = $(`${container} ${location.hash}:not(.match)`);
|
||||
if ($el.length) {
|
||||
$.scrollTo($el[0], { offset });
|
||||
|
@ -165,9 +170,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
|
||||
// Activate a tab based on the current action
|
||||
activateTab(action) {
|
||||
const activate = action === 'show' ? 'notes' : action;
|
||||
// important note: the .tab('show') method triggers 'shown.bs.tab' event itself
|
||||
$(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
|
||||
$(`.merge-request-tabs a[data-action='${action}']`).tab('show');
|
||||
}
|
||||
|
||||
// Replaces the current Merge Request-specific action in the URL with a new one
|
||||
|
@ -182,7 +186,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
|
||||
//
|
||||
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
|
||||
// setCurrentAction('notes')
|
||||
// setCurrentAction('show')
|
||||
// location.pathname # => "/namespace/project/merge_requests/1"
|
||||
//
|
||||
// location.pathname # => "/namespace/project/merge_requests/1/diffs"
|
||||
|
@ -191,13 +195,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
//
|
||||
// Returns the new URL String
|
||||
setCurrentAction(action) {
|
||||
this.currentAction = action === 'show' ? 'notes' : action;
|
||||
this.currentAction = action;
|
||||
|
||||
// Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
|
||||
let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
|
||||
// Remove a trailing '/commits' '/diffs' '/pipelines'
|
||||
let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
|
||||
|
||||
// Append the new action if we're on a tab other than 'notes'
|
||||
if (this.currentAction !== 'notes') {
|
||||
if (this.currentAction !== 'show' && this.currentAction !== 'new') {
|
||||
newState += `/${this.currentAction}`;
|
||||
}
|
||||
|
||||
|
@ -291,7 +295,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
// Scroll any linked note into view
|
||||
// Similar to `toggler_behavior` in the discussion tab
|
||||
const hash = window.gl.utils.getLocationHash();
|
||||
const anchor = hash && $container.find(`[id="${hash}"]`);
|
||||
const anchor = hash && $container.find(`.note[id="${hash}"]`);
|
||||
if (anchor && anchor.length > 0) {
|
||||
const notesContent = anchor.closest('.notes_content');
|
||||
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
|
||||
|
@ -301,6 +305,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
|
|||
forceShow: true,
|
||||
});
|
||||
anchor[0].scrollIntoView();
|
||||
window.gl.utils.handleLocationHash();
|
||||
// We have multiple elements on the page with `#note_xxx`
|
||||
// (discussion and diff tabs) and `:target` only applies to the first
|
||||
anchor.addClass('target');
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<script>
|
||||
/* global Flash */
|
||||
import _ from 'underscore';
|
||||
import statusCodes from '../../lib/utils/http_status';
|
||||
import MonitoringService from '../services/monitoring_service';
|
||||
import monitoringRow from './monitoring_row.vue';
|
||||
import monitoringState from './monitoring_state.vue';
|
||||
import MonitoringStore from '../stores/monitoring_store';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
|
||||
data() {
|
||||
const metricsData = document.querySelector('#prometheus-graphs').dataset;
|
||||
const store = new MonitoringStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
state: 'gettingStarted',
|
||||
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
|
||||
documentationPath: metricsData.documentationPath,
|
||||
settingsPath: metricsData.settingsPath,
|
||||
endpoint: metricsData.additionalMetrics,
|
||||
deploymentEndpoint: metricsData.deploymentEndpoint,
|
||||
showEmptyState: true,
|
||||
backOffRequestCounter: 0,
|
||||
updateAspectRatio: false,
|
||||
updatedAspectRatios: 0,
|
||||
resizeThrottled: {},
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
monitoringRow,
|
||||
monitoringState,
|
||||
},
|
||||
|
||||
methods: {
|
||||
getGraphsData() {
|
||||
const maxNumberOfRequests = 3;
|
||||
this.state = 'loading';
|
||||
gl.utils.backOff((next, stop) => {
|
||||
this.service.get().then((resp) => {
|
||||
if (resp.status === statusCodes.NO_CONTENT) {
|
||||
this.backOffRequestCounter = this.backOffRequestCounter += 1;
|
||||
if (this.backOffRequestCounter < maxNumberOfRequests) {
|
||||
next();
|
||||
} else {
|
||||
stop(new Error('Failed to connect to the prometheus server'));
|
||||
}
|
||||
} else {
|
||||
stop(resp);
|
||||
}
|
||||
}).catch(stop);
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.status === statusCodes.NO_CONTENT) {
|
||||
this.state = 'unableToConnect';
|
||||
return false;
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then((metricGroupsData) => {
|
||||
if (!metricGroupsData) return false;
|
||||
this.store.storeMetrics(metricGroupsData.data);
|
||||
return this.getDeploymentData();
|
||||
})
|
||||
.then((deploymentData) => {
|
||||
if (deploymentData !== false) {
|
||||
this.store.storeDeploymentData(deploymentData.deployments);
|
||||
this.showEmptyState = false;
|
||||
}
|
||||
return {};
|
||||
})
|
||||
.catch(() => {
|
||||
this.state = 'unableToConnect';
|
||||
});
|
||||
},
|
||||
|
||||
getDeploymentData() {
|
||||
return this.service.getDeploymentData(this.deploymentEndpoint)
|
||||
.then(resp => resp.json())
|
||||
.catch(() => new Flash('Error getting deployment information.'));
|
||||
},
|
||||
|
||||
resize() {
|
||||
this.updateAspectRatio = true;
|
||||
},
|
||||
|
||||
toggleAspectRatio() {
|
||||
this.updatedAspectRatios = this.updatedAspectRatios += 1;
|
||||
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
|
||||
this.updateAspectRatio = !this.updateAspectRatio;
|
||||
this.updatedAspectRatios = 0;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
created() {
|
||||
this.service = new MonitoringService(this.endpoint);
|
||||
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
|
||||
window.removeEventListener('resize', this.resizeThrottled, false);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.resizeThrottled = _.throttle(this.resize, 600);
|
||||
if (!this.hasMetrics) {
|
||||
this.state = 'gettingStarted';
|
||||
} else {
|
||||
this.getGraphsData();
|
||||
window.addEventListener('resize', this.resizeThrottled, false);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="prometheus-graphs"
|
||||
v-if="!showEmptyState">
|
||||
<div
|
||||
class="row"
|
||||
v-for="(groupData, index) in store.groups"
|
||||
:key="index">
|
||||
<div
|
||||
class="col-md-12">
|
||||
<div
|
||||
class="panel panel-default prometheus-panel">
|
||||
<div
|
||||
class="panel-heading">
|
||||
<h4>{{groupData.group}}</h4>
|
||||
</div>
|
||||
<div
|
||||
class="panel-body">
|
||||
<monitoring-row
|
||||
v-for="(row, index) in groupData.metrics"
|
||||
:key="index"
|
||||
:row-data="row"
|
||||
:update-aspect-ratio="updateAspectRatio"
|
||||
:deployment-data="store.deploymentData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<monitoring-state
|
||||
:selected-state="state"
|
||||
:documentation-path="documentationPath"
|
||||
:settings-path="settingsPath"
|
||||
v-else
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,297 @@
|
|||
<script>
|
||||
/* global Breakpoints */
|
||||
import d3 from 'd3';
|
||||
import monitoringLegends from './monitoring_legends.vue';
|
||||
import monitoringFlag from './monitoring_flag.vue';
|
||||
import monitoringDeployment from './monitoring_deployment.vue';
|
||||
import MonitoringMixin from '../mixins/monitoring_mixins';
|
||||
import eventHub from '../event_hub';
|
||||
import measurements from '../utils/measurements';
|
||||
import { formatRelevantDigits } from '../../lib/utils/number_utils';
|
||||
|
||||
const bisectDate = d3.bisector(d => d.time).left;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
columnData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
classType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
updateAspectRatio: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
deploymentData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [MonitoringMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
graphHeight: 450,
|
||||
graphWidth: 600,
|
||||
graphHeightOffset: 120,
|
||||
xScale: {},
|
||||
yScale: {},
|
||||
margin: {},
|
||||
data: [],
|
||||
breakpointHandler: Breakpoints.get(),
|
||||
unitOfDisplay: '',
|
||||
areaColorRgb: '#8fbce8',
|
||||
lineColorRgb: '#1f78d1',
|
||||
yAxisLabel: '',
|
||||
legendTitle: '',
|
||||
reducedDeploymentData: [],
|
||||
area: '',
|
||||
line: '',
|
||||
measurements: measurements.large,
|
||||
currentData: {
|
||||
time: new Date(),
|
||||
value: 0,
|
||||
},
|
||||
currentYCoordinate: 0,
|
||||
currentXCoordinate: 0,
|
||||
currentFlagPosition: 0,
|
||||
metricUsage: '',
|
||||
showFlag: false,
|
||||
showDeployInfo: true,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
monitoringLegends,
|
||||
monitoringFlag,
|
||||
monitoringDeployment,
|
||||
},
|
||||
|
||||
computed: {
|
||||
outterViewBox() {
|
||||
return `0 0 ${this.graphWidth} ${this.graphHeight}`;
|
||||
},
|
||||
|
||||
innerViewBox() {
|
||||
if ((this.graphWidth - 150) > 0) {
|
||||
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
|
||||
}
|
||||
return '0 0 0 0';
|
||||
},
|
||||
|
||||
axisTransform() {
|
||||
return `translate(70, ${this.graphHeight - 100})`;
|
||||
},
|
||||
|
||||
paddingBottomRootSvg() {
|
||||
return {
|
||||
paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
draw() {
|
||||
const breakpointSize = this.breakpointHandler.getBreakpointSize();
|
||||
const query = this.columnData.queries[0];
|
||||
this.margin = measurements.large.margin;
|
||||
if (breakpointSize === 'xs' || breakpointSize === 'sm') {
|
||||
this.graphHeight = 300;
|
||||
this.margin = measurements.small.margin;
|
||||
this.measurements = measurements.small;
|
||||
}
|
||||
this.data = query.result[0].values;
|
||||
this.unitOfDisplay = query.unit || '';
|
||||
this.yAxisLabel = this.columnData.y_label || 'Values';
|
||||
this.legendTitle = query.label || 'Average';
|
||||
this.graphWidth = this.$refs.baseSvg.clientWidth -
|
||||
this.margin.left - this.margin.right;
|
||||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||
if (this.data !== undefined) {
|
||||
this.renderAxesPaths();
|
||||
this.formatDeployments();
|
||||
}
|
||||
},
|
||||
|
||||
handleMouseOverGraph(e) {
|
||||
let point = this.$refs.graphData.createSVGPoint();
|
||||
point.x = e.clientX;
|
||||
point.y = e.clientY;
|
||||
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
|
||||
point.x = point.x += 7;
|
||||
const timeValueOverlay = this.xScale.invert(point.x);
|
||||
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1);
|
||||
const d0 = this.data[overlayIndex - 1];
|
||||
const d1 = this.data[overlayIndex];
|
||||
if (d0 === undefined || d1 === undefined) return;
|
||||
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
|
||||
this.currentData = evalTime ? d1 : d0;
|
||||
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time));
|
||||
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
|
||||
this.currentYCoordinate = this.yScale(this.currentData.value);
|
||||
|
||||
if (this.currentXCoordinate > (this.graphWidth - 200)) {
|
||||
this.currentFlagPosition = this.currentXCoordinate - 103;
|
||||
} else {
|
||||
this.currentFlagPosition = this.currentXCoordinate;
|
||||
}
|
||||
|
||||
if (currentDeployXPos) {
|
||||
this.showFlag = false;
|
||||
} else {
|
||||
this.showFlag = true;
|
||||
}
|
||||
|
||||
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
|
||||
},
|
||||
|
||||
renderAxesPaths() {
|
||||
const axisXScale = d3.time.scale()
|
||||
.range([0, this.graphWidth]);
|
||||
this.yScale = d3.scale.linear()
|
||||
.range([this.graphHeight - this.graphHeightOffset, 0]);
|
||||
axisXScale.domain(d3.extent(this.data, d => d.time));
|
||||
this.yScale.domain([0, d3.max(this.data.map(d => d.value))]);
|
||||
|
||||
const xAxis = d3.svg.axis()
|
||||
.scale(axisXScale)
|
||||
.ticks(measurements.xTicks)
|
||||
.orient('bottom');
|
||||
|
||||
const yAxis = d3.svg.axis()
|
||||
.scale(this.yScale)
|
||||
.ticks(measurements.yTicks)
|
||||
.orient('left');
|
||||
|
||||
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
|
||||
|
||||
const width = this.graphWidth;
|
||||
d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
|
||||
.selectAll('.tick')
|
||||
.each(function createTickLines(d, i) {
|
||||
if (i > 0) {
|
||||
d3.select(this).select('line')
|
||||
.attr('x2', width)
|
||||
.attr('class', 'axis-tick');
|
||||
} // Avoid adding the class to the first tick, to prevent coloring
|
||||
}); // This will select all of the ticks once they're rendered
|
||||
|
||||
this.xScale = d3.time.scale()
|
||||
.range([0, this.graphWidth - 70]);
|
||||
|
||||
this.xScale.domain(d3.extent(this.data, d => d.time));
|
||||
|
||||
const areaFunction = d3.svg.area()
|
||||
.x(d => this.xScale(d.time))
|
||||
.y0(this.graphHeight - this.graphHeightOffset)
|
||||
.y1(d => this.yScale(d.value))
|
||||
.interpolate('linear');
|
||||
|
||||
const lineFunction = d3.svg.line()
|
||||
.x(d => this.xScale(d.time))
|
||||
.y(d => this.yScale(d.value));
|
||||
|
||||
this.line = lineFunction(this.data);
|
||||
|
||||
this.area = areaFunction(this.data);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
updateAspectRatio() {
|
||||
if (this.updateAspectRatio) {
|
||||
this.graphHeight = 450;
|
||||
this.graphWidth = 600;
|
||||
this.measurements = measurements.large;
|
||||
this.draw();
|
||||
eventHub.$emit('toggleAspectRatio');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.draw();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="classType">
|
||||
<h5
|
||||
class="text-center graph-title">
|
||||
{{columnData.title}}
|
||||
</h5>
|
||||
<div
|
||||
class="prometheus-svg-container"
|
||||
:style="paddingBottomRootSvg">
|
||||
<svg
|
||||
:viewBox="outterViewBox"
|
||||
ref="baseSvg">
|
||||
<g
|
||||
class="x-axis"
|
||||
:transform="axisTransform">
|
||||
</g>
|
||||
<g
|
||||
class="y-axis"
|
||||
transform="translate(70, 20)">
|
||||
</g>
|
||||
<monitoring-legends
|
||||
:graph-width="graphWidth"
|
||||
:graph-height="graphHeight"
|
||||
:margin="margin"
|
||||
:measurements="measurements"
|
||||
:area-color-rgb="areaColorRgb"
|
||||
:legend-title="legendTitle"
|
||||
:y-axis-label="yAxisLabel"
|
||||
:metric-usage="metricUsage"
|
||||
/>
|
||||
<svg
|
||||
class="graph-data"
|
||||
:viewBox="innerViewBox"
|
||||
ref="graphData">
|
||||
<path
|
||||
class="metric-area"
|
||||
:d="area"
|
||||
:fill="areaColorRgb"
|
||||
transform="translate(-5, 20)">
|
||||
</path>
|
||||
<path
|
||||
class="metric-line"
|
||||
:d="line"
|
||||
:stroke="lineColorRgb"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
transform="translate(-5, 20)">
|
||||
</path>
|
||||
<rect
|
||||
class="prometheus-graph-overlay"
|
||||
:width="(graphWidth - 70)"
|
||||
:height="(graphHeight - 100)"
|
||||
transform="translate(-5, 20)"
|
||||
ref="graphOverlay"
|
||||
@mousemove="handleMouseOverGraph($event)">
|
||||
</rect>
|
||||
<monitoring-deployment
|
||||
:show-deploy-info="showDeployInfo"
|
||||
:deployment-data="reducedDeploymentData"
|
||||
:graph-height="graphHeight"
|
||||
:graph-height-offset="graphHeightOffset"
|
||||
/>
|
||||
<monitoring-flag
|
||||
v-if="showFlag"
|
||||
:current-x-coordinate="currentXCoordinate"
|
||||
:current-y-coordinate="currentYCoordinate"
|
||||
:current-data="currentData"
|
||||
:current-flag-position="currentFlagPosition"
|
||||
:graph-height="graphHeight"
|
||||
:graph-height-offset="graphHeightOffset"
|
||||
/>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,136 @@
|
|||
<script>
|
||||
import {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showDeployInfo: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
deploymentData: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeightOffset: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
calculatedHeight() {
|
||||
return this.graphHeight - this.graphHeightOffset;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refText(d) {
|
||||
return d.tag ? d.ref : d.sha.slice(0, 6);
|
||||
},
|
||||
|
||||
formatTime(deploymentTime) {
|
||||
return timeFormat(deploymentTime);
|
||||
},
|
||||
|
||||
formatDate(deploymentTime) {
|
||||
return dateFormat(deploymentTime);
|
||||
},
|
||||
|
||||
nameDeploymentClass(deployment) {
|
||||
return `deploy-info-${deployment.id}`;
|
||||
},
|
||||
|
||||
transformDeploymentGroup(deployment) {
|
||||
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<g
|
||||
class="deploy-info"
|
||||
v-if="showDeployInfo">
|
||||
<g
|
||||
v-for="(deployment, index) in deploymentData"
|
||||
:key="index"
|
||||
:class="nameDeploymentClass(deployment)"
|
||||
:transform="transformDeploymentGroup(deployment)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
:height="calculatedHeight"
|
||||
width="3"
|
||||
fill="url(#shadow-gradient)">
|
||||
</rect>
|
||||
<line
|
||||
class="deployment-line"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
:y2="calculatedHeight"
|
||||
stroke="#000">
|
||||
</line>
|
||||
<svg
|
||||
v-if="deployment.showDeploymentFlag"
|
||||
class="js-deploy-info-box"
|
||||
x="3"
|
||||
y="0"
|
||||
width="92"
|
||||
height="60">
|
||||
<rect
|
||||
class="rect-text-metric deploy-info-rect rect-metric"
|
||||
x="1"
|
||||
y="1"
|
||||
rx="2"
|
||||
width="90"
|
||||
height="58">
|
||||
</rect>
|
||||
<g
|
||||
transform="translate(5, 2)">
|
||||
<text
|
||||
class="deploy-info-text text-metric-bold">
|
||||
{{refText(deployment)}}
|
||||
</text>
|
||||
</g>
|
||||
<text
|
||||
class="deploy-info-text"
|
||||
y="18"
|
||||
transform="translate(5, 2)">
|
||||
{{formatDate(deployment.time)}}
|
||||
</text>
|
||||
<text
|
||||
class="deploy-info-text text-metric-bold"
|
||||
y="38"
|
||||
transform="translate(5, 2)">
|
||||
{{formatTime(deployment.time)}}
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<svg
|
||||
height="0"
|
||||
width="0">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="shadow-gradient">
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#000"
|
||||
stop-opacity="0.4">
|
||||
</stop>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#000"
|
||||
stop-opacity="0">
|
||||
</stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</g>
|
||||
</template>
|
|
@ -0,0 +1,104 @@
|
|||
<script>
|
||||
import {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
currentXCoordinate: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentYCoordinate: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentFlagPosition: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeightOffset: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
circleColorRgb: '#8fbce8',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
formatTime() {
|
||||
return timeFormat(this.currentData.time);
|
||||
},
|
||||
|
||||
formatDate() {
|
||||
return dateFormat(this.currentData.time);
|
||||
},
|
||||
|
||||
calculatedHeight() {
|
||||
return this.graphHeight - this.graphHeightOffset;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<g class="mouse-over-flag">
|
||||
<line
|
||||
class="selected-metric-line"
|
||||
:x1="currentXCoordinate"
|
||||
:y1="0"
|
||||
:x2="currentXCoordinate"
|
||||
:y2="calculatedHeight"
|
||||
transform="translate(-5, 20)">
|
||||
</line>
|
||||
<circle
|
||||
class="circle-metric"
|
||||
:fill="circleColorRgb"
|
||||
stroke="#000"
|
||||
:cx="currentXCoordinate"
|
||||
:cy="currentYCoordinate"
|
||||
r="5"
|
||||
transform="translate(-5, 20)">
|
||||
</circle>
|
||||
<svg
|
||||
class="rect-text-metric"
|
||||
:x="currentFlagPosition"
|
||||
y="0">
|
||||
<rect
|
||||
class="rect-metric"
|
||||
x="4"
|
||||
y="1"
|
||||
rx="2"
|
||||
width="90"
|
||||
height="40"
|
||||
transform="translate(-3, 20)">
|
||||
</rect>
|
||||
<text
|
||||
class="text-metric text-metric-bold"
|
||||
x="16"
|
||||
y="35"
|
||||
transform="translate(-5, 20)">
|
||||
{{formatTime}}
|
||||
</text>
|
||||
<text
|
||||
class="text-metric"
|
||||
x="16"
|
||||
y="15"
|
||||
transform="translate(-5, 20)">
|
||||
{{formatDate}}
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
</template>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue