Merge remote-tracking branch 'upstream/master' into qa-add-more-key-tests
* upstream/master: (536 commits) Fix flash errors in performance bar for cached responses Force content to align right Method to track recoverable exceptions in sentry Move PipelineFailed vue component fixed targetmode being included in project FE Docs: Fix header hierarchy in Vuex section of Vue.md Adds helpers to remove withespace and linebreaks `package-qa` was renamed to `package-and-qa` Fix container scanning in vendored GitLab CI configuration for Auto Devops Fixed web IDE not working for sub-groups Remove web ide beta flag from docs and update Make the message break into a new line instead of truncating it Backport Web IDE docs to gitlab-ce link to product handbook piece on permissions Update faraday_middlewar to 0.12.2 Double-check next value for internal ids. Resolve "skeleton placeholder on diff has white background" Replace the `project/commits/comments.feature` spinach test with an rspec analog Set ENV['IN_MEMORY_APPLICATION_SETTINGS'] to 'true in spec/db/production/settings_spec.rb Update docs on `.gitlab-ci.yml` and variables policy ...
This commit is contained in:
		
						commit
						8d4ad0c7db
					
				| 
						 | 
					@ -10,12 +10,6 @@ engines:
 | 
				
			||||||
      - javascript
 | 
					      - javascript
 | 
				
			||||||
    exclude_paths:
 | 
					    exclude_paths:
 | 
				
			||||||
      - "lib/api/v3/*"
 | 
					      - "lib/api/v3/*"
 | 
				
			||||||
  eslint:
 | 
					 | 
				
			||||||
    enabled: true
 | 
					 | 
				
			||||||
    channel: "eslint-4"
 | 
					 | 
				
			||||||
  rubocop:
 | 
					 | 
				
			||||||
    enabled: true
 | 
					 | 
				
			||||||
    channel: "gitlab-rubocop-0-52-1"
 | 
					 | 
				
			||||||
ratings:
 | 
					ratings:
 | 
				
			||||||
  paths:
 | 
					  paths:
 | 
				
			||||||
  - Gemfile.lock
 | 
					  - Gemfile.lock
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										173
									
								
								.gitlab-ci.yml
								
								
								
								
							
							
						
						
									
										173
									
								
								.gitlab-ci.yml
								
								
								
								
							| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
 | 
					image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.dedicated-runner: &dedicated-runner
 | 
					.dedicated-runner: &dedicated-runner
 | 
				
			||||||
  retry: 1
 | 
					  retry: 1
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git
 | 
				
			||||||
    - gitlab-org
 | 
					    - gitlab-org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.default-cache: &default-cache
 | 
					.default-cache: &default-cache
 | 
				
			||||||
  key: "ruby-2.3.6-with-yarn"
 | 
					  key: "ruby-2.3.7-with-yarn"
 | 
				
			||||||
  paths:
 | 
					  paths:
 | 
				
			||||||
    - vendor/ruby
 | 
					    - vendor/ruby
 | 
				
			||||||
    - .yarn-cache/
 | 
					    - .yarn-cache/
 | 
				
			||||||
| 
						 | 
					@ -78,6 +78,19 @@ stages:
 | 
				
			||||||
    - mysql:latest
 | 
					    - mysql:latest
 | 
				
			||||||
    - redis:alpine
 | 
					    - redis:alpine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rails5-variables: &rails5-variables
 | 
				
			||||||
 | 
					  script:
 | 
				
			||||||
 | 
					    - export RAILS5=${RAILS5}
 | 
				
			||||||
 | 
					    - export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rails5: &rails5
 | 
				
			||||||
 | 
					  allow_failure: true
 | 
				
			||||||
 | 
					  only:
 | 
				
			||||||
 | 
					    - /rails5/
 | 
				
			||||||
 | 
					  variables:
 | 
				
			||||||
 | 
					    BUNDLE_GEMFILE: "Gemfile.rails5"
 | 
				
			||||||
 | 
					    RAILS5: "true"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Skip all jobs except the ones that begin with 'docs/'.
 | 
					# Skip all jobs except the ones that begin with 'docs/'.
 | 
				
			||||||
# Used for commits including ONLY documentation changes.
 | 
					# Used for commits including ONLY documentation changes.
 | 
				
			||||||
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
 | 
					# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
 | 
				
			||||||
| 
						 | 
					@ -118,6 +131,7 @@ stages:
 | 
				
			||||||
  <<: *dedicated-runner
 | 
					  <<: *dedicated-runner
 | 
				
			||||||
  <<: *except-docs-and-qa
 | 
					  <<: *except-docs-and-qa
 | 
				
			||||||
  <<: *pull-cache
 | 
					  <<: *pull-cache
 | 
				
			||||||
 | 
					  <<: *rails5-variables
 | 
				
			||||||
  stage: test
 | 
					  stage: test
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - JOB_NAME=( $CI_JOB_NAME )
 | 
					    - JOB_NAME=( $CI_JOB_NAME )
 | 
				
			||||||
| 
						 | 
					@ -148,14 +162,23 @@ stages:
 | 
				
			||||||
  <<: *rspec-metadata
 | 
					  <<: *rspec-metadata
 | 
				
			||||||
  <<: *use-pg
 | 
					  <<: *use-pg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					  <<: *rspec-metadata-pg
 | 
				
			||||||
 | 
					  <<: *rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.rspec-metadata-mysql: &rspec-metadata-mysql
 | 
					.rspec-metadata-mysql: &rspec-metadata-mysql
 | 
				
			||||||
  <<: *rspec-metadata
 | 
					  <<: *rspec-metadata
 | 
				
			||||||
  <<: *use-mysql
 | 
					  <<: *use-mysql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					  <<: *rspec-metadata-mysql
 | 
				
			||||||
 | 
					  <<: *rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.spinach-metadata: &spinach-metadata
 | 
					.spinach-metadata: &spinach-metadata
 | 
				
			||||||
  <<: *dedicated-runner
 | 
					  <<: *dedicated-runner
 | 
				
			||||||
  <<: *except-docs-and-qa
 | 
					  <<: *except-docs-and-qa
 | 
				
			||||||
  <<: *pull-cache
 | 
					  <<: *pull-cache
 | 
				
			||||||
 | 
					  <<: *rails5-variables
 | 
				
			||||||
  stage: test
 | 
					  stage: test
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - JOB_NAME=( $CI_JOB_NAME )
 | 
					    - JOB_NAME=( $CI_JOB_NAME )
 | 
				
			||||||
| 
						 | 
					@ -179,10 +202,18 @@ stages:
 | 
				
			||||||
  <<: *spinach-metadata
 | 
					  <<: *spinach-metadata
 | 
				
			||||||
  <<: *use-pg
 | 
					  <<: *use-pg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
 | 
				
			||||||
 | 
					  <<: *spinach-metadata-pg
 | 
				
			||||||
 | 
					  <<: *rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.spinach-metadata-mysql: &spinach-metadata-mysql
 | 
					.spinach-metadata-mysql: &spinach-metadata-mysql
 | 
				
			||||||
  <<: *spinach-metadata
 | 
					  <<: *spinach-metadata
 | 
				
			||||||
  <<: *use-mysql
 | 
					  <<: *use-mysql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
 | 
				
			||||||
 | 
					  <<: *spinach-metadata-mysql
 | 
				
			||||||
 | 
					  <<: *rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.only-canonical-masters: &only-canonical-masters
 | 
					.only-canonical-masters: &only-canonical-masters
 | 
				
			||||||
  only:
 | 
					  only:
 | 
				
			||||||
    - master@gitlab-org/gitlab-ce
 | 
					    - master@gitlab-org/gitlab-ce
 | 
				
			||||||
| 
						 | 
					@ -266,12 +297,13 @@ package-and-qa:
 | 
				
			||||||
  when: manual
 | 
					  when: manual
 | 
				
			||||||
  variables:
 | 
					  variables:
 | 
				
			||||||
    GIT_STRATEGY: none
 | 
					    GIT_STRATEGY: none
 | 
				
			||||||
 | 
					  retry: 0
 | 
				
			||||||
  before_script:
 | 
					  before_script:
 | 
				
			||||||
    # We need to download the script rather than clone the repo since the
 | 
					    # We need to download the script rather than clone the repo since the
 | 
				
			||||||
    # package-and-qa job will not be able to run when the branch gets
 | 
					    # package-and-qa job will not be able to run when the branch gets
 | 
				
			||||||
    # deleted (when merging the MR).
 | 
					    # deleted (when merging the MR).
 | 
				
			||||||
    - apk add --update openssl
 | 
					    - apk add --update openssl
 | 
				
			||||||
    - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
 | 
					    - wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
 | 
				
			||||||
    - chmod 755 trigger-build-omnibus
 | 
					    - chmod 755 trigger-build-omnibus
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - ./trigger-build-omnibus
 | 
					    - ./trigger-build-omnibus
 | 
				
			||||||
| 
						 | 
					@ -332,10 +364,11 @@ update-tests-metadata:
 | 
				
			||||||
      - rspec_flaky/
 | 
					      - rspec_flaky/
 | 
				
			||||||
    policy: push
 | 
					    policy: push
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - retry gem install fog-aws mime-types
 | 
					    - retry gem install fog-aws mime-types activesupport
 | 
				
			||||||
    - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
 | 
					    - 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
 | 
					    - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
 | 
				
			||||||
    - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
 | 
					    - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
 | 
				
			||||||
 | 
					    - FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
 | 
				
			||||||
    - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
 | 
					    - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
 | 
				
			||||||
    - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
 | 
					    - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
 | 
				
			||||||
    - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
 | 
					    - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
 | 
				
			||||||
| 
						 | 
					@ -467,6 +500,70 @@ spinach-pg 1 2: *spinach-metadata-pg
 | 
				
			||||||
spinach-mysql 0 2: *spinach-metadata-mysql
 | 
					spinach-mysql 0 2: *spinach-metadata-mysql
 | 
				
			||||||
spinach-mysql 1 2: *spinach-metadata-mysql
 | 
					spinach-mysql 1 2: *spinach-metadata-mysql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5
 | 
				
			||||||
 | 
					spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5
 | 
				
			||||||
 | 
					spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static-analysis:
 | 
					static-analysis:
 | 
				
			||||||
  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
					  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
| 
						 | 
					@ -475,7 +572,7 @@ static-analysis:
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - scripts/static-analysis
 | 
					    - scripts/static-analysis
 | 
				
			||||||
  cache:
 | 
					  cache:
 | 
				
			||||||
    key: "ruby-2.3.6-with-yarn-and-rubocop"
 | 
					    key: "ruby-2.3.7-with-yarn-and-rubocop"
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - vendor/ruby
 | 
					      - vendor/ruby
 | 
				
			||||||
      - .yarn-cache/
 | 
					      - .yarn-cache/
 | 
				
			||||||
| 
						 | 
					@ -617,36 +714,72 @@ karma:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
codequality:
 | 
					codequality:
 | 
				
			||||||
  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
					  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
				
			||||||
  image: docker:latest
 | 
					  image: docker:stable
 | 
				
			||||||
 | 
					  allow_failure: true
 | 
				
			||||||
 | 
					  # gitlab-org runners set `privileged: false` but we need to have it set to true
 | 
				
			||||||
 | 
					  # since we're using Docker in Docker
 | 
				
			||||||
 | 
					  tags: []
 | 
				
			||||||
  before_script: []
 | 
					  before_script: []
 | 
				
			||||||
  services:
 | 
					  services:
 | 
				
			||||||
    - docker:dind
 | 
					    - docker:stable-dind
 | 
				
			||||||
  variables:
 | 
					  variables:
 | 
				
			||||||
    SETUP_DB: "false"
 | 
					    SETUP_DB: "false"
 | 
				
			||||||
    DOCKER_DRIVER: overlay2
 | 
					    DOCKER_DRIVER: overlay2
 | 
				
			||||||
    CODECLIMATE_FORMAT: json
 | 
					 | 
				
			||||||
  cache: {}
 | 
					  cache: {}
 | 
				
			||||||
  dependencies: []
 | 
					  dependencies: []
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - apk update && apk add jq
 | 
					    # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
 | 
				
			||||||
    - ./scripts/codequality analyze -f json > raw_codeclimate.json || true
 | 
					    - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
 | 
				
			||||||
    # The following line keeps only the fields used in the MR widget, reducing the JSON artifact size
 | 
					    - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
 | 
				
			||||||
    - jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json
 | 
					 | 
				
			||||||
  artifacts:
 | 
					  artifacts:
 | 
				
			||||||
    paths: [codeclimate.json]
 | 
					    paths: [codeclimate.json]
 | 
				
			||||||
    expire_in: 1 week
 | 
					    expire_in: 1 week
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sast:
 | 
					sast:
 | 
				
			||||||
  <<: *except-docs
 | 
					  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
				
			||||||
  image: registry.gitlab.com/gitlab-org/gl-sast:latest
 | 
					  image: docker:stable
 | 
				
			||||||
  variables:
 | 
					  variables:
 | 
				
			||||||
    CONFIDENCE_LEVEL: 2
 | 
					    SAST_CONFIDENCE_LEVEL: 2
 | 
				
			||||||
 | 
					    DOCKER_DRIVER: overlay2
 | 
				
			||||||
 | 
					  allow_failure: true
 | 
				
			||||||
 | 
					  tags: []
 | 
				
			||||||
  before_script: []
 | 
					  before_script: []
 | 
				
			||||||
 | 
					  cache: {}
 | 
				
			||||||
 | 
					  dependencies: []
 | 
				
			||||||
 | 
					  services:
 | 
				
			||||||
 | 
					    - docker:stable-dind
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - /app/bin/run .
 | 
					    - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
 | 
				
			||||||
 | 
					    - docker run
 | 
				
			||||||
 | 
					        --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
 | 
				
			||||||
 | 
					        --volume "$PWD:/code"
 | 
				
			||||||
 | 
					        --volume /var/run/docker.sock:/var/run/docker.sock
 | 
				
			||||||
 | 
					        "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
 | 
				
			||||||
  artifacts:
 | 
					  artifacts:
 | 
				
			||||||
    paths: [gl-sast-report.json]
 | 
					    paths: [gl-sast-report.json]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dependency_scanning:
 | 
				
			||||||
 | 
					  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
				
			||||||
 | 
					  image: docker:stable
 | 
				
			||||||
 | 
					  variables:
 | 
				
			||||||
 | 
					    DOCKER_DRIVER: overlay2
 | 
				
			||||||
 | 
					  allow_failure: true
 | 
				
			||||||
 | 
					  tags: []
 | 
				
			||||||
 | 
					  before_script: []
 | 
				
			||||||
 | 
					  cache: {}
 | 
				
			||||||
 | 
					  dependencies: []
 | 
				
			||||||
 | 
					  services:
 | 
				
			||||||
 | 
					    - docker:stable-dind
 | 
				
			||||||
 | 
					  script:
 | 
				
			||||||
 | 
					    - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
 | 
				
			||||||
 | 
					    - docker run
 | 
				
			||||||
 | 
					        --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
 | 
				
			||||||
 | 
					        --volume "$PWD:/code"
 | 
				
			||||||
 | 
					        --volume /var/run/docker.sock:/var/run/docker.sock
 | 
				
			||||||
 | 
					        "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
 | 
				
			||||||
 | 
					  artifacts:
 | 
				
			||||||
 | 
					    paths: [gl-dependency-scanning-report.json]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
qa:internal:
 | 
					qa:internal:
 | 
				
			||||||
  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
					  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
				
			||||||
  services: []
 | 
					  services: []
 | 
				
			||||||
| 
						 | 
					@ -664,7 +797,13 @@ qa:selectors:
 | 
				
			||||||
    - bundle exec bin/qa Test::Sanity::Selectors
 | 
					    - bundle exec bin/qa Test::Sanity::Selectors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
coverage:
 | 
					coverage:
 | 
				
			||||||
  <<: *dedicated-no-docs-no-db-pull-cache-job
 | 
					  # Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to
 | 
				
			||||||
 | 
					  # download artifacts from all the rspec jobs instead of from setup-test-env only
 | 
				
			||||||
 | 
					  <<: *dedicated-runner
 | 
				
			||||||
 | 
					  <<: *except-docs-and-qa
 | 
				
			||||||
 | 
					  <<: *pull-cache
 | 
				
			||||||
 | 
					  variables:
 | 
				
			||||||
 | 
					    SETUP_DB: "false"
 | 
				
			||||||
  stage: post-test
 | 
					  stage: post-test
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    - bundle exec scripts/merge-simplecov
 | 
					    - bundle exec scripts/merge-simplecov
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,13 +33,16 @@ When removing columns, tables, indexes or other structures:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## General Checklist
 | 
					## General Checklist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary
 | 
					- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
 | 
				
			||||||
- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
 | 
					- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
 | 
				
			||||||
- [ ] API support added
 | 
					- [ ] API support added
 | 
				
			||||||
- [ ] Tests added for this feature/bug
 | 
					- [ ] Tests added for this feature/bug
 | 
				
			||||||
- Review
 | 
					- Review
 | 
				
			||||||
  - [ ] Has been reviewed by Backend
 | 
					  - [ ] Has been reviewed by Backend
 | 
				
			||||||
  - [ ] Has been reviewed by Database
 | 
					  - [ ] Has been reviewed by Database
 | 
				
			||||||
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
 | 
					- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
 | 
				
			||||||
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
 | 
					- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
 | 
				
			||||||
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
 | 
					- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
 | 
				
			||||||
 | 
					- [ ] Internationalization required/considered
 | 
				
			||||||
 | 
					- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
 | 
				
			||||||
 | 
					- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
2.3.6
 | 
					2.3.7
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,6 +59,8 @@ linters:
 | 
				
			||||||
  # Reports when you define the same property twice in a single rule set.
 | 
					  # Reports when you define the same property twice in a single rule set.
 | 
				
			||||||
  DuplicateProperty:
 | 
					  DuplicateProperty:
 | 
				
			||||||
    enabled: true
 | 
					    enabled: true
 | 
				
			||||||
 | 
					    ignore_consecutive:
 | 
				
			||||||
 | 
					      - cursor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Separate rule, function, and mixin declarations with empty lines.
 | 
					  # Separate rule, function, and mixin declarations with empty lines.
 | 
				
			||||||
  EmptyLineBetweenBlocks:
 | 
					  EmptyLineBetweenBlocks:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										43
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										43
									
								
								CHANGELOG.md
								
								
								
								
							| 
						 | 
					@ -2,6 +2,32 @@
 | 
				
			||||||
documentation](doc/development/changelog.md) for instructions on adding your own
 | 
					documentation](doc/development/changelog.md) for instructions on adding your own
 | 
				
			||||||
entry.
 | 
					entry.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 10.6.4 (2018-04-09)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed (8 changes, 1 of them is from the community)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Correct copy text for the promote milestone and label modals. !17726
 | 
				
			||||||
 | 
					- Avoid validation errors when running the Pages domain verification service. !17992
 | 
				
			||||||
 | 
					- Fix autolinking URLs containing ampersands. !18045
 | 
				
			||||||
 | 
					- Fix exceptions raised when migrating pipeline stages in the background. !18076
 | 
				
			||||||
 | 
					- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
 | 
				
			||||||
 | 
					- Don't show Jump to Discussion button on Issues.
 | 
				
			||||||
 | 
					- Fix listing commit branch/tags that contain special characters.
 | 
				
			||||||
 | 
					- Fix 404 in group boards when moving issue between lists.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Performance (1 change)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Free open file descriptors and libgit2 buffers in UpdatePagesService.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 10.6.3 (2018-04-03)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Security (2 changes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fix XSS on diff view stored on filenames.
 | 
				
			||||||
 | 
					- Adds confidential notes channel for Slack/Mattermost.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 10.6.2 (2018-03-29)
 | 
					## 10.6.2 (2018-03-29)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Fixed (2 changes, 1 of them is from the community)
 | 
					### Fixed (2 changes, 1 of them is from the community)
 | 
				
			||||||
| 
						 | 
					@ -191,7 +217,6 @@ entry.
 | 
				
			||||||
- Enable privileged mode for GitLab Runner. !17528
 | 
					- Enable privileged mode for GitLab Runner. !17528
 | 
				
			||||||
- Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
 | 
					- Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
 | 
				
			||||||
- Upgrade GitLab Workhorse to 4.0.0.
 | 
					- Upgrade GitLab Workhorse to 4.0.0.
 | 
				
			||||||
- Allow CI/CD Jobs being grouped on version strings.
 | 
					 | 
				
			||||||
- Add discussions API for Issues and Snippets.
 | 
					- Add discussions API for Issues and Snippets.
 | 
				
			||||||
- Add one group board to Libre.
 | 
					- Add one group board to Libre.
 | 
				
			||||||
- Add support for filtering by source and target branch to merge requests API.
 | 
					- Add support for filtering by source and target branch to merge requests API.
 | 
				
			||||||
| 
						 | 
					@ -218,6 +243,14 @@ entry.
 | 
				
			||||||
- Use host URL to build JIRA remote link icon.
 | 
					- Use host URL to build JIRA remote link icon.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 10.5.7 (2018-04-03)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Security (2 changes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fix XSS on diff view stored on filenames.
 | 
				
			||||||
 | 
					- Adds confidential notes channel for Slack/Mattermost.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 10.5.6 (2018-03-16)
 | 
					## 10.5.6 (2018-03-16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Security (2 changes)
 | 
					### Security (2 changes)
 | 
				
			||||||
| 
						 | 
					@ -485,6 +518,14 @@ entry.
 | 
				
			||||||
- Adds empty state illustration for pending job.
 | 
					- Adds empty state illustration for pending job.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 10.4.7 (2018-04-03)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Security (2 changes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fix XSS on diff view stored on filenames.
 | 
				
			||||||
 | 
					- Adds confidential notes channel for Slack/Mattermost.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 10.4.6 (2018-03-16)
 | 
					## 10.4.6 (2018-03-16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Security (2 changes)
 | 
					### Security (2 changes)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
0.92.0
 | 
					0.95.0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
0.7.1
 | 
					0.8.0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
7.1.1
 | 
					7.1.2
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
4.0.0
 | 
					4.1.0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										16
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -82,16 +82,9 @@ gem 'net-ldap'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Git Wiki
 | 
					# Git Wiki
 | 
				
			||||||
# Required manually in config/initializers/gollum.rb to control load order
 | 
					# Required manually in config/initializers/gollum.rb to control load order
 | 
				
			||||||
# Before updating this gem, check if
 | 
					gem 'gitlab-gollum-lib', '~> 4.2'
 | 
				
			||||||
# https://github.com/gollum/gollum-lib/pull/292 has been merged.
 | 
					 | 
				
			||||||
# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer
 | 
					 | 
				
			||||||
# in config/initializers/gollum.rb
 | 
					 | 
				
			||||||
gem 'gollum-lib', '~> 4.2', require: false
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Before updating this gem, check if
 | 
					gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
 | 
				
			||||||
# https://github.com/gollum/rugged_adapter/pull/28 has been merged.
 | 
					 | 
				
			||||||
# If it has, then remove the monkey patch for tree_entry in config/initializers/gollum.rb
 | 
					 | 
				
			||||||
gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Language detection
 | 
					# Language detection
 | 
				
			||||||
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
 | 
					gem 'github-linguist', '~> 5.3.3', require: 'linguist'
 | 
				
			||||||
| 
						 | 
					@ -384,6 +377,7 @@ group :test do
 | 
				
			||||||
  gem 'email_spec', '~> 1.6.0'
 | 
					  gem 'email_spec', '~> 1.6.0'
 | 
				
			||||||
  gem 'json-schema', '~> 2.8.0'
 | 
					  gem 'json-schema', '~> 2.8.0'
 | 
				
			||||||
  gem 'webmock', '~> 2.3.2'
 | 
					  gem 'webmock', '~> 2.3.2'
 | 
				
			||||||
 | 
					  gem 'rails-controller-testing' if rails5? # Rails5 only gem.
 | 
				
			||||||
  gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
 | 
					  gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
 | 
				
			||||||
  gem 'sham_rack', '~> 1.3.6'
 | 
					  gem 'sham_rack', '~> 1.3.6'
 | 
				
			||||||
  gem 'concurrent-ruby', '~> 1.0.5'
 | 
					  gem 'concurrent-ruby', '~> 1.0.5'
 | 
				
			||||||
| 
						 | 
					@ -421,7 +415,7 @@ group :ed25519 do
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Gitaly GRPC client
 | 
					# Gitaly GRPC client
 | 
				
			||||||
gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
 | 
					gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
 | 
				
			||||||
gem 'grpc', '~> 1.10.0'
 | 
					gem 'grpc', '~> 1.10.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
 | 
					# Locked until https://github.com/google/protobuf/issues/4210 is closed
 | 
				
			||||||
| 
						 | 
					@ -440,3 +434,5 @@ gem 'grape_logging', '~> 1.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Asset synchronization
 | 
					# Asset synchronization
 | 
				
			||||||
gem 'asset_sync', '~> 2.2.0'
 | 
					gem 'asset_sync', '~> 2.2.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gem 'goldiloader', '~> 2.0'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										42
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										42
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
					@ -206,7 +206,7 @@ GEM
 | 
				
			||||||
      railties (>= 3.0.0)
 | 
					      railties (>= 3.0.0)
 | 
				
			||||||
    faraday (0.12.2)
 | 
					    faraday (0.12.2)
 | 
				
			||||||
      multipart-post (>= 1.2, < 3)
 | 
					      multipart-post (>= 1.2, < 3)
 | 
				
			||||||
    faraday_middleware (0.11.0.1)
 | 
					    faraday_middleware (0.12.2)
 | 
				
			||||||
      faraday (>= 0.7.4, < 1.0)
 | 
					      faraday (>= 0.7.4, < 1.0)
 | 
				
			||||||
    faraday_middleware-multi_json (0.0.6)
 | 
					    faraday_middleware-multi_json (0.0.6)
 | 
				
			||||||
      faraday_middleware
 | 
					      faraday_middleware
 | 
				
			||||||
| 
						 | 
					@ -290,7 +290,7 @@ GEM
 | 
				
			||||||
      po_to_json (>= 1.0.0)
 | 
					      po_to_json (>= 1.0.0)
 | 
				
			||||||
      rails (>= 3.2.0)
 | 
					      rails (>= 3.2.0)
 | 
				
			||||||
    gherkin-ruby (0.3.2)
 | 
					    gherkin-ruby (0.3.2)
 | 
				
			||||||
    gitaly-proto (0.91.0)
 | 
					    gitaly-proto (0.94.0)
 | 
				
			||||||
      google-protobuf (~> 3.1)
 | 
					      google-protobuf (~> 3.1)
 | 
				
			||||||
      grpc (~> 1.0)
 | 
					      grpc (~> 1.0)
 | 
				
			||||||
    github-linguist (5.3.3)
 | 
					    github-linguist (5.3.3)
 | 
				
			||||||
| 
						 | 
					@ -298,11 +298,22 @@ GEM
 | 
				
			||||||
      escape_utils (~> 1.1.0)
 | 
					      escape_utils (~> 1.1.0)
 | 
				
			||||||
      mime-types (>= 1.19)
 | 
					      mime-types (>= 1.19)
 | 
				
			||||||
      rugged (>= 0.25.1)
 | 
					      rugged (>= 0.25.1)
 | 
				
			||||||
    github-markup (1.6.1)
 | 
					    github-markup (1.7.0)
 | 
				
			||||||
    gitlab-flowdock-git-hook (1.0.1)
 | 
					    gitlab-flowdock-git-hook (1.0.1)
 | 
				
			||||||
      flowdock (~> 0.7)
 | 
					      flowdock (~> 0.7)
 | 
				
			||||||
      gitlab-grit (>= 2.4.1)
 | 
					      gitlab-grit (>= 2.4.1)
 | 
				
			||||||
      multi_json
 | 
					      multi_json
 | 
				
			||||||
 | 
					    gitlab-gollum-lib (4.2.7.1)
 | 
				
			||||||
 | 
					      gemojione (~> 3.2)
 | 
				
			||||||
 | 
					      github-markup (~> 1.6)
 | 
				
			||||||
 | 
					      gollum-grit_adapter (~> 1.0)
 | 
				
			||||||
 | 
					      nokogiri (>= 1.6.1, < 2.0)
 | 
				
			||||||
 | 
					      rouge (~> 2.1)
 | 
				
			||||||
 | 
					      sanitize (~> 2.1)
 | 
				
			||||||
 | 
					      stringex (~> 2.6)
 | 
				
			||||||
 | 
					    gitlab-gollum-rugged_adapter (0.4.4)
 | 
				
			||||||
 | 
					      mime-types (>= 1.15)
 | 
				
			||||||
 | 
					      rugged (~> 0.25)
 | 
				
			||||||
    gitlab-grit (2.8.2)
 | 
					    gitlab-grit (2.8.2)
 | 
				
			||||||
      charlock_holmes (~> 0.6)
 | 
					      charlock_holmes (~> 0.6)
 | 
				
			||||||
      diff-lcs (~> 1.1)
 | 
					      diff-lcs (~> 1.1)
 | 
				
			||||||
| 
						 | 
					@ -320,19 +331,11 @@ GEM
 | 
				
			||||||
      rubyntlm (~> 0.5)
 | 
					      rubyntlm (~> 0.5)
 | 
				
			||||||
    globalid (0.4.1)
 | 
					    globalid (0.4.1)
 | 
				
			||||||
      activesupport (>= 4.2.0)
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
 | 
					    goldiloader (2.0.1)
 | 
				
			||||||
 | 
					      activerecord (>= 4.2, < 5.2)
 | 
				
			||||||
 | 
					      activesupport (>= 4.2, < 5.2)
 | 
				
			||||||
    gollum-grit_adapter (1.0.1)
 | 
					    gollum-grit_adapter (1.0.1)
 | 
				
			||||||
      gitlab-grit (~> 2.7, >= 2.7.1)
 | 
					      gitlab-grit (~> 2.7, >= 2.7.1)
 | 
				
			||||||
    gollum-lib (4.2.7)
 | 
					 | 
				
			||||||
      gemojione (~> 3.2)
 | 
					 | 
				
			||||||
      github-markup (~> 1.6)
 | 
					 | 
				
			||||||
      gollum-grit_adapter (~> 1.0)
 | 
					 | 
				
			||||||
      nokogiri (>= 1.6.1, < 2.0)
 | 
					 | 
				
			||||||
      rouge (~> 2.1)
 | 
					 | 
				
			||||||
      sanitize (~> 2.1)
 | 
					 | 
				
			||||||
      stringex (~> 2.6)
 | 
					 | 
				
			||||||
    gollum-rugged_adapter (0.4.4)
 | 
					 | 
				
			||||||
      mime-types (>= 1.15)
 | 
					 | 
				
			||||||
      rugged (~> 0.25)
 | 
					 | 
				
			||||||
    gon (6.1.0)
 | 
					    gon (6.1.0)
 | 
				
			||||||
      actionpack (>= 3.0)
 | 
					      actionpack (>= 3.0)
 | 
				
			||||||
      json
 | 
					      json
 | 
				
			||||||
| 
						 | 
					@ -587,7 +590,7 @@ GEM
 | 
				
			||||||
    orm_adapter (0.5.0)
 | 
					    orm_adapter (0.5.0)
 | 
				
			||||||
    os (0.9.6)
 | 
					    os (0.9.6)
 | 
				
			||||||
    parallel (1.12.1)
 | 
					    parallel (1.12.1)
 | 
				
			||||||
    parser (2.5.0.3)
 | 
					    parser (2.5.1.0)
 | 
				
			||||||
      ast (~> 2.4.0)
 | 
					      ast (~> 2.4.0)
 | 
				
			||||||
    parslet (1.5.0)
 | 
					    parslet (1.5.0)
 | 
				
			||||||
      blankslate (~> 2.0)
 | 
					      blankslate (~> 2.0)
 | 
				
			||||||
| 
						 | 
					@ -907,7 +910,7 @@ GEM
 | 
				
			||||||
    state_machines-activerecord (0.5.1)
 | 
					    state_machines-activerecord (0.5.1)
 | 
				
			||||||
      activerecord (>= 4.1, < 6.0)
 | 
					      activerecord (>= 4.1, < 6.0)
 | 
				
			||||||
      state_machines-activemodel (>= 0.5.0)
 | 
					      state_machines-activemodel (>= 0.5.0)
 | 
				
			||||||
    stringex (2.7.1)
 | 
					    stringex (2.8.4)
 | 
				
			||||||
    sys-filesystem (1.1.6)
 | 
					    sys-filesystem (1.1.6)
 | 
				
			||||||
      ffi
 | 
					      ffi
 | 
				
			||||||
    sysexits (1.2.0)
 | 
					    sysexits (1.2.0)
 | 
				
			||||||
| 
						 | 
					@ -1061,14 +1064,15 @@ DEPENDENCIES
 | 
				
			||||||
  gettext (~> 3.2.2)
 | 
					  gettext (~> 3.2.2)
 | 
				
			||||||
  gettext_i18n_rails (~> 1.8.0)
 | 
					  gettext_i18n_rails (~> 1.8.0)
 | 
				
			||||||
  gettext_i18n_rails_js (~> 1.3)
 | 
					  gettext_i18n_rails_js (~> 1.3)
 | 
				
			||||||
  gitaly-proto (~> 0.91.0)
 | 
					  gitaly-proto (~> 0.94.0)
 | 
				
			||||||
  github-linguist (~> 5.3.3)
 | 
					  github-linguist (~> 5.3.3)
 | 
				
			||||||
  gitlab-flowdock-git-hook (~> 1.0.1)
 | 
					  gitlab-flowdock-git-hook (~> 1.0.1)
 | 
				
			||||||
 | 
					  gitlab-gollum-lib (~> 4.2)
 | 
				
			||||||
 | 
					  gitlab-gollum-rugged_adapter (~> 0.4.4)
 | 
				
			||||||
  gitlab-markup (~> 1.6.2)
 | 
					  gitlab-markup (~> 1.6.2)
 | 
				
			||||||
  gitlab-styles (~> 2.3)
 | 
					  gitlab-styles (~> 2.3)
 | 
				
			||||||
  gitlab_omniauth-ldap (~> 2.0.4)
 | 
					  gitlab_omniauth-ldap (~> 2.0.4)
 | 
				
			||||||
  gollum-lib (~> 4.2)
 | 
					  goldiloader (~> 2.0)
 | 
				
			||||||
  gollum-rugged_adapter (~> 0.4.4)
 | 
					 | 
				
			||||||
  gon (~> 6.1.0)
 | 
					  gon (~> 6.1.0)
 | 
				
			||||||
  google-api-client (~> 0.19.8)
 | 
					  google-api-client (~> 0.19.8)
 | 
				
			||||||
  google-protobuf (= 3.5.1)
 | 
					  google-protobuf (= 3.5.1)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,7 @@ GEM
 | 
				
			||||||
      autoprefixer-rails (>= 5.2.1)
 | 
					      autoprefixer-rails (>= 5.2.1)
 | 
				
			||||||
      sass (>= 3.3.4)
 | 
					      sass (>= 3.3.4)
 | 
				
			||||||
    bootstrap_form (2.7.0)
 | 
					    bootstrap_form (2.7.0)
 | 
				
			||||||
    brakeman (3.6.2)
 | 
					    brakeman (4.2.1)
 | 
				
			||||||
    browser (2.5.3)
 | 
					    browser (2.5.3)
 | 
				
			||||||
    builder (3.2.3)
 | 
					    builder (3.2.3)
 | 
				
			||||||
    bullet (5.5.1)
 | 
					    bullet (5.5.1)
 | 
				
			||||||
| 
						 | 
					@ -291,7 +291,7 @@ GEM
 | 
				
			||||||
      po_to_json (>= 1.0.0)
 | 
					      po_to_json (>= 1.0.0)
 | 
				
			||||||
      rails (>= 3.2.0)
 | 
					      rails (>= 3.2.0)
 | 
				
			||||||
    gherkin-ruby (0.3.2)
 | 
					    gherkin-ruby (0.3.2)
 | 
				
			||||||
    gitaly-proto (0.91.0)
 | 
					    gitaly-proto (0.94.0)
 | 
				
			||||||
      google-protobuf (~> 3.1)
 | 
					      google-protobuf (~> 3.1)
 | 
				
			||||||
      grpc (~> 1.0)
 | 
					      grpc (~> 1.0)
 | 
				
			||||||
    github-linguist (5.3.3)
 | 
					    github-linguist (5.3.3)
 | 
				
			||||||
| 
						 | 
					@ -321,6 +321,9 @@ GEM
 | 
				
			||||||
      rubyntlm (~> 0.5)
 | 
					      rubyntlm (~> 0.5)
 | 
				
			||||||
    globalid (0.4.1)
 | 
					    globalid (0.4.1)
 | 
				
			||||||
      activesupport (>= 4.2.0)
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
 | 
					    goldiloader (2.0.1)
 | 
				
			||||||
 | 
					      activerecord (>= 4.2, < 5.2)
 | 
				
			||||||
 | 
					      activesupport (>= 4.2, < 5.2)
 | 
				
			||||||
    gollum-grit_adapter (1.0.1)
 | 
					    gollum-grit_adapter (1.0.1)
 | 
				
			||||||
      gitlab-grit (~> 2.7, >= 2.7.1)
 | 
					      gitlab-grit (~> 2.7, >= 2.7.1)
 | 
				
			||||||
    gollum-lib (4.2.7)
 | 
					    gollum-lib (4.2.7)
 | 
				
			||||||
| 
						 | 
					@ -400,7 +403,7 @@ GEM
 | 
				
			||||||
    hipchat (1.5.4)
 | 
					    hipchat (1.5.4)
 | 
				
			||||||
      httparty
 | 
					      httparty
 | 
				
			||||||
      mimemagic
 | 
					      mimemagic
 | 
				
			||||||
    html-pipeline (2.6.0)
 | 
					    html-pipeline (2.7.1)
 | 
				
			||||||
      activesupport (>= 2)
 | 
					      activesupport (>= 2)
 | 
				
			||||||
      nokogiri (>= 1.4)
 | 
					      nokogiri (>= 1.4)
 | 
				
			||||||
    html2text (0.2.1)
 | 
					    html2text (0.2.1)
 | 
				
			||||||
| 
						 | 
					@ -587,7 +590,7 @@ GEM
 | 
				
			||||||
    orm_adapter (0.5.0)
 | 
					    orm_adapter (0.5.0)
 | 
				
			||||||
    os (0.9.6)
 | 
					    os (0.9.6)
 | 
				
			||||||
    parallel (1.12.1)
 | 
					    parallel (1.12.1)
 | 
				
			||||||
    parser (2.5.0.4)
 | 
					    parser (2.5.0.5)
 | 
				
			||||||
      ast (~> 2.4.0)
 | 
					      ast (~> 2.4.0)
 | 
				
			||||||
    parslet (1.5.0)
 | 
					    parslet (1.5.0)
 | 
				
			||||||
      blankslate (~> 2.0)
 | 
					      blankslate (~> 2.0)
 | 
				
			||||||
| 
						 | 
					@ -678,6 +681,10 @@ GEM
 | 
				
			||||||
      bundler (>= 1.3.0)
 | 
					      bundler (>= 1.3.0)
 | 
				
			||||||
      railties (= 5.0.6)
 | 
					      railties (= 5.0.6)
 | 
				
			||||||
      sprockets-rails (>= 2.0.0)
 | 
					      sprockets-rails (>= 2.0.0)
 | 
				
			||||||
 | 
					    rails-controller-testing (1.0.2)
 | 
				
			||||||
 | 
					      actionpack (~> 5.x, >= 5.0.1)
 | 
				
			||||||
 | 
					      actionview (~> 5.x, >= 5.0.1)
 | 
				
			||||||
 | 
					      activesupport (~> 5.x)
 | 
				
			||||||
    rails-deprecated_sanitizer (1.0.3)
 | 
					    rails-deprecated_sanitizer (1.0.3)
 | 
				
			||||||
      activesupport (>= 4.2.0.alpha)
 | 
					      activesupport (>= 4.2.0.alpha)
 | 
				
			||||||
    rails-dom-testing (2.0.3)
 | 
					    rails-dom-testing (2.0.3)
 | 
				
			||||||
| 
						 | 
					@ -874,7 +881,7 @@ GEM
 | 
				
			||||||
      simplecov-html (~> 0.10.0)
 | 
					      simplecov-html (~> 0.10.0)
 | 
				
			||||||
    simplecov-html (0.10.2)
 | 
					    simplecov-html (0.10.2)
 | 
				
			||||||
    slack-notifier (1.5.1)
 | 
					    slack-notifier (1.5.1)
 | 
				
			||||||
    spinach (0.10.1)
 | 
					    spinach (0.8.10)
 | 
				
			||||||
      colorize
 | 
					      colorize
 | 
				
			||||||
      gherkin-ruby (>= 0.3.2)
 | 
					      gherkin-ruby (>= 0.3.2)
 | 
				
			||||||
      json
 | 
					      json
 | 
				
			||||||
| 
						 | 
					@ -1013,7 +1020,7 @@ DEPENDENCIES
 | 
				
			||||||
  binding_of_caller (~> 0.7.2)
 | 
					  binding_of_caller (~> 0.7.2)
 | 
				
			||||||
  bootstrap-sass (~> 3.3.0)
 | 
					  bootstrap-sass (~> 3.3.0)
 | 
				
			||||||
  bootstrap_form (~> 2.7.0)
 | 
					  bootstrap_form (~> 2.7.0)
 | 
				
			||||||
  brakeman (~> 3.6.0)
 | 
					  brakeman (~> 4.2)
 | 
				
			||||||
  browser (~> 2.2)
 | 
					  browser (~> 2.2)
 | 
				
			||||||
  bullet (~> 5.5.0)
 | 
					  bullet (~> 5.5.0)
 | 
				
			||||||
  bundler-audit (~> 0.5.0)
 | 
					  bundler-audit (~> 0.5.0)
 | 
				
			||||||
| 
						 | 
					@ -1062,12 +1069,13 @@ DEPENDENCIES
 | 
				
			||||||
  gettext (~> 3.2.2)
 | 
					  gettext (~> 3.2.2)
 | 
				
			||||||
  gettext_i18n_rails (~> 1.8.0)
 | 
					  gettext_i18n_rails (~> 1.8.0)
 | 
				
			||||||
  gettext_i18n_rails_js (~> 1.3)
 | 
					  gettext_i18n_rails_js (~> 1.3)
 | 
				
			||||||
  gitaly-proto (~> 0.91.0)
 | 
					  gitaly-proto (~> 0.94.0)
 | 
				
			||||||
  github-linguist (~> 5.3.3)
 | 
					  github-linguist (~> 5.3.3)
 | 
				
			||||||
  gitlab-flowdock-git-hook (~> 1.0.1)
 | 
					  gitlab-flowdock-git-hook (~> 1.0.1)
 | 
				
			||||||
  gitlab-markup (~> 1.6.2)
 | 
					  gitlab-markup (~> 1.6.2)
 | 
				
			||||||
  gitlab-styles (~> 2.3)
 | 
					  gitlab-styles (~> 2.3)
 | 
				
			||||||
  gitlab_omniauth-ldap (~> 2.0.4)
 | 
					  gitlab_omniauth-ldap (~> 2.0.4)
 | 
				
			||||||
 | 
					  goldiloader (~> 2.0)
 | 
				
			||||||
  gollum-lib (~> 4.2)
 | 
					  gollum-lib (~> 4.2)
 | 
				
			||||||
  gollum-rugged_adapter (~> 0.4.4)
 | 
					  gollum-rugged_adapter (~> 0.4.4)
 | 
				
			||||||
  gon (~> 6.1.0)
 | 
					  gon (~> 6.1.0)
 | 
				
			||||||
| 
						 | 
					@ -1084,7 +1092,7 @@ DEPENDENCIES
 | 
				
			||||||
  hashie-forbidden_attributes
 | 
					  hashie-forbidden_attributes
 | 
				
			||||||
  health_check (~> 2.6.0)
 | 
					  health_check (~> 2.6.0)
 | 
				
			||||||
  hipchat (~> 1.5.0)
 | 
					  hipchat (~> 1.5.0)
 | 
				
			||||||
  html-pipeline (~> 2.6.0)
 | 
					  html-pipeline (~> 2.7.1)
 | 
				
			||||||
  html2text
 | 
					  html2text
 | 
				
			||||||
  httparty (~> 0.13.3)
 | 
					  httparty (~> 0.13.3)
 | 
				
			||||||
  influxdb (~> 0.2)
 | 
					  influxdb (~> 0.2)
 | 
				
			||||||
| 
						 | 
					@ -1145,6 +1153,7 @@ DEPENDENCIES
 | 
				
			||||||
  rack-oauth2 (~> 1.2.1)
 | 
					  rack-oauth2 (~> 1.2.1)
 | 
				
			||||||
  rack-proxy (~> 0.6.0)
 | 
					  rack-proxy (~> 0.6.0)
 | 
				
			||||||
  rails (= 5.0.6)
 | 
					  rails (= 5.0.6)
 | 
				
			||||||
 | 
					  rails-controller-testing
 | 
				
			||||||
  rails-deprecated_sanitizer (~> 1.0.3)
 | 
					  rails-deprecated_sanitizer (~> 1.0.3)
 | 
				
			||||||
  rails-i18n (~> 5.1)
 | 
					  rails-i18n (~> 5.1)
 | 
				
			||||||
  rainbow (~> 2.2)
 | 
					  rainbow (~> 2.2)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1018 B  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 494 B  | 
| 
						 | 
					@ -4,7 +4,8 @@ import $ from 'jquery';
 | 
				
			||||||
import _ from 'underscore';
 | 
					import _ from 'underscore';
 | 
				
			||||||
import Cookies from 'js-cookie';
 | 
					import Cookies from 'js-cookie';
 | 
				
			||||||
import { __ } from './locale';
 | 
					import { __ } from './locale';
 | 
				
			||||||
import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
 | 
					import { updateTooltipTitle } from './lib/utils/common_utils';
 | 
				
			||||||
 | 
					import { isInVueNoteablePage } from './lib/utils/dom_utils';
 | 
				
			||||||
import flash from './flash';
 | 
					import flash from './flash';
 | 
				
			||||||
import axios from './lib/utils/axios_utils';
 | 
					import axios from './lib/utils/axios_utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -243,7 +244,7 @@ class AwardsHandler {
 | 
				
			||||||
  addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
 | 
					  addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
 | 
				
			||||||
    const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
 | 
					    const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
 | 
					    if (isInVueNoteablePage() && !isMainAwardsBlock) {
 | 
				
			||||||
      const id = votesBlock.attr('id').replace('note_', '');
 | 
					      const id = votesBlock.attr('id').replace('note_', '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.hideMenuElement($('.emoji-menu'));
 | 
					      this.hideMenuElement($('.emoji-menu'));
 | 
				
			||||||
| 
						 | 
					@ -295,16 +296,8 @@ class AwardsHandler {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isVueMRDiscussions() {
 | 
					 | 
				
			||||||
    return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  isInVueNoteablePage() {
 | 
					 | 
				
			||||||
    return isInIssuePage() || this.isVueMRDiscussions();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getVotesBlock() {
 | 
					  getVotesBlock() {
 | 
				
			||||||
    if (this.isInVueNoteablePage()) {
 | 
					    if (isInVueNoteablePage()) {
 | 
				
			||||||
      const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
 | 
					      const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if ($el.length) {
 | 
					      if ($el.length) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,121 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import Icon from '~/vue_shared/components/icon.vue';
 | 
				
			||||||
 | 
					import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
 | 
				
			||||||
 | 
					import Tooltip from '~/vue_shared/directives/tooltip';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'Badge',
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    Icon,
 | 
				
			||||||
 | 
					    LoadingIcon,
 | 
				
			||||||
 | 
					    Tooltip,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  directives: {
 | 
				
			||||||
 | 
					    Tooltip,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    imageUrl: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    linkUrl: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      hasError: false,
 | 
				
			||||||
 | 
					      isLoading: true,
 | 
				
			||||||
 | 
					      numRetries: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    imageUrlWithRetries() {
 | 
				
			||||||
 | 
					      if (this.numRetries === 0) {
 | 
				
			||||||
 | 
					        return this.imageUrl;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return `${this.imageUrl}#retries=${this.numRetries}`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    imageUrl() {
 | 
				
			||||||
 | 
					      this.hasError = false;
 | 
				
			||||||
 | 
					      this.isLoading = true;
 | 
				
			||||||
 | 
					      this.numRetries = 0;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    onError() {
 | 
				
			||||||
 | 
					      this.isLoading = false;
 | 
				
			||||||
 | 
					      this.hasError = true;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onLoad() {
 | 
				
			||||||
 | 
					      this.isLoading = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    reloadImage() {
 | 
				
			||||||
 | 
					      this.hasError = false;
 | 
				
			||||||
 | 
					      this.isLoading = true;
 | 
				
			||||||
 | 
					      this.numRetries += 1;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					      v-show="!isLoading && !hasError"
 | 
				
			||||||
 | 
					      :href="linkUrl"
 | 
				
			||||||
 | 
					      target="_blank"
 | 
				
			||||||
 | 
					      rel="noopener noreferrer"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <img
 | 
				
			||||||
 | 
					        class="project-badge"
 | 
				
			||||||
 | 
					        :src="imageUrlWithRetries"
 | 
				
			||||||
 | 
					        @load="onLoad"
 | 
				
			||||||
 | 
					        @error="onError"
 | 
				
			||||||
 | 
					        aria-hidden="true"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <loading-icon
 | 
				
			||||||
 | 
					      v-show="isLoading"
 | 
				
			||||||
 | 
					      :inline="true"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-show="hasError"
 | 
				
			||||||
 | 
					      class="btn-group"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div class="btn btn-default btn-xs disabled">
 | 
				
			||||||
 | 
					        <icon
 | 
				
			||||||
 | 
					          class="prepend-left-8 append-right-8"
 | 
				
			||||||
 | 
					          name="doc_image"
 | 
				
			||||||
 | 
					          :size="16"
 | 
				
			||||||
 | 
					          aria-hidden="true"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        class="btn btn-default btn-xs disabled"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      v-show="hasError"
 | 
				
			||||||
 | 
					      class="btn btn-transparent btn-xs text-primary"
 | 
				
			||||||
 | 
					      type="button"
 | 
				
			||||||
 | 
					      v-tooltip
 | 
				
			||||||
 | 
					      :title="s__('Badges|Reload badge image')"
 | 
				
			||||||
 | 
					      @click="reloadImage"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <icon
 | 
				
			||||||
 | 
					        name="retry"
 | 
				
			||||||
 | 
					        :size="16"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,219 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import _ from 'underscore';
 | 
				
			||||||
 | 
					import { mapActions, mapState } from 'vuex';
 | 
				
			||||||
 | 
					import createFlash from '~/flash';
 | 
				
			||||||
 | 
					import { s__, sprintf } from '~/locale';
 | 
				
			||||||
 | 
					import LoadingButton from '~/vue_shared/components/loading_button.vue';
 | 
				
			||||||
 | 
					import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
 | 
				
			||||||
 | 
					import createEmptyBadge from '../empty_badge';
 | 
				
			||||||
 | 
					import Badge from './badge.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const badgePreviewDelayInMilliseconds = 1500;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'BadgeForm',
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    Badge,
 | 
				
			||||||
 | 
					    LoadingButton,
 | 
				
			||||||
 | 
					    LoadingIcon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    isEditing: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState([
 | 
				
			||||||
 | 
					      'badgeInAddForm',
 | 
				
			||||||
 | 
					      'badgeInEditForm',
 | 
				
			||||||
 | 
					      'docsUrl',
 | 
				
			||||||
 | 
					      'isRendering',
 | 
				
			||||||
 | 
					      'isSaving',
 | 
				
			||||||
 | 
					      'renderedBadge',
 | 
				
			||||||
 | 
					    ]),
 | 
				
			||||||
 | 
					    badge() {
 | 
				
			||||||
 | 
					      if (this.isEditing) {
 | 
				
			||||||
 | 
					        return this.badgeInEditForm;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return this.badgeInAddForm;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canSubmit() {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        this.badge !== null &&
 | 
				
			||||||
 | 
					        this.badge.imageUrl &&
 | 
				
			||||||
 | 
					        this.badge.imageUrl.trim() !== '' &&
 | 
				
			||||||
 | 
					        this.badge.linkUrl &&
 | 
				
			||||||
 | 
					        this.badge.linkUrl.trim() !== '' &&
 | 
				
			||||||
 | 
					        !this.isSaving
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    helpText() {
 | 
				
			||||||
 | 
					      const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
 | 
				
			||||||
 | 
					        .map(placeholder => `<code>%{${placeholder}}</code>`)
 | 
				
			||||||
 | 
					        .join(', ');
 | 
				
			||||||
 | 
					      return sprintf(
 | 
				
			||||||
 | 
					        s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'),
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          docsLinkEnd: '</a>',
 | 
				
			||||||
 | 
					          docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`,
 | 
				
			||||||
 | 
					          placeholders,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    renderedImageUrl() {
 | 
				
			||||||
 | 
					      return this.renderedBadge ? this.renderedBadge.renderedImageUrl : '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    renderedLinkUrl() {
 | 
				
			||||||
 | 
					      return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    imageUrl: {
 | 
				
			||||||
 | 
					      get() {
 | 
				
			||||||
 | 
					        return this.badge ? this.badge.imageUrl : '';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      set(imageUrl) {
 | 
				
			||||||
 | 
					        const badge = this.badge || createEmptyBadge();
 | 
				
			||||||
 | 
					        this.updateBadgeInForm({
 | 
				
			||||||
 | 
					          ...badge,
 | 
				
			||||||
 | 
					          imageUrl,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    linkUrl: {
 | 
				
			||||||
 | 
					      get() {
 | 
				
			||||||
 | 
					        return this.badge ? this.badge.linkUrl : '';
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      set(linkUrl) {
 | 
				
			||||||
 | 
					        const badge = this.badge || createEmptyBadge();
 | 
				
			||||||
 | 
					        this.updateBadgeInForm({
 | 
				
			||||||
 | 
					          ...badge,
 | 
				
			||||||
 | 
					          linkUrl,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    submitButtonLabel() {
 | 
				
			||||||
 | 
					      if (this.isEditing) {
 | 
				
			||||||
 | 
					        return s__('Badges|Save changes');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return s__('Badges|Add badge');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    ...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
 | 
				
			||||||
 | 
					    debouncedPreview: _.debounce(function preview() {
 | 
				
			||||||
 | 
					      this.renderBadge();
 | 
				
			||||||
 | 
					    }, badgePreviewDelayInMilliseconds),
 | 
				
			||||||
 | 
					    onCancel() {
 | 
				
			||||||
 | 
					      this.stopEditing();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onSubmit() {
 | 
				
			||||||
 | 
					      if (!this.canSubmit) {
 | 
				
			||||||
 | 
					        return Promise.resolve();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.isEditing) {
 | 
				
			||||||
 | 
					        return this.saveBadge()
 | 
				
			||||||
 | 
					          .then(() => {
 | 
				
			||||||
 | 
					            createFlash(s__('Badges|The badge was saved.'), 'notice');
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch(error => {
 | 
				
			||||||
 | 
					            createFlash(
 | 
				
			||||||
 | 
					              s__('Badges|Saving the badge failed, please check the entered URLs and try again.'),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            throw error;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return this.addBadge()
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          createFlash(s__('Badges|A new badge was added.'), 'notice');
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(error => {
 | 
				
			||||||
 | 
					          createFlash(
 | 
				
			||||||
 | 
					            s__('Badges|Adding the badge failed, please check the entered URLs and try again.'),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          throw error;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  badgeImageUrlPlaceholder:
 | 
				
			||||||
 | 
					    'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
 | 
				
			||||||
 | 
					  badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <form
 | 
				
			||||||
 | 
					    class="prepend-top-default append-bottom-default"
 | 
				
			||||||
 | 
					    @submit.prevent.stop="onSubmit"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <div class="form-group">
 | 
				
			||||||
 | 
					      <label for="badge-link-url">{{ s__('Badges|Link') }}</label>
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        id="badge-link-url"
 | 
				
			||||||
 | 
					        type="text"
 | 
				
			||||||
 | 
					        class="form-control"
 | 
				
			||||||
 | 
					        v-model="linkUrl"
 | 
				
			||||||
 | 
					        :placeholder="$options.badgeLinkUrlPlaceholder"
 | 
				
			||||||
 | 
					        @input="debouncedPreview"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <span
 | 
				
			||||||
 | 
					        class="help-block"
 | 
				
			||||||
 | 
					        v-html="helpText"
 | 
				
			||||||
 | 
					      ></span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="form-group">
 | 
				
			||||||
 | 
					      <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        id="badge-image-url"
 | 
				
			||||||
 | 
					        type="text"
 | 
				
			||||||
 | 
					        class="form-control"
 | 
				
			||||||
 | 
					        v-model="imageUrl"
 | 
				
			||||||
 | 
					        :placeholder="$options.badgeImageUrlPlaceholder"
 | 
				
			||||||
 | 
					        @input="debouncedPreview"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <span
 | 
				
			||||||
 | 
					        class="help-block"
 | 
				
			||||||
 | 
					        v-html="helpText"
 | 
				
			||||||
 | 
					      ></span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="form-group">
 | 
				
			||||||
 | 
					      <label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
 | 
				
			||||||
 | 
					      <badge
 | 
				
			||||||
 | 
					        id="badge-preview"
 | 
				
			||||||
 | 
					        v-show="renderedBadge && !isRendering"
 | 
				
			||||||
 | 
					        :image-url="renderedImageUrl"
 | 
				
			||||||
 | 
					        :link-url="renderedLinkUrl"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <p v-show="isRendering">
 | 
				
			||||||
 | 
					        <loading-icon
 | 
				
			||||||
 | 
					          :inline="true"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					      <p
 | 
				
			||||||
 | 
					        v-show="!renderedBadge && !isRendering"
 | 
				
			||||||
 | 
					        class="disabled-content"
 | 
				
			||||||
 | 
					      >{{ s__('Badges|No image to preview') }}</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="row-content-block">
 | 
				
			||||||
 | 
					      <loading-button
 | 
				
			||||||
 | 
					        type="submit"
 | 
				
			||||||
 | 
					        container-class="btn btn-success"
 | 
				
			||||||
 | 
					        :disabled="!canSubmit"
 | 
				
			||||||
 | 
					        :loading="isSaving"
 | 
				
			||||||
 | 
					        :label="submitButtonLabel"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <button
 | 
				
			||||||
 | 
					        class="btn btn-cancel"
 | 
				
			||||||
 | 
					        type="button"
 | 
				
			||||||
 | 
					        v-if="isEditing"
 | 
				
			||||||
 | 
					        @click="onCancel"
 | 
				
			||||||
 | 
					      >{{ __('Cancel') }}</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { mapState } from 'vuex';
 | 
				
			||||||
 | 
					import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
 | 
				
			||||||
 | 
					import BadgeListRow from './badge_list_row.vue';
 | 
				
			||||||
 | 
					import { GROUP_BADGE } from '../constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'BadgeList',
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    BadgeListRow,
 | 
				
			||||||
 | 
					    LoadingIcon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState(['badges', 'isLoading', 'kind']),
 | 
				
			||||||
 | 
					    hasNoBadges() {
 | 
				
			||||||
 | 
					      return !this.isLoading && (!this.badges || !this.badges.length);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isGroupBadge() {
 | 
				
			||||||
 | 
					      return this.kind === GROUP_BADGE;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="panel panel-default">
 | 
				
			||||||
 | 
					    <div class="panel-heading">
 | 
				
			||||||
 | 
					      {{ s__('Badges|Your badges') }}
 | 
				
			||||||
 | 
					      <span
 | 
				
			||||||
 | 
					        v-show="!isLoading"
 | 
				
			||||||
 | 
					        class="badge"
 | 
				
			||||||
 | 
					      >{{ badges.length }}</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <loading-icon
 | 
				
			||||||
 | 
					      v-show="isLoading"
 | 
				
			||||||
 | 
					      class="panel-body"
 | 
				
			||||||
 | 
					      size="2"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-if="hasNoBadges"
 | 
				
			||||||
 | 
					      class="panel-body"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
 | 
				
			||||||
 | 
					      <span v-else>{{ s__('Badges|This project has no badges') }}</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      v-else
 | 
				
			||||||
 | 
					      class="panel-body"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <badge-list-row
 | 
				
			||||||
 | 
					        v-for="badge in badges"
 | 
				
			||||||
 | 
					        :key="badge.id"
 | 
				
			||||||
 | 
					        :badge="badge"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,89 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { mapActions, mapState } from 'vuex';
 | 
				
			||||||
 | 
					import { s__ } from '~/locale';
 | 
				
			||||||
 | 
					import Icon from '~/vue_shared/components/icon.vue';
 | 
				
			||||||
 | 
					import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
 | 
				
			||||||
 | 
					import { PROJECT_BADGE } from '../constants';
 | 
				
			||||||
 | 
					import Badge from './badge.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'BadgeListRow',
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    Badge,
 | 
				
			||||||
 | 
					    Icon,
 | 
				
			||||||
 | 
					    LoadingIcon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    badge: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState(['kind']),
 | 
				
			||||||
 | 
					    badgeKindText() {
 | 
				
			||||||
 | 
					      if (this.badge.kind === PROJECT_BADGE) {
 | 
				
			||||||
 | 
					        return s__('Badges|Project Badge');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return s__('Badges|Group Badge');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    canEditBadge() {
 | 
				
			||||||
 | 
					      return this.badge.kind === this.kind;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    ...mapActions(['editBadge', 'updateBadgeInModal']),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="gl-responsive-table-row-layout gl-responsive-table-row">
 | 
				
			||||||
 | 
					    <badge
 | 
				
			||||||
 | 
					      class="table-section section-30"
 | 
				
			||||||
 | 
					      :image-url="badge.renderedImageUrl"
 | 
				
			||||||
 | 
					      :link-url="badge.renderedLinkUrl"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
 | 
				
			||||||
 | 
					    <div class="table-section section-10">
 | 
				
			||||||
 | 
					      <span class="badge">{{ badgeKindText }}</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="table-section section-10 table-button-footer">
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        v-if="canEditBadge"
 | 
				
			||||||
 | 
					        class="table-action-buttons">
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					          class="btn btn-default append-right-8"
 | 
				
			||||||
 | 
					          type="button"
 | 
				
			||||||
 | 
					          :disabled="badge.isDeleting"
 | 
				
			||||||
 | 
					          @click="editBadge(badge)"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <icon
 | 
				
			||||||
 | 
					            name="pencil"
 | 
				
			||||||
 | 
					            :size="16"
 | 
				
			||||||
 | 
					            :aria-label="__('Edit')"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					          class="btn btn-danger"
 | 
				
			||||||
 | 
					          type="button"
 | 
				
			||||||
 | 
					          data-toggle="modal"
 | 
				
			||||||
 | 
					          data-target="#delete-badge-modal"
 | 
				
			||||||
 | 
					          :disabled="badge.isDeleting"
 | 
				
			||||||
 | 
					          @click="updateBadgeInModal(badge)"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <icon
 | 
				
			||||||
 | 
					            name="remove"
 | 
				
			||||||
 | 
					            :size="16"
 | 
				
			||||||
 | 
					            :aria-label="__('Delete')"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <loading-icon
 | 
				
			||||||
 | 
					          v-show="badge.isDeleting"
 | 
				
			||||||
 | 
					          :inline="true"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,70 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { mapState, mapActions } from 'vuex';
 | 
				
			||||||
 | 
					import createFlash from '~/flash';
 | 
				
			||||||
 | 
					import { s__ } from '~/locale';
 | 
				
			||||||
 | 
					import GlModal from '~/vue_shared/components/gl_modal.vue';
 | 
				
			||||||
 | 
					import Badge from './badge.vue';
 | 
				
			||||||
 | 
					import BadgeForm from './badge_form.vue';
 | 
				
			||||||
 | 
					import BadgeList from './badge_list.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'BadgeSettings',
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    Badge,
 | 
				
			||||||
 | 
					    BadgeForm,
 | 
				
			||||||
 | 
					    BadgeList,
 | 
				
			||||||
 | 
					    GlModal,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    ...mapState(['badgeInModal', 'isEditing']),
 | 
				
			||||||
 | 
					    deleteModalText() {
 | 
				
			||||||
 | 
					      return s__(
 | 
				
			||||||
 | 
					        'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    ...mapActions(['deleteBadge']),
 | 
				
			||||||
 | 
					    onSubmitModal() {
 | 
				
			||||||
 | 
					      this.deleteBadge(this.badgeInModal)
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          createFlash(s__('Badges|The badge was deleted.'), 'notice');
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(error => {
 | 
				
			||||||
 | 
					          createFlash(s__('Badges|Deleting the badge failed, please try again.'));
 | 
				
			||||||
 | 
					          throw error;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="badge-settings">
 | 
				
			||||||
 | 
					    <gl-modal
 | 
				
			||||||
 | 
					      id="delete-badge-modal"
 | 
				
			||||||
 | 
					      :header-title-text="s__('Badges|Delete badge?')"
 | 
				
			||||||
 | 
					      footer-primary-button-variant="danger"
 | 
				
			||||||
 | 
					      :footer-primary-button-text="s__('Badges|Delete badge')"
 | 
				
			||||||
 | 
					      @submit="onSubmitModal">
 | 
				
			||||||
 | 
					      <div class="well">
 | 
				
			||||||
 | 
					        <badge
 | 
				
			||||||
 | 
					          :image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''"
 | 
				
			||||||
 | 
					          :link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <p v-html="deleteModalText"></p>
 | 
				
			||||||
 | 
					    </gl-modal>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <badge-form
 | 
				
			||||||
 | 
					      v-show="isEditing"
 | 
				
			||||||
 | 
					      :is-editing="true"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <badge-form
 | 
				
			||||||
 | 
					      v-show="!isEditing"
 | 
				
			||||||
 | 
					      :is-editing="false"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <badge-list v-show="!isEditing" />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					export const GROUP_BADGE = 'group';
 | 
				
			||||||
 | 
					export const PROJECT_BADGE = 'project';
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					export default () => ({
 | 
				
			||||||
 | 
					  imageUrl: '',
 | 
				
			||||||
 | 
					  isDeleting: false,
 | 
				
			||||||
 | 
					  linkUrl: '',
 | 
				
			||||||
 | 
					  renderedImageUrl: '',
 | 
				
			||||||
 | 
					  renderedLinkUrl: '',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,167 @@
 | 
				
			||||||
 | 
					import axios from '~/lib/utils/axios_utils';
 | 
				
			||||||
 | 
					import types from './mutation_types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const transformBackendBadge = badge => ({
 | 
				
			||||||
 | 
					  id: badge.id,
 | 
				
			||||||
 | 
					  imageUrl: badge.image_url,
 | 
				
			||||||
 | 
					  kind: badge.kind,
 | 
				
			||||||
 | 
					  linkUrl: badge.link_url,
 | 
				
			||||||
 | 
					  renderedImageUrl: badge.rendered_image_url,
 | 
				
			||||||
 | 
					  renderedLinkUrl: badge.rendered_link_url,
 | 
				
			||||||
 | 
					  isDeleting: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  requestNewBadge({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.REQUEST_NEW_BADGE);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveNewBadge({ commit }, newBadge) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_NEW_BADGE, newBadge);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveNewBadgeError({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_NEW_BADGE_ERROR);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  addBadge({ dispatch, state }) {
 | 
				
			||||||
 | 
					    const newBadge = state.badgeInAddForm;
 | 
				
			||||||
 | 
					    const endpoint = state.apiEndpointUrl;
 | 
				
			||||||
 | 
					    dispatch('requestNewBadge');
 | 
				
			||||||
 | 
					    return axios
 | 
				
			||||||
 | 
					      .post(endpoint, {
 | 
				
			||||||
 | 
					        image_url: newBadge.imageUrl,
 | 
				
			||||||
 | 
					        link_url: newBadge.linkUrl,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(error => {
 | 
				
			||||||
 | 
					        dispatch('receiveNewBadgeError');
 | 
				
			||||||
 | 
					        throw error;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(res => {
 | 
				
			||||||
 | 
					        dispatch('receiveNewBadge', transformBackendBadge(res.data));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  requestDeleteBadge({ commit }, badgeId) {
 | 
				
			||||||
 | 
					    commit(types.REQUEST_DELETE_BADGE, badgeId);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveDeleteBadge({ commit }, badgeId) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_DELETE_BADGE, badgeId);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveDeleteBadgeError({ commit }, badgeId) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  deleteBadge({ dispatch, state }, badge) {
 | 
				
			||||||
 | 
					    const badgeId = badge.id;
 | 
				
			||||||
 | 
					    dispatch('requestDeleteBadge', badgeId);
 | 
				
			||||||
 | 
					    const endpoint = `${state.apiEndpointUrl}/${badgeId}`;
 | 
				
			||||||
 | 
					    return axios
 | 
				
			||||||
 | 
					      .delete(endpoint)
 | 
				
			||||||
 | 
					      .catch(error => {
 | 
				
			||||||
 | 
					        dispatch('receiveDeleteBadgeError', badgeId);
 | 
				
			||||||
 | 
					        throw error;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(() => {
 | 
				
			||||||
 | 
					        dispatch('receiveDeleteBadge', badgeId);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editBadge({ commit }, badge) {
 | 
				
			||||||
 | 
					    commit(types.START_EDITING, badge);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  requestLoadBadges({ commit }, data) {
 | 
				
			||||||
 | 
					    commit(types.REQUEST_LOAD_BADGES, data);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveLoadBadges({ commit }, badges) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_LOAD_BADGES, badges);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveLoadBadgesError({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_LOAD_BADGES_ERROR);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  loadBadges({ dispatch, state }, data) {
 | 
				
			||||||
 | 
					    dispatch('requestLoadBadges', data);
 | 
				
			||||||
 | 
					    const endpoint = state.apiEndpointUrl;
 | 
				
			||||||
 | 
					    return axios
 | 
				
			||||||
 | 
					      .get(endpoint)
 | 
				
			||||||
 | 
					      .catch(error => {
 | 
				
			||||||
 | 
					        dispatch('receiveLoadBadgesError');
 | 
				
			||||||
 | 
					        throw error;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(res => {
 | 
				
			||||||
 | 
					        dispatch('receiveLoadBadges', res.data.map(transformBackendBadge));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  requestRenderedBadge({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.REQUEST_RENDERED_BADGE);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveRenderedBadge({ commit }, renderedBadge) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_RENDERED_BADGE, renderedBadge);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveRenderedBadgeError({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_RENDERED_BADGE_ERROR);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  renderBadge({ dispatch, state }) {
 | 
				
			||||||
 | 
					    const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm;
 | 
				
			||||||
 | 
					    const { linkUrl, imageUrl } = badge;
 | 
				
			||||||
 | 
					    if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') {
 | 
				
			||||||
 | 
					      return Promise.resolve(badge);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch('requestRenderedBadge');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const parameters = [
 | 
				
			||||||
 | 
					      `link_url=${encodeURIComponent(linkUrl)}`,
 | 
				
			||||||
 | 
					      `image_url=${encodeURIComponent(imageUrl)}`,
 | 
				
			||||||
 | 
					    ].join('&');
 | 
				
			||||||
 | 
					    const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`;
 | 
				
			||||||
 | 
					    return axios
 | 
				
			||||||
 | 
					      .get(renderEndpoint)
 | 
				
			||||||
 | 
					      .catch(error => {
 | 
				
			||||||
 | 
					        dispatch('receiveRenderedBadgeError');
 | 
				
			||||||
 | 
					        throw error;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(res => {
 | 
				
			||||||
 | 
					        dispatch('receiveRenderedBadge', transformBackendBadge(res.data));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  requestUpdatedBadge({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.REQUEST_UPDATED_BADGE);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveUpdatedBadge({ commit }, updatedBadge) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_UPDATED_BADGE, updatedBadge);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  receiveUpdatedBadgeError({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.RECEIVE_UPDATED_BADGE_ERROR);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  saveBadge({ dispatch, state }) {
 | 
				
			||||||
 | 
					    const badge = state.badgeInEditForm;
 | 
				
			||||||
 | 
					    const endpoint = `${state.apiEndpointUrl}/${badge.id}`;
 | 
				
			||||||
 | 
					    dispatch('requestUpdatedBadge');
 | 
				
			||||||
 | 
					    return axios
 | 
				
			||||||
 | 
					      .put(endpoint, {
 | 
				
			||||||
 | 
					        image_url: badge.imageUrl,
 | 
				
			||||||
 | 
					        link_url: badge.linkUrl,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch(error => {
 | 
				
			||||||
 | 
					        dispatch('receiveUpdatedBadgeError');
 | 
				
			||||||
 | 
					        throw error;
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then(res => {
 | 
				
			||||||
 | 
					        dispatch('receiveUpdatedBadge', transformBackendBadge(res.data));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stopEditing({ commit }) {
 | 
				
			||||||
 | 
					    commit(types.STOP_EDITING);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateBadgeInForm({ commit }, badge) {
 | 
				
			||||||
 | 
					    commit(types.UPDATE_BADGE_IN_FORM, badge);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateBadgeInModal({ commit }, badge) {
 | 
				
			||||||
 | 
					    commit(types.UPDATE_BADGE_IN_MODAL, badge);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import Vuex from 'vuex';
 | 
				
			||||||
 | 
					import createState from './state';
 | 
				
			||||||
 | 
					import actions from './actions';
 | 
				
			||||||
 | 
					import mutations from './mutations';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vue.use(Vuex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default new Vuex.Store({
 | 
				
			||||||
 | 
					  state: createState(),
 | 
				
			||||||
 | 
					  actions,
 | 
				
			||||||
 | 
					  mutations,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE',
 | 
				
			||||||
 | 
					  RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR',
 | 
				
			||||||
 | 
					  RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES',
 | 
				
			||||||
 | 
					  RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR',
 | 
				
			||||||
 | 
					  RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE',
 | 
				
			||||||
 | 
					  RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR',
 | 
				
			||||||
 | 
					  RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE',
 | 
				
			||||||
 | 
					  RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR',
 | 
				
			||||||
 | 
					  RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE',
 | 
				
			||||||
 | 
					  RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR',
 | 
				
			||||||
 | 
					  REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE',
 | 
				
			||||||
 | 
					  REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES',
 | 
				
			||||||
 | 
					  REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE',
 | 
				
			||||||
 | 
					  REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE',
 | 
				
			||||||
 | 
					  REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE',
 | 
				
			||||||
 | 
					  START_EDITING: 'START_EDITING',
 | 
				
			||||||
 | 
					  STOP_EDITING: 'STOP_EDITING',
 | 
				
			||||||
 | 
					  UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM',
 | 
				
			||||||
 | 
					  UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,158 @@
 | 
				
			||||||
 | 
					import types from './mutation_types';
 | 
				
			||||||
 | 
					import { PROJECT_BADGE } from '../constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const reorderBadges = badges =>
 | 
				
			||||||
 | 
					  badges.sort((a, b) => {
 | 
				
			||||||
 | 
					    if (a.kind !== b.kind) {
 | 
				
			||||||
 | 
					      return a.kind === PROJECT_BADGE ? 1 : -1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return a.id - b.id;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  [types.RECEIVE_NEW_BADGE](state, newBadge) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badgeInAddForm: null,
 | 
				
			||||||
 | 
					      badges: reorderBadges(state.badges.concat(newBadge)),
 | 
				
			||||||
 | 
					      isSaving: false,
 | 
				
			||||||
 | 
					      renderedBadge: null,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.RECEIVE_NEW_BADGE_ERROR](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      isSaving: false,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.REQUEST_NEW_BADGE](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      isSaving: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.RECEIVE_UPDATED_BADGE](state, updatedBadge) {
 | 
				
			||||||
 | 
					    const badges = state.badges.map(badge => {
 | 
				
			||||||
 | 
					      if (badge.id === updatedBadge.id) {
 | 
				
			||||||
 | 
					        return updatedBadge;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return badge;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badgeInEditForm: null,
 | 
				
			||||||
 | 
					      badges,
 | 
				
			||||||
 | 
					      isEditing: false,
 | 
				
			||||||
 | 
					      isSaving: false,
 | 
				
			||||||
 | 
					      renderedBadge: null,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.RECEIVE_UPDATED_BADGE_ERROR](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      isSaving: false,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.REQUEST_UPDATED_BADGE](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      isSaving: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.RECEIVE_LOAD_BADGES](state, badges) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badges: reorderBadges(badges),
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.RECEIVE_LOAD_BADGES_ERROR](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      isLoading: false,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.REQUEST_LOAD_BADGES](state, data) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      kind: data.kind, // project or group
 | 
				
			||||||
 | 
					      apiEndpointUrl: data.apiEndpointUrl,
 | 
				
			||||||
 | 
					      docsUrl: data.docsUrl,
 | 
				
			||||||
 | 
					      isLoading: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.RECEIVE_DELETE_BADGE](state, badgeId) {
 | 
				
			||||||
 | 
					    const badges = state.badges.filter(badge => badge.id !== badgeId);
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badges,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) {
 | 
				
			||||||
 | 
					    const badges = state.badges.map(badge => {
 | 
				
			||||||
 | 
					      if (badge.id === badgeId) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          ...badge,
 | 
				
			||||||
 | 
					          isDeleting: false,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return badge;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badges,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.REQUEST_DELETE_BADGE](state, badgeId) {
 | 
				
			||||||
 | 
					    const badges = state.badges.map(badge => {
 | 
				
			||||||
 | 
					      if (badge.id === badgeId) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          ...badge,
 | 
				
			||||||
 | 
					          isDeleting: true,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return badge;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badges,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.RECEIVE_RENDERED_BADGE](state, renderedBadge) {
 | 
				
			||||||
 | 
					    Object.assign(state, { isRendering: false, renderedBadge });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.RECEIVE_RENDERED_BADGE_ERROR](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, { isRendering: false });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.REQUEST_RENDERED_BADGE](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, { isRendering: true });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.START_EDITING](state, badge) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badgeInEditForm: { ...badge },
 | 
				
			||||||
 | 
					      isEditing: true,
 | 
				
			||||||
 | 
					      renderedBadge: { ...badge },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.STOP_EDITING](state) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badgeInEditForm: null,
 | 
				
			||||||
 | 
					      isEditing: false,
 | 
				
			||||||
 | 
					      renderedBadge: null,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.UPDATE_BADGE_IN_FORM](state, badge) {
 | 
				
			||||||
 | 
					    if (state.isEditing) {
 | 
				
			||||||
 | 
					      Object.assign(state, {
 | 
				
			||||||
 | 
					        badgeInEditForm: badge,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      Object.assign(state, {
 | 
				
			||||||
 | 
					        badgeInAddForm: badge,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [types.UPDATE_BADGE_IN_MODAL](state, badge) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      badgeInModal: badge,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					export default () => ({
 | 
				
			||||||
 | 
					  apiEndpointUrl: null,
 | 
				
			||||||
 | 
					  badgeInAddForm: null,
 | 
				
			||||||
 | 
					  badgeInEditForm: null,
 | 
				
			||||||
 | 
					  badgeInModal: null,
 | 
				
			||||||
 | 
					  badges: [],
 | 
				
			||||||
 | 
					  docsUrl: null,
 | 
				
			||||||
 | 
					  renderedBadge: null,
 | 
				
			||||||
 | 
					  isEditing: false,
 | 
				
			||||||
 | 
					  isLoading: false,
 | 
				
			||||||
 | 
					  isRendering: false,
 | 
				
			||||||
 | 
					  isSaving: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -94,7 +94,7 @@ export default class FileTemplateMediator {
 | 
				
			||||||
      const hash = urlPieces[1];
 | 
					      const hash = urlPieces[1];
 | 
				
			||||||
      if (hash === 'preview') {
 | 
					      if (hash === 'preview') {
 | 
				
			||||||
        this.hideTemplateSelectorMenu();
 | 
					        this.hideTemplateSelectorMenu();
 | 
				
			||||||
      } else if (hash === 'editor') {
 | 
					      } else if (hash === 'editor' && !this.typeSelector.isHidden()) {
 | 
				
			||||||
        this.showTemplateSelectorMenu();
 | 
					        this.showTemplateSelectorMenu();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,6 +32,10 @@ export default class FileTemplateSelector {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isHidden() {
 | 
				
			||||||
 | 
					    return this.$wrapper.hasClass('hidden');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getToggleText() {
 | 
					  getToggleText() {
 | 
				
			||||||
    return this.$dropdownToggleText.text();
 | 
					    return this.$dropdownToggleText.text();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
import AccessorUtilities from '../../lib/utils/accessor';
 | 
					import AccessorUtilities from '../../lib/utils/accessor';
 | 
				
			||||||
import boardList from './board_list.vue';
 | 
					import boardList from './board_list.vue';
 | 
				
			||||||
import boardBlankState from './board_blank_state';
 | 
					import BoardBlankState from './board_blank_state.vue';
 | 
				
			||||||
import './board_delete';
 | 
					import './board_delete';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Store = gl.issueBoards.BoardsStore;
 | 
					const Store = gl.issueBoards.BoardsStore;
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    boardList,
 | 
					    boardList,
 | 
				
			||||||
    'board-delete': gl.issueBoards.BoardDelete,
 | 
					    'board-delete': gl.issueBoards.BoardDelete,
 | 
				
			||||||
    boardBlankState,
 | 
					    BoardBlankState,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    list: Object,
 | 
					    list: Object,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,42 +1,11 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
/* global ListLabel */
 | 
					/* global ListLabel */
 | 
				
			||||||
 | 
					 | 
				
			||||||
import _ from 'underscore';
 | 
					import _ from 'underscore';
 | 
				
			||||||
import Cookies from 'js-cookie';
 | 
					import Cookies from 'js-cookie';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Store = gl.issueBoards.BoardsStore;
 | 
					const Store = gl.issueBoards.BoardsStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  template: `
 | 
					 | 
				
			||||||
    <div class="board-blank-state">
 | 
					 | 
				
			||||||
      <p>
 | 
					 | 
				
			||||||
        Add the following default lists to your Issue Board with one click:
 | 
					 | 
				
			||||||
      </p>
 | 
					 | 
				
			||||||
      <ul class="board-blank-state-list">
 | 
					 | 
				
			||||||
        <li v-for="label in predefinedLabels">
 | 
					 | 
				
			||||||
          <span
 | 
					 | 
				
			||||||
            class="label-color"
 | 
					 | 
				
			||||||
            :style="{ backgroundColor: label.color }">
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          {{ label.title }}
 | 
					 | 
				
			||||||
        </li>
 | 
					 | 
				
			||||||
      </ul>
 | 
					 | 
				
			||||||
      <p>
 | 
					 | 
				
			||||||
        Starting out with the default set of lists will get you right on the way to making the most of your board.
 | 
					 | 
				
			||||||
      </p>
 | 
					 | 
				
			||||||
      <button
 | 
					 | 
				
			||||||
        class="btn btn-create btn-inverted btn-block"
 | 
					 | 
				
			||||||
        type="button"
 | 
					 | 
				
			||||||
        @click.stop="addDefaultLists">
 | 
					 | 
				
			||||||
        Add default lists
 | 
					 | 
				
			||||||
      </button>
 | 
					 | 
				
			||||||
      <button
 | 
					 | 
				
			||||||
        class="btn btn-default btn-block"
 | 
					 | 
				
			||||||
        type="button"
 | 
					 | 
				
			||||||
        @click.stop="clearBlankState">
 | 
					 | 
				
			||||||
        Nevermind, I'll use my own
 | 
					 | 
				
			||||||
      </button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  `,
 | 
					 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      predefinedLabels: [
 | 
					      predefinedLabels: [
 | 
				
			||||||
| 
						 | 
					@ -89,3 +58,41 @@ export default {
 | 
				
			||||||
    clearBlankState: Store.removeBlankState.bind(Store),
 | 
					    clearBlankState: Store.removeBlankState.bind(Store),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="board-blank-state">
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Add the following default lists to your Issue Board with one click:
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					    <ul class="board-blank-state-list">
 | 
				
			||||||
 | 
					      <li
 | 
				
			||||||
 | 
					        v-for="(label, index) in predefinedLabels"
 | 
				
			||||||
 | 
					        :key="index"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          class="label-color"
 | 
				
			||||||
 | 
					          :style="{ backgroundColor: label.color }">
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        {{ label.title }}
 | 
				
			||||||
 | 
					      </li>
 | 
				
			||||||
 | 
					    </ul>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					      Starting out with the default set of lists will get you
 | 
				
			||||||
 | 
					      right on the way to making the most of your board.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      class="btn btn-create btn-inverted btn-block"
 | 
				
			||||||
 | 
					      type="button"
 | 
				
			||||||
 | 
					      @click.stop="addDefaultLists">
 | 
				
			||||||
 | 
					      Add default lists
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					    <button
 | 
				
			||||||
 | 
					      class="btn btn-default btn-block"
 | 
				
			||||||
 | 
					      type="button"
 | 
				
			||||||
 | 
					      @click.stop="clearBlankState">
 | 
				
			||||||
 | 
					      Nevermind, I'll use my own
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.issue = this.detail.issue;
 | 
					        this.issue = this.detail.issue;
 | 
				
			||||||
        this.list = this.detail.list;
 | 
					        this.list = this.detail.list;
 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$nextTick(() => {
 | 
					 | 
				
			||||||
          this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      deep: true
 | 
					      deep: true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
 | 
				
			||||||
    saveAssignees () {
 | 
					    saveAssignees () {
 | 
				
			||||||
      this.loadingAssignees = true;
 | 
					      this.loadingAssignees = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
 | 
					      gl.issueBoards.BoardsStore.detail.issue.update()
 | 
				
			||||||
        .then(() => {
 | 
					        .then(() => {
 | 
				
			||||||
          this.loadingAssignees = false;
 | 
					          this.loadingAssignees = false;
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return this.issue.assignees.length > this.numberOverLimit;
 | 
					      return this.issue.assignees.length > this.numberOverLimit;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    cardUrl() {
 | 
					 | 
				
			||||||
      let baseUrl = this.issueLinkBase;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.groupId && this.issue.project) {
 | 
					 | 
				
			||||||
        baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return `${baseUrl}/${this.issue.iid}`;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    issueId() {
 | 
					    issueId() {
 | 
				
			||||||
      if (this.issue.iid) {
 | 
					      if (this.issue.iid) {
 | 
				
			||||||
        return `#${this.issue.iid}`;
 | 
					        return `#${this.issue.iid}`;
 | 
				
			||||||
| 
						 | 
					@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <a
 | 
					          <a
 | 
				
			||||||
            class="js-no-trigger"
 | 
					            class="js-no-trigger"
 | 
				
			||||||
            :href="cardUrl"
 | 
					            :href="issue.path"
 | 
				
			||||||
            :title="issue.title">{{ issue.title }}</a>
 | 
					            :title="issue.title">{{ issue.title }}</a>
 | 
				
			||||||
          <span
 | 
					          <span
 | 
				
			||||||
            class="card-number"
 | 
					            class="card-number"
 | 
				
			||||||
            v-if="issueId"
 | 
					            v-if="issueId"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
 | 
					            {{ issue.referencePath }}
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
        </h4>
 | 
					        </h4>
 | 
				
			||||||
        <div class="card-assignee">
 | 
					        <div class="card-assignee">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					import modalMixin from '../../mixins/modal_mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalEmptyState = Vue.extend({
 | 
					gl.issueBoards.ModalEmptyState = Vue.extend({
 | 
				
			||||||
  mixins: [gl.issueBoards.ModalMixins],
 | 
					  mixins: [modalMixin],
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return ModalStore.store;
 | 
					    return ModalStore.store;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,11 +3,11 @@ import Flash from '../../../flash';
 | 
				
			||||||
import { __ } from '../../../locale';
 | 
					import { __ } from '../../../locale';
 | 
				
			||||||
import './lists_dropdown';
 | 
					import './lists_dropdown';
 | 
				
			||||||
import { pluralize } from '../../../lib/utils/text_utility';
 | 
					import { pluralize } from '../../../lib/utils/text_utility';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					import modalMixin from '../../mixins/modal_mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalFooter = Vue.extend({
 | 
					gl.issueBoards.ModalFooter = Vue.extend({
 | 
				
			||||||
  mixins: [gl.issueBoards.ModalMixins],
 | 
					  mixins: [modalMixin],
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      modal: ModalStore.store,
 | 
					      modal: ModalStore.store,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
import modalFilters from './filters';
 | 
					import modalFilters from './filters';
 | 
				
			||||||
import './tabs';
 | 
					import './tabs';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					import modalMixin from '../../mixins/modal_mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalHeader = Vue.extend({
 | 
					gl.issueBoards.ModalHeader = Vue.extend({
 | 
				
			||||||
  mixins: [gl.issueBoards.ModalMixins],
 | 
					  mixins: [modalMixin],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    projectId: {
 | 
					    projectId: {
 | 
				
			||||||
      type: Number,
 | 
					      type: Number,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,8 +7,7 @@ import './header';
 | 
				
			||||||
import './list';
 | 
					import './list';
 | 
				
			||||||
import './footer';
 | 
					import './footer';
 | 
				
			||||||
import './empty_state';
 | 
					import './empty_state';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.IssuesModal = Vue.extend({
 | 
					gl.issueBoards.IssuesModal = Vue.extend({
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,8 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
import bp from '../../../breakpoints';
 | 
					import bp from '../../../breakpoints';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalList = Vue.extend({
 | 
					gl.issueBoards.ModalList = Vue.extend({
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
 | 
					gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import ModalStore from '../../stores/modal_store';
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					import modalMixin from '../../mixins/modal_mixins';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalTabs = Vue.extend({
 | 
					gl.issueBoards.ModalTabs = Vue.extend({
 | 
				
			||||||
  mixins: [gl.issueBoards.ModalMixins],
 | 
					  mixins: [modalMixin],
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return ModalStore.store;
 | 
					    return ModalStore.store;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
 | 
				
			||||||
      type: Object,
 | 
					      type: Object,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    issueUpdate: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    updateUrl() {
 | 
					    updateUrl() {
 | 
				
			||||||
      return this.issueUpdate.replace(':project_path', this.issue.project.path);
 | 
					      return this.issue.path;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
 | 
				
			||||||
  constructor(store, updateUrl = false, cantEdit = []) {
 | 
					  constructor(store, updateUrl = false, cantEdit = []) {
 | 
				
			||||||
    super({
 | 
					    super({
 | 
				
			||||||
      page: 'boards',
 | 
					      page: 'boards',
 | 
				
			||||||
 | 
					      isGroupDecendent: true,
 | 
				
			||||||
      stateFiltersSelector: '.issues-state-filters',
 | 
					      stateFiltersSelector: '.issues-state-filters',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,9 +17,9 @@ import './models/milestone';
 | 
				
			||||||
import './models/project';
 | 
					import './models/project';
 | 
				
			||||||
import './models/assignee';
 | 
					import './models/assignee';
 | 
				
			||||||
import './stores/boards_store';
 | 
					import './stores/boards_store';
 | 
				
			||||||
import './stores/modal_store';
 | 
					import ModalStore from './stores/modal_store';
 | 
				
			||||||
import BoardService from './services/board_service';
 | 
					import BoardService from './services/board_service';
 | 
				
			||||||
import './mixins/modal_mixins';
 | 
					import modalMixin from './mixins/modal_mixins';
 | 
				
			||||||
import './mixins/sortable_default_options';
 | 
					import './mixins/sortable_default_options';
 | 
				
			||||||
import './filters/due_date_filters';
 | 
					import './filters/due_date_filters';
 | 
				
			||||||
import './components/board';
 | 
					import './components/board';
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
 | 
				
			||||||
export default () => {
 | 
					export default () => {
 | 
				
			||||||
  const $boardApp = document.getElementById('board-app');
 | 
					  const $boardApp = document.getElementById('board-app');
 | 
				
			||||||
  const Store = gl.issueBoards.BoardsStore;
 | 
					  const Store = gl.issueBoards.BoardsStore;
 | 
				
			||||||
  const ModalStore = gl.issueBoards.ModalStore;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  window.gl = window.gl || {};
 | 
					  window.gl = window.gl || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -176,7 +175,7 @@ export default () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gl.IssueBoardsModalAddBtn = new Vue({
 | 
					  gl.IssueBoardsModalAddBtn = new Vue({
 | 
				
			||||||
    el: document.getElementById('js-add-issues-btn'),
 | 
					    el: document.getElementById('js-add-issues-btn'),
 | 
				
			||||||
    mixins: [gl.issueBoards.ModalMixins],
 | 
					    mixins: [modalMixin],
 | 
				
			||||||
    data() {
 | 
					    data() {
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        modal: ModalStore.store,
 | 
					        modal: ModalStore.store,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
const ModalStore = gl.issueBoards.ModalStore;
 | 
					import ModalStore from '../stores/modal_store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalMixins = {
 | 
					export default {
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    toggleModal(toggle) {
 | 
					    toggleModal(toggle) {
 | 
				
			||||||
      ModalStore.store.showAddIssuesModal = toggle;
 | 
					      ModalStore.store.showAddIssuesModal = toggle;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,8 @@ class ListIssue {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    this.isLoading = {};
 | 
					    this.isLoading = {};
 | 
				
			||||||
    this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
 | 
					    this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
 | 
				
			||||||
 | 
					    this.referencePath = obj.reference_path;
 | 
				
			||||||
 | 
					    this.path = obj.real_path;
 | 
				
			||||||
    this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
 | 
					    this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
 | 
				
			||||||
    this.milestone_id = obj.milestone_id;
 | 
					    this.milestone_id = obj.milestone_id;
 | 
				
			||||||
    this.project_id = obj.project_id;
 | 
					    this.project_id = obj.project_id;
 | 
				
			||||||
| 
						 | 
					@ -98,7 +100,7 @@ class ListIssue {
 | 
				
			||||||
    this.isLoading[key] = value;
 | 
					    this.isLoading[key] = value;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update (url) {
 | 
					  update () {
 | 
				
			||||||
    const data = {
 | 
					    const data = {
 | 
				
			||||||
      issue: {
 | 
					      issue: {
 | 
				
			||||||
        milestone_id: this.milestone ? this.milestone.id : null,
 | 
					        milestone_id: this.milestone ? this.milestone.id : null,
 | 
				
			||||||
| 
						 | 
					@ -113,7 +115,7 @@ class ListIssue {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const projectPath = this.project ? this.project.path : '';
 | 
					    const projectPath = this.project ? this.project.path : '';
 | 
				
			||||||
    return Vue.http.patch(url.replace(':project_path', projectPath), data);
 | 
					    return Vue.http.patch(`${this.path}.json`, data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ export default class BoardService {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static generateIssuePath(boardId, id) {
 | 
					  static generateIssuePath(boardId, id) {
 | 
				
			||||||
    return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
 | 
					    return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  all() {
 | 
					  all() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,3 @@
 | 
				
			||||||
window.gl = window.gl || {};
 | 
					 | 
				
			||||||
window.gl.issueBoards = window.gl.issueBoards || {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ModalStore {
 | 
					class ModalStore {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    this.store = {
 | 
					    this.store = {
 | 
				
			||||||
| 
						 | 
					@ -95,4 +92,4 @@ class ModalStore {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gl.issueBoards.ModalStore = new ModalStore();
 | 
					export default new ModalStore();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,22 +55,20 @@
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      successCallback(resp) {
 | 
					      successCallback(resp) {
 | 
				
			||||||
        return resp.json().then((response) => {
 | 
					        // depending of the endpoint the response can either bring a `pipelines` key or not.
 | 
				
			||||||
          // depending of the endpoint the response can either bring a `pipelines` key or not.
 | 
					        const pipelines = resp.data.pipelines || resp.data;
 | 
				
			||||||
          const pipelines = response.pipelines || response;
 | 
					        this.setCommonData(pipelines);
 | 
				
			||||||
          this.setCommonData(pipelines);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
 | 
					        const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
 | 
				
			||||||
            detail: {
 | 
					          detail: {
 | 
				
			||||||
              pipelines: response,
 | 
					            pipelines: resp.data,
 | 
				
			||||||
            },
 | 
					          },
 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // notifiy to update the count in tabs
 | 
					 | 
				
			||||||
          if (this.$el.parentElement) {
 | 
					 | 
				
			||||||
            this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // notifiy to update the count in tabs
 | 
				
			||||||
 | 
					        if (this.$el.parentElement) {
 | 
				
			||||||
 | 
					          this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +1,19 @@
 | 
				
			||||||
import $ from 'jquery';
 | 
					import $ from 'jquery';
 | 
				
			||||||
import _ from 'underscore';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getSelector,
 | 
					  getSelector,
 | 
				
			||||||
  togglePopover,
 | 
					 | 
				
			||||||
  inserted,
 | 
					  inserted,
 | 
				
			||||||
  mouseenter,
 | 
					 | 
				
			||||||
  mouseleave,
 | 
					 | 
				
			||||||
} from './feature_highlight_helper';
 | 
					} from './feature_highlight_helper';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  togglePopover,
 | 
				
			||||||
 | 
					  mouseenter,
 | 
				
			||||||
 | 
					  debouncedMouseleave,
 | 
				
			||||||
 | 
					} from '../shared/popover';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
 | 
					export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
 | 
				
			||||||
  const $selector = $(getSelector(id));
 | 
					  const $selector = $(getSelector(id));
 | 
				
			||||||
  const $parent = $selector.parent();
 | 
					  const $parent = $selector.parent();
 | 
				
			||||||
  const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
 | 
					  const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
 | 
				
			||||||
  const hideOnScroll = togglePopover.bind($selector, false);
 | 
					  const hideOnScroll = togglePopover.bind($selector, false);
 | 
				
			||||||
  const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $selector
 | 
					  $selector
 | 
				
			||||||
    // Setup popover
 | 
					    // Setup popover
 | 
				
			||||||
| 
						 | 
					@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .on('mouseenter', mouseenter)
 | 
					    .on('mouseenter', mouseenter)
 | 
				
			||||||
    .on('mouseleave', debouncedMouseleave)
 | 
					    .on('mouseleave', debouncedMouseleave(debounceTimeout))
 | 
				
			||||||
    .on('inserted.bs.popover', inserted)
 | 
					    .on('inserted.bs.popover', inserted)
 | 
				
			||||||
    .on('show.bs.popover', () => {
 | 
					    .on('show.bs.popover', () => {
 | 
				
			||||||
      window.addEventListener('scroll', hideOnScroll);
 | 
					      window.addEventListener('scroll', hideOnScroll, { once: true });
 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .on('hide.bs.popover', () => {
 | 
					 | 
				
			||||||
      window.removeEventListener('scroll', hideOnScroll);
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    // Display feature highlight
 | 
					    // Display feature highlight
 | 
				
			||||||
    .removeAttr('disabled');
 | 
					    .removeAttr('disabled');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils';
 | 
				
			||||||
import { __ } from '../locale';
 | 
					import { __ } from '../locale';
 | 
				
			||||||
import Flash from '../flash';
 | 
					import Flash from '../flash';
 | 
				
			||||||
import LazyLoader from '../lazy_loader';
 | 
					import LazyLoader from '../lazy_loader';
 | 
				
			||||||
 | 
					import { togglePopover } from '../shared/popover';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
 | 
					export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function togglePopover(show) {
 | 
					 | 
				
			||||||
  const isAlreadyShown = this.hasClass('js-popover-show');
 | 
					 | 
				
			||||||
  if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  this.popover(show ? 'show' : 'hide');
 | 
					 | 
				
			||||||
  this.toggleClass('disable-animation js-popover-show', show);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return true;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function dismiss(highlightId) {
 | 
					export function dismiss(highlightId) {
 | 
				
			||||||
  axios.post(this.attr('data-dismiss-endpoint'), {
 | 
					  axios.post(this.attr('data-dismiss-endpoint'), {
 | 
				
			||||||
    feature_name: highlightId,
 | 
					    feature_name: highlightId,
 | 
				
			||||||
| 
						 | 
					@ -27,23 +17,6 @@ export function dismiss(highlightId) {
 | 
				
			||||||
  this.hide();
 | 
					  this.hide();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function mouseleave() {
 | 
					 | 
				
			||||||
  if (!$('.popover:hover').length > 0) {
 | 
					 | 
				
			||||||
    const $featureHighlight = $(this);
 | 
					 | 
				
			||||||
    togglePopover.call($featureHighlight, false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function mouseenter() {
 | 
					 | 
				
			||||||
  const $featureHighlight = $(this);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const showedPopover = togglePopover.call($featureHighlight, true);
 | 
					 | 
				
			||||||
  if (showedPopover) {
 | 
					 | 
				
			||||||
    $('.popover')
 | 
					 | 
				
			||||||
      .on('mouseleave', mouseleave.bind($featureHighlight));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function inserted() {
 | 
					export function inserted() {
 | 
				
			||||||
  const popoverId = this.getAttribute('aria-describedby');
 | 
					  const popoverId = this.getAttribute('aria-describedby');
 | 
				
			||||||
  const highlightId = this.dataset.highlight;
 | 
					  const highlightId = this.dataset.highlight;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
 | 
				
			||||||
    this.filteredSearchInput = this.container.querySelector('.filtered-search');
 | 
					    this.filteredSearchInput = this.container.querySelector('.filtered-search');
 | 
				
			||||||
    this.page = page;
 | 
					    this.page = page;
 | 
				
			||||||
    this.groupsOnly = isGroup;
 | 
					    this.groupsOnly = isGroup;
 | 
				
			||||||
    this.groupAncestor = isGroupAncestor;
 | 
					    this.includeAncestorGroups = isGroupAncestor;
 | 
				
			||||||
    this.isGroupDecendent = isGroupDecendent;
 | 
					    this.includeDescendantGroups = isGroupDecendent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setupMapping();
 | 
					    this.setupMapping();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getLabelsEndpoint() {
 | 
					  getLabelsEndpoint() {
 | 
				
			||||||
    const endpoint = `${this.baseEndpoint}/labels.json`;
 | 
					    let endpoint = `${this.baseEndpoint}/labels.json?`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.groupsOnly) {
 | 
				
			||||||
 | 
					      endpoint = `${endpoint}only_group_labels=true&`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.includeAncestorGroups) {
 | 
				
			||||||
 | 
					      endpoint = `${endpoint}include_ancestor_groups=true&`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.includeDescendantGroups) {
 | 
				
			||||||
 | 
					      endpoint = `${endpoint}include_descendant_groups=true`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return endpoint;
 | 
					    return endpoint;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export default class FilteredSearchManager {
 | 
				
			||||||
  constructor({
 | 
					  constructor({
 | 
				
			||||||
    page,
 | 
					    page,
 | 
				
			||||||
    isGroup = false,
 | 
					    isGroup = false,
 | 
				
			||||||
    isGroupAncestor = false,
 | 
					    isGroupAncestor = true,
 | 
				
			||||||
    isGroupDecendent = false,
 | 
					    isGroupDecendent = false,
 | 
				
			||||||
    filteredSearchTokenKeys = FilteredSearchTokenKeys,
 | 
					    filteredSearchTokenKeys = FilteredSearchTokenKeys,
 | 
				
			||||||
    stateFiltersSelector = '.issues-state-filters',
 | 
					    stateFiltersSelector = '.issues-state-filters',
 | 
				
			||||||
| 
						 | 
					@ -86,6 +86,7 @@ export default class FilteredSearchManager {
 | 
				
			||||||
        page: this.page,
 | 
					        page: this.page,
 | 
				
			||||||
        isGroup: this.isGroup,
 | 
					        isGroup: this.isGroup,
 | 
				
			||||||
        isGroupAncestor: this.isGroupAncestor,
 | 
					        isGroupAncestor: this.isGroupAncestor,
 | 
				
			||||||
 | 
					        isGroupDecendent: this.isGroupDecendent,
 | 
				
			||||||
        filteredSearchTokenKeys: this.filteredSearchTokenKeys,
 | 
					        filteredSearchTokenKeys: this.filteredSearchTokenKeys,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,38 +1,36 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  import { mapActions } from 'vuex';
 | 
					import { mapActions } from 'vuex';
 | 
				
			||||||
  import icon from '~/vue_shared/components/icon.vue';
 | 
					import Icon from '~/vue_shared/components/icon.vue';
 | 
				
			||||||
  import router from '../../ide_router';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					export default {
 | 
				
			||||||
    components: {
 | 
					  components: {
 | 
				
			||||||
      icon,
 | 
					    Icon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    file: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    props: {
 | 
					  },
 | 
				
			||||||
      file: {
 | 
					  computed: {
 | 
				
			||||||
        type: Object,
 | 
					    iconName() {
 | 
				
			||||||
        required: true,
 | 
					      return this.file.tempFile ? 'file-addition' : 'file-modified';
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    iconClass() {
 | 
				
			||||||
      iconName() {
 | 
					      return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
 | 
				
			||||||
        return this.file.tempFile ? 'file-addition' : 'file-modified';
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      iconClass() {
 | 
					 | 
				
			||||||
        return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    methods: {
 | 
					  },
 | 
				
			||||||
      ...mapActions([
 | 
					  methods: {
 | 
				
			||||||
        'discardFileChanges',
 | 
					    ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
 | 
				
			||||||
        'updateViewer',
 | 
					    openFileInEditor(file) {
 | 
				
			||||||
      ]),
 | 
					      return this.openPendingTab(file).then(changeViewer => {
 | 
				
			||||||
      openFileInEditor(file) {
 | 
					        if (changeViewer) {
 | 
				
			||||||
        this.updateViewer('diff');
 | 
					          this.updateViewer('diff');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        router.push(`/project${file.url}`);
 | 
					      });
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex';
 | 
				
			||||||
import ideSidebar from './ide_side_bar.vue';
 | 
					import ideSidebar from './ide_side_bar.vue';
 | 
				
			||||||
import ideContextbar from './ide_context_bar.vue';
 | 
					import ideContextbar from './ide_context_bar.vue';
 | 
				
			||||||
import repoTabs from './repo_tabs.vue';
 | 
					import repoTabs from './repo_tabs.vue';
 | 
				
			||||||
import repoFileButtons from './repo_file_buttons.vue';
 | 
					 | 
				
			||||||
import ideStatusBar from './ide_status_bar.vue';
 | 
					import ideStatusBar from './ide_status_bar.vue';
 | 
				
			||||||
import repoEditor from './repo_editor.vue';
 | 
					import repoEditor from './repo_editor.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +11,6 @@ export default {
 | 
				
			||||||
    ideSidebar,
 | 
					    ideSidebar,
 | 
				
			||||||
    ideContextbar,
 | 
					    ideContextbar,
 | 
				
			||||||
    repoTabs,
 | 
					    repoTabs,
 | 
				
			||||||
    repoFileButtons,
 | 
					 | 
				
			||||||
    ideStatusBar,
 | 
					    ideStatusBar,
 | 
				
			||||||
    repoEditor,
 | 
					    repoEditor,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -60,6 +58,7 @@ export default {
 | 
				
			||||||
        v-if="activeFile"
 | 
					        v-if="activeFile"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <repo-tabs
 | 
					        <repo-tabs
 | 
				
			||||||
 | 
					          :active-file="activeFile"
 | 
				
			||||||
          :files="openFiles"
 | 
					          :files="openFiles"
 | 
				
			||||||
          :viewer="viewer"
 | 
					          :viewer="viewer"
 | 
				
			||||||
          :has-changes="hasChanges"
 | 
					          :has-changes="hasChanges"
 | 
				
			||||||
| 
						 | 
					@ -69,9 +68,6 @@ export default {
 | 
				
			||||||
          class="multi-file-edit-pane-content"
 | 
					          class="multi-file-edit-pane-content"
 | 
				
			||||||
          :file="activeFile"
 | 
					          :file="activeFile"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <repo-file-buttons
 | 
					 | 
				
			||||||
          :file="activeFile"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <ide-status-bar
 | 
					        <ide-status-bar
 | 
				
			||||||
          :file="activeFile"
 | 
					          :file="activeFile"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { __ } from '~/locale';
 | 
				
			||||||
 | 
					import tooltip from '~/vue_shared/directives/tooltip';
 | 
				
			||||||
 | 
					import Icon from '~/vue_shared/components/icon.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    Icon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  directives: {
 | 
				
			||||||
 | 
					    tooltip,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    file: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    showButtons() {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    rawDownloadButtonLabel() {
 | 
				
			||||||
 | 
					      return this.file.binary ? __('Download') : __('Raw');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    v-if="showButtons"
 | 
				
			||||||
 | 
					    class="pull-right ide-btn-group"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					      v-tooltip
 | 
				
			||||||
 | 
					      v-if="!file.binary"
 | 
				
			||||||
 | 
					      :href="file.blamePath"
 | 
				
			||||||
 | 
					      :title="__('Blame')"
 | 
				
			||||||
 | 
					      class="btn btn-xs btn-transparent blame"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <icon
 | 
				
			||||||
 | 
					        name="blame"
 | 
				
			||||||
 | 
					        :size="16"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					      v-tooltip
 | 
				
			||||||
 | 
					      :href="file.commitsPath"
 | 
				
			||||||
 | 
					      :title="__('History')"
 | 
				
			||||||
 | 
					      class="btn btn-xs btn-transparent history"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <icon
 | 
				
			||||||
 | 
					        name="history"
 | 
				
			||||||
 | 
					        :size="16"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					      v-tooltip
 | 
				
			||||||
 | 
					      :href="file.permalink"
 | 
				
			||||||
 | 
					      :title="__('Permalink')"
 | 
				
			||||||
 | 
					      class="btn btn-xs btn-transparent permalink"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <icon
 | 
				
			||||||
 | 
					        name="link"
 | 
				
			||||||
 | 
					        :size="16"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					      v-tooltip
 | 
				
			||||||
 | 
					      :href="file.rawPath"
 | 
				
			||||||
 | 
					      target="_blank"
 | 
				
			||||||
 | 
					      class="btn btn-xs btn-transparent prepend-left-10 raw"
 | 
				
			||||||
 | 
					      rel="noopener noreferrer"
 | 
				
			||||||
 | 
					      :title="rawDownloadButtonLabel">
 | 
				
			||||||
 | 
					      <icon
 | 
				
			||||||
 | 
					        name="download"
 | 
				
			||||||
 | 
					        :size="16"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </a>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -1,25 +1,23 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  import icon from '~/vue_shared/components/icon.vue';
 | 
					import icon from '~/vue_shared/components/icon.vue';
 | 
				
			||||||
  import tooltip from '~/vue_shared/directives/tooltip';
 | 
					import tooltip from '~/vue_shared/directives/tooltip';
 | 
				
			||||||
  import timeAgoMixin from '~/vue_shared/mixins/timeago';
 | 
					import timeAgoMixin from '~/vue_shared/mixins/timeago';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					export default {
 | 
				
			||||||
    components: {
 | 
					  components: {
 | 
				
			||||||
      icon,
 | 
					    icon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  directives: {
 | 
				
			||||||
 | 
					    tooltip,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mixins: [timeAgoMixin],
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    file: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    directives: {
 | 
					  },
 | 
				
			||||||
      tooltip,
 | 
					};
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    mixins: [
 | 
					 | 
				
			||||||
      timeAgoMixin,
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
    props: {
 | 
					 | 
				
			||||||
      file: {
 | 
					 | 
				
			||||||
        type: Object,
 | 
					 | 
				
			||||||
        required: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					@ -50,7 +48,9 @@
 | 
				
			||||||
    <div class="text-right">
 | 
					    <div class="text-right">
 | 
				
			||||||
      {{ file.eol }}
 | 
					      {{ file.eol }}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="text-right">
 | 
					    <div
 | 
				
			||||||
 | 
					      class="text-right"
 | 
				
			||||||
 | 
					      v-if="!file.binary">
 | 
				
			||||||
      {{ file.editorRow }}:{{ file.editorColumn }}
 | 
					      {{ file.editorRow }}:{{ file.editorColumn }}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="text-right">
 | 
					    <div class="text-right">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,16 @@
 | 
				
			||||||
/* global monaco */
 | 
					/* global monaco */
 | 
				
			||||||
import { mapState, mapGetters, mapActions } from 'vuex';
 | 
					import { mapState, mapGetters, mapActions } from 'vuex';
 | 
				
			||||||
import flash from '~/flash';
 | 
					import flash from '~/flash';
 | 
				
			||||||
 | 
					import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
 | 
				
			||||||
import monacoLoader from '../monaco_loader';
 | 
					import monacoLoader from '../monaco_loader';
 | 
				
			||||||
import Editor from '../lib/editor';
 | 
					import Editor from '../lib/editor';
 | 
				
			||||||
 | 
					import IdeFileButtons from './ide_file_buttons.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    ContentViewer,
 | 
				
			||||||
 | 
					    IdeFileButtons,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    file: {
 | 
					    file: {
 | 
				
			||||||
      type: Object,
 | 
					      type: Object,
 | 
				
			||||||
| 
						 | 
					@ -13,27 +19,40 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
 | 
					    ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
 | 
				
			||||||
    ...mapGetters(['currentMergeRequest']),
 | 
					    ...mapGetters(['currentMergeRequest']),
 | 
				
			||||||
    shouldHideEditor() {
 | 
					    shouldHideEditor() {
 | 
				
			||||||
      return this.file && this.file.binary && !this.file.raw;
 | 
					      return this.file && this.file.binary && !this.file.content;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    editTabCSS() {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        active: this.file.viewMode === 'edit',
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    previewTabCSS() {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        active: this.file.viewMode === 'preview',
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
    file(oldVal, newVal) {
 | 
					    file(oldVal, newVal) {
 | 
				
			||||||
      if (newVal.path !== this.file.path) {
 | 
					      // Compare key to allow for files opened in review mode to be cached differently
 | 
				
			||||||
 | 
					      if (newVal.key !== this.file.key) {
 | 
				
			||||||
        this.initMonaco();
 | 
					        this.initMonaco();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    leftPanelCollapsed() {
 | 
					 | 
				
			||||||
      this.editor.updateDimensions();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    rightPanelCollapsed() {
 | 
					    rightPanelCollapsed() {
 | 
				
			||||||
      this.editor.updateDimensions();
 | 
					      this.editor.updateDimensions();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    viewer() {
 | 
					    viewer() {
 | 
				
			||||||
      this.createEditorInstance();
 | 
					      this.createEditorInstance();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    panelResizing() {
 | 
				
			||||||
 | 
					      if (!this.panelResizing) {
 | 
				
			||||||
 | 
					        this.editor.updateDimensions();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeDestroy() {
 | 
					  beforeDestroy() {
 | 
				
			||||||
    this.editor.dispose();
 | 
					    this.editor.dispose();
 | 
				
			||||||
| 
						 | 
					@ -55,6 +74,7 @@ export default {
 | 
				
			||||||
      'changeFileContent',
 | 
					      'changeFileContent',
 | 
				
			||||||
      'setFileLanguage',
 | 
					      'setFileLanguage',
 | 
				
			||||||
      'setEditorPosition',
 | 
					      'setEditorPosition',
 | 
				
			||||||
 | 
					      'setFileViewMode',
 | 
				
			||||||
      'setFileEOL',
 | 
					      'setFileEOL',
 | 
				
			||||||
      'updateViewer',
 | 
					      'updateViewer',
 | 
				
			||||||
      'updateDelayViewerUpdated',
 | 
					      'updateDelayViewerUpdated',
 | 
				
			||||||
| 
						 | 
					@ -70,7 +90,7 @@ export default {
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
        .then(() => {
 | 
					        .then(() => {
 | 
				
			||||||
          const viewerPromise = this.delayViewerUpdated
 | 
					          const viewerPromise = this.delayViewerUpdated
 | 
				
			||||||
            ? this.updateViewer('editor')
 | 
					            ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
 | 
				
			||||||
            : Promise.resolve();
 | 
					            : Promise.resolve();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return viewerPromise;
 | 
					          return viewerPromise;
 | 
				
			||||||
| 
						 | 
					@ -151,16 +171,49 @@ export default {
 | 
				
			||||||
    id="ide"
 | 
					    id="ide"
 | 
				
			||||||
    class="blob-viewer-container blob-editor-container"
 | 
					    class="blob-viewer-container blob-editor-container"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <div
 | 
					    <div class="ide-mode-tabs clearfix">
 | 
				
			||||||
      v-if="shouldHideEditor"
 | 
					      <ul
 | 
				
			||||||
      v-html="file.html"
 | 
					        class="nav-links pull-left"
 | 
				
			||||||
    >
 | 
					        v-if="!shouldHideEditor">
 | 
				
			||||||
 | 
					        <li :class="editTabCSS">
 | 
				
			||||||
 | 
					          <a
 | 
				
			||||||
 | 
					            href="javascript:void(0);"
 | 
				
			||||||
 | 
					            role="button"
 | 
				
			||||||
 | 
					            @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
 | 
				
			||||||
 | 
					            <template v-if="viewer === 'editor'">
 | 
				
			||||||
 | 
					              {{ __('Edit') }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <template v-else>
 | 
				
			||||||
 | 
					              {{ __('Review') }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					        <li
 | 
				
			||||||
 | 
					          v-if="file.previewMode"
 | 
				
			||||||
 | 
					          :class="previewTabCSS">
 | 
				
			||||||
 | 
					          <a
 | 
				
			||||||
 | 
					            href="javascript:void(0);"
 | 
				
			||||||
 | 
					            role="button"
 | 
				
			||||||
 | 
					            @click.prevent="setFileViewMode({ file, viewMode:'preview' })">
 | 
				
			||||||
 | 
					            {{ file.previewMode.previewTitle }}
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        </li>
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					      <ide-file-buttons
 | 
				
			||||||
 | 
					        :file="file"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      v-show="!shouldHideEditor"
 | 
					      v-show="!shouldHideEditor && file.viewMode === 'edit'"
 | 
				
			||||||
      ref="editor"
 | 
					      ref="editor"
 | 
				
			||||||
      class="multi-file-editor-holder"
 | 
					      class="multi-file-editor-holder"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <content-viewer
 | 
				
			||||||
 | 
					      v-if="shouldHideEditor || file.viewMode === 'preview'"
 | 
				
			||||||
 | 
					      :content="file.content || file.raw"
 | 
				
			||||||
 | 
					      :path="file.rawPath || file.path"
 | 
				
			||||||
 | 
					      :file-size="file.size"
 | 
				
			||||||
 | 
					      :project-path="file.projectId"/>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,11 +62,7 @@ export default {
 | 
				
			||||||
        this.toggleTreeOpen(this.file.path);
 | 
					        this.toggleTreeOpen(this.file.path);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const delayPromise = this.file.changed
 | 
					      return this.updateDelayViewerUpdated(true).then(() => {
 | 
				
			||||||
        ? Promise.resolve()
 | 
					 | 
				
			||||||
        : this.updateDelayViewerUpdated(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return delayPromise.then(() => {
 | 
					 | 
				
			||||||
        router.push(`/project${this.file.url}`);
 | 
					        router.push(`/project${this.file.url}`);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,61 +0,0 @@
 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    file: {
 | 
					 | 
				
			||||||
      type: Object,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    showButtons() {
 | 
					 | 
				
			||||||
      return this.file.rawPath ||
 | 
					 | 
				
			||||||
        this.file.blamePath ||
 | 
					 | 
				
			||||||
        this.file.commitsPath ||
 | 
					 | 
				
			||||||
        this.file.permalink;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    rawDownloadButtonLabel() {
 | 
					 | 
				
			||||||
      return this.file.binary ? 'Download' : 'Raw';
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div
 | 
					 | 
				
			||||||
    v-if="showButtons"
 | 
					 | 
				
			||||||
    class="multi-file-editor-btn-group"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <a
 | 
					 | 
				
			||||||
      :href="file.rawPath"
 | 
					 | 
				
			||||||
      target="_blank"
 | 
					 | 
				
			||||||
      class="btn btn-default btn-sm raw"
 | 
					 | 
				
			||||||
      rel="noopener noreferrer">
 | 
					 | 
				
			||||||
      {{ rawDownloadButtonLabel }}
 | 
					 | 
				
			||||||
    </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div
 | 
					 | 
				
			||||||
      class="btn-group"
 | 
					 | 
				
			||||||
      role="group"
 | 
					 | 
				
			||||||
      aria-label="File actions"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <a
 | 
					 | 
				
			||||||
        :href="file.blamePath"
 | 
					 | 
				
			||||||
        class="btn btn-default btn-sm blame"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        Blame
 | 
					 | 
				
			||||||
      </a>
 | 
					 | 
				
			||||||
      <a
 | 
					 | 
				
			||||||
        :href="file.commitsPath"
 | 
					 | 
				
			||||||
        class="btn btn-default btn-sm history"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        History
 | 
					 | 
				
			||||||
      </a>
 | 
					 | 
				
			||||||
      <a
 | 
					 | 
				
			||||||
        :href="file.permalink"
 | 
					 | 
				
			||||||
        class="btn btn-default btn-sm permalink"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        Permalink
 | 
					 | 
				
			||||||
      </a>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,60 +1,64 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  import { mapActions } from 'vuex';
 | 
					import { mapActions } from 'vuex';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  import fileIcon from '~/vue_shared/components/file_icon.vue';
 | 
					import FileIcon from '~/vue_shared/components/file_icon.vue';
 | 
				
			||||||
  import icon from '~/vue_shared/components/icon.vue';
 | 
					import Icon from '~/vue_shared/components/icon.vue';
 | 
				
			||||||
  import fileStatusIcon from './repo_file_status_icon.vue';
 | 
					import FileStatusIcon from './repo_file_status_icon.vue';
 | 
				
			||||||
  import changedFileIcon from './changed_file_icon.vue';
 | 
					import ChangedFileIcon from './changed_file_icon.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					export default {
 | 
				
			||||||
    components: {
 | 
					  components: {
 | 
				
			||||||
      fileStatusIcon,
 | 
					    FileStatusIcon,
 | 
				
			||||||
      fileIcon,
 | 
					    FileIcon,
 | 
				
			||||||
      icon,
 | 
					    Icon,
 | 
				
			||||||
      changedFileIcon,
 | 
					    ChangedFileIcon,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    tab: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    props: {
 | 
					  },
 | 
				
			||||||
      tab: {
 | 
					  data() {
 | 
				
			||||||
        type: Object,
 | 
					    return {
 | 
				
			||||||
        required: true,
 | 
					      tabMouseOver: false,
 | 
				
			||||||
      },
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    closeLabel() {
 | 
				
			||||||
 | 
					      if (this.tab.changed || this.tab.tempFile) {
 | 
				
			||||||
 | 
					        return `${this.tab.name} changed`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return `Close ${this.tab.name}`;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data() {
 | 
					    showChangedIcon() {
 | 
				
			||||||
      return {
 | 
					      return this.tab.changed ? !this.tabMouseOver : false;
 | 
				
			||||||
        tabMouseOver: false,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    computed: {
 | 
					 | 
				
			||||||
      closeLabel() {
 | 
					 | 
				
			||||||
        if (this.tab.changed || this.tab.tempFile) {
 | 
					 | 
				
			||||||
          return `${this.tab.name} changed`;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return `Close ${this.tab.name}`;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      showChangedIcon() {
 | 
					 | 
				
			||||||
        return this.tab.changed ? !this.tabMouseOver : false;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    methods: {
 | 
					  methods: {
 | 
				
			||||||
      ...mapActions([
 | 
					    ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
 | 
				
			||||||
        'closeFile',
 | 
					    clickFile(tab) {
 | 
				
			||||||
      ]),
 | 
					      this.updateDelayViewerUpdated(true);
 | 
				
			||||||
      clickFile(tab) {
 | 
					
 | 
				
			||||||
 | 
					      if (tab.pending) {
 | 
				
			||||||
 | 
					        this.openPendingTab(tab);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
        this.$router.push(`/project${tab.url}`);
 | 
					        this.$router.push(`/project${tab.url}`);
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
      mouseOverTab() {
 | 
					 | 
				
			||||||
        if (this.tab.changed) {
 | 
					 | 
				
			||||||
          this.tabMouseOver = true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      mouseOutTab() {
 | 
					 | 
				
			||||||
        if (this.tab.changed) {
 | 
					 | 
				
			||||||
          this.tabMouseOver = false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					    mouseOverTab() {
 | 
				
			||||||
 | 
					      if (this.tab.changed) {
 | 
				
			||||||
 | 
					        this.tabMouseOver = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mouseOutTab() {
 | 
				
			||||||
 | 
					      if (this.tab.changed) {
 | 
				
			||||||
 | 
					        this.tabMouseOver = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					@ -66,7 +70,7 @@
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      type="button"
 | 
					      type="button"
 | 
				
			||||||
      class="multi-file-tab-close"
 | 
					      class="multi-file-tab-close"
 | 
				
			||||||
      @click.stop.prevent="closeFile(tab.path)"
 | 
					      @click.stop.prevent="closeFile(tab)"
 | 
				
			||||||
      :aria-label="closeLabel"
 | 
					      :aria-label="closeLabel"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <icon
 | 
					      <icon
 | 
				
			||||||
| 
						 | 
					@ -82,7 +86,9 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      class="multi-file-tab"
 | 
					      class="multi-file-tab"
 | 
				
			||||||
      :class="{active : tab.active }"
 | 
					      :class="{
 | 
				
			||||||
 | 
					        active: tab.active
 | 
				
			||||||
 | 
					      }"
 | 
				
			||||||
      :title="tab.url"
 | 
					      :title="tab.url"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <file-icon
 | 
					      <file-icon
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@
 | 
				
			||||||
import { mapActions } from 'vuex';
 | 
					import { mapActions } from 'vuex';
 | 
				
			||||||
import RepoTab from './repo_tab.vue';
 | 
					import RepoTab from './repo_tab.vue';
 | 
				
			||||||
import EditorMode from './editor_mode_dropdown.vue';
 | 
					import EditorMode from './editor_mode_dropdown.vue';
 | 
				
			||||||
 | 
					import router from '../ide_router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
| 
						 | 
					@ -9,6 +10,10 @@ export default {
 | 
				
			||||||
    EditorMode,
 | 
					    EditorMode,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
 | 
					    activeFile: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    files: {
 | 
					    files: {
 | 
				
			||||||
      type: Array,
 | 
					      type: Array,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
| 
						 | 
					@ -38,7 +43,18 @@ export default {
 | 
				
			||||||
    this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
 | 
					    this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    ...mapActions(['updateViewer']),
 | 
					    ...mapActions(['updateViewer', 'removePendingTab']),
 | 
				
			||||||
 | 
					    openFileViewer(viewer) {
 | 
				
			||||||
 | 
					      this.updateViewer(viewer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.activeFile.pending) {
 | 
				
			||||||
 | 
					        return this.removePendingTab(this.activeFile).then(() => {
 | 
				
			||||||
 | 
					          router.push(`/project${this.activeFile.url}`);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -60,7 +76,7 @@ export default {
 | 
				
			||||||
      :show-shadow="showShadow"
 | 
					      :show-shadow="showShadow"
 | 
				
			||||||
      :has-changes="hasChanges"
 | 
					      :has-changes="hasChanges"
 | 
				
			||||||
      :merge-request-id="mergeRequestId"
 | 
					      :merge-request-id="mergeRequestId"
 | 
				
			||||||
      @click="updateViewer"
 | 
					      @click="openFileViewer"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,67 +1,64 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  import { mapActions, mapState } from 'vuex';
 | 
					import { mapActions, mapState } from 'vuex';
 | 
				
			||||||
  import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
 | 
					import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export default {
 | 
					export default {
 | 
				
			||||||
    components: {
 | 
					  components: {
 | 
				
			||||||
      PanelResizer,
 | 
					    PanelResizer,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    collapsible: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    props: {
 | 
					    initialWidth: {
 | 
				
			||||||
      collapsible: {
 | 
					      type: Number,
 | 
				
			||||||
        type: Boolean,
 | 
					      required: true,
 | 
				
			||||||
        required: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      initialWidth: {
 | 
					 | 
				
			||||||
        type: Number,
 | 
					 | 
				
			||||||
        required: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      minSize: {
 | 
					 | 
				
			||||||
        type: Number,
 | 
					 | 
				
			||||||
        required: false,
 | 
					 | 
				
			||||||
        default: 200,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      side: {
 | 
					 | 
				
			||||||
        type: String,
 | 
					 | 
				
			||||||
        required: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data() {
 | 
					    minSize: {
 | 
				
			||||||
      return {
 | 
					      type: Number,
 | 
				
			||||||
        width: this.initialWidth,
 | 
					      required: false,
 | 
				
			||||||
      };
 | 
					      default: 340,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    computed: {
 | 
					    side: {
 | 
				
			||||||
      ...mapState({
 | 
					      type: String,
 | 
				
			||||||
        collapsed(state) {
 | 
					      required: true,
 | 
				
			||||||
          return state[`${this.side}PanelCollapsed`];
 | 
					    },
 | 
				
			||||||
        },
 | 
					  },
 | 
				
			||||||
      }),
 | 
					  data() {
 | 
				
			||||||
      panelStyle() {
 | 
					    return {
 | 
				
			||||||
        if (!this.collapsed) {
 | 
					      width: this.initialWidth,
 | 
				
			||||||
          return {
 | 
					    };
 | 
				
			||||||
            width: `${this.width}px`,
 | 
					  },
 | 
				
			||||||
          };
 | 
					  computed: {
 | 
				
			||||||
        }
 | 
					    ...mapState({
 | 
				
			||||||
 | 
					      collapsed(state) {
 | 
				
			||||||
 | 
					        return state[`${this.side}PanelCollapsed`];
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    panelStyle() {
 | 
				
			||||||
 | 
					      if (!this.collapsed) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          width: `${this.width}px`,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {};
 | 
					      return {};
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    methods: {
 | 
					  },
 | 
				
			||||||
      ...mapActions([
 | 
					  methods: {
 | 
				
			||||||
        'setPanelCollapsedStatus',
 | 
					    ...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
 | 
				
			||||||
        'setResizingStatus',
 | 
					    toggleFullbarCollapsed() {
 | 
				
			||||||
      ]),
 | 
					      if (this.collapsed && this.collapsible) {
 | 
				
			||||||
      toggleFullbarCollapsed() {
 | 
					        this.setPanelCollapsedStatus({
 | 
				
			||||||
        if (this.collapsed && this.collapsible) {
 | 
					          side: this.side,
 | 
				
			||||||
          this.setPanelCollapsedStatus({
 | 
					          collapsed: !this.collapsed,
 | 
				
			||||||
            side: this.side,
 | 
					        });
 | 
				
			||||||
            collapsed: !this.collapsed,
 | 
					      }
 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    maxSize: (window.innerWidth / 2),
 | 
					  },
 | 
				
			||||||
  };
 | 
					  maxSize: window.innerWidth / 2,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,11 +36,11 @@ const router = new VueRouter({
 | 
				
			||||||
  base: `${gon.relative_url_root}/-/ide/`,
 | 
					  base: `${gon.relative_url_root}/-/ide/`,
 | 
				
			||||||
  routes: [
 | 
					  routes: [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      path: '/project/:namespace/:project',
 | 
					      path: '/project/:namespace/:project+',
 | 
				
			||||||
      component: EmptyRouterComponent,
 | 
					      component: EmptyRouterComponent,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          path: ':targetmode/:branch/*',
 | 
					          path: ':targetmode(edit|tree|blob)/:branch/*',
 | 
				
			||||||
          component: EmptyRouterComponent,
 | 
					          component: EmptyRouterComponent,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => {
 | 
				
			||||||
              if (to.params[0]) {
 | 
					              if (to.params[0]) {
 | 
				
			||||||
                const path =
 | 
					                const path =
 | 
				
			||||||
                  to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
 | 
					                  to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
 | 
				
			||||||
                const treeEntry = store.state.entries[path];
 | 
					                const treeEntryKey = Object.keys(store.state.entries).find(
 | 
				
			||||||
 | 
					                  key => key === path && !store.state.entries[key].pending,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                const treeEntry = store.state.entries[treeEntryKey];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (treeEntry) {
 | 
					                if (treeEntry) {
 | 
				
			||||||
                  store.dispatch('handleTreeEntryAction', treeEntry);
 | 
					                  store.dispatch('handleTreeEntryAction', treeEntry);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,12 +13,12 @@ export default class Model {
 | 
				
			||||||
      (this.originalModel = this.monaco.editor.createModel(
 | 
					      (this.originalModel = this.monaco.editor.createModel(
 | 
				
			||||||
        this.file.raw,
 | 
					        this.file.raw,
 | 
				
			||||||
        undefined,
 | 
					        undefined,
 | 
				
			||||||
        new this.monaco.Uri(null, null, `original/${this.file.path}`),
 | 
					        new this.monaco.Uri(null, null, `original/${this.file.key}`),
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
      (this.model = this.monaco.editor.createModel(
 | 
					      (this.model = this.monaco.editor.createModel(
 | 
				
			||||||
        this.content,
 | 
					        this.content,
 | 
				
			||||||
        undefined,
 | 
					        undefined,
 | 
				
			||||||
        new this.monaco.Uri(null, null, this.file.path),
 | 
					        new this.monaco.Uri(null, null, this.file.key),
 | 
				
			||||||
      )),
 | 
					      )),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    if (this.file.mrChange) {
 | 
					    if (this.file.mrChange) {
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ export default class Model {
 | 
				
			||||||
    this.updateContent = this.updateContent.bind(this);
 | 
					    this.updateContent = this.updateContent.bind(this);
 | 
				
			||||||
    this.dispose = this.dispose.bind(this);
 | 
					    this.dispose = this.dispose.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
 | 
					    eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
 | 
				
			||||||
    eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
 | 
					    eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,7 +53,7 @@ export default class Model {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get path() {
 | 
					  get path() {
 | 
				
			||||||
    return this.file.path;
 | 
					    return this.file.key;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getModel() {
 | 
					  getModel() {
 | 
				
			||||||
| 
						 | 
					@ -88,7 +88,7 @@ export default class Model {
 | 
				
			||||||
    this.disposable.dispose();
 | 
					    this.disposable.dispose();
 | 
				
			||||||
    this.events.clear();
 | 
					    this.events.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
 | 
					    eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
 | 
				
			||||||
    eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
 | 
					    eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,17 +9,17 @@ export default class ModelManager {
 | 
				
			||||||
    this.models = new Map();
 | 
					    this.models = new Map();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  hasCachedModel(path) {
 | 
					  hasCachedModel(key) {
 | 
				
			||||||
    return this.models.has(path);
 | 
					    return this.models.has(key);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getModel(path) {
 | 
					  getModel(key) {
 | 
				
			||||||
    return this.models.get(path);
 | 
					    return this.models.get(key);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addModel(file) {
 | 
					  addModel(file) {
 | 
				
			||||||
    if (this.hasCachedModel(file.path)) {
 | 
					    if (this.hasCachedModel(file.key)) {
 | 
				
			||||||
      return this.getModel(file.path);
 | 
					      return this.getModel(file.key);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const model = new Model(this.monaco, file);
 | 
					    const model = new Model(this.monaco, file);
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ export default class ModelManager {
 | 
				
			||||||
    this.disposable.add(model);
 | 
					    this.disposable.add(model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eventHub.$on(
 | 
					    eventHub.$on(
 | 
				
			||||||
      `editor.update.model.dispose.${file.path}`,
 | 
					      `editor.update.model.dispose.${file.key}`,
 | 
				
			||||||
      this.removeCachedModel.bind(this, file),
 | 
					      this.removeCachedModel.bind(this, file),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,12 +35,9 @@ export default class ModelManager {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  removeCachedModel(file) {
 | 
					  removeCachedModel(file) {
 | 
				
			||||||
    this.models.delete(file.path);
 | 
					    this.models.delete(file.key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    eventHub.$off(
 | 
					    eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
 | 
				
			||||||
      `editor.update.model.dispose.${file.path}`,
 | 
					 | 
				
			||||||
      this.removeCachedModel,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dispose() {
 | 
					  dispose() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,6 +69,7 @@ export default class Editor {
 | 
				
			||||||
          occurrencesHighlight: false,
 | 
					          occurrencesHighlight: false,
 | 
				
			||||||
          renderLineHighlight: 'none',
 | 
					          renderLineHighlight: 'none',
 | 
				
			||||||
          hideCursorInOverviewRuler: true,
 | 
					          hideCursorInOverviewRuler: true,
 | 
				
			||||||
 | 
					          renderSideBySide: Editor.renderSideBySide(domElement),
 | 
				
			||||||
        })),
 | 
					        })),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,7 +82,7 @@ export default class Editor {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attachModel(model) {
 | 
					  attachModel(model) {
 | 
				
			||||||
    if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
 | 
					    if (this.isDiffEditorType) {
 | 
				
			||||||
      this.instance.setModel({
 | 
					      this.instance.setModel({
 | 
				
			||||||
        original: model.getOriginalModel(),
 | 
					        original: model.getOriginalModel(),
 | 
				
			||||||
        modified: model.getModel(),
 | 
					        modified: model.getModel(),
 | 
				
			||||||
| 
						 | 
					@ -153,6 +154,7 @@ export default class Editor {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateDimensions() {
 | 
					  updateDimensions() {
 | 
				
			||||||
    this.instance.layout();
 | 
					    this.instance.layout();
 | 
				
			||||||
 | 
					    this.updateDiffView();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setPosition({ lineNumber, column }) {
 | 
					  setPosition({ lineNumber, column }) {
 | 
				
			||||||
| 
						 | 
					@ -171,4 +173,20 @@ export default class Editor {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
 | 
					    this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateDiffView() {
 | 
				
			||||||
 | 
					    if (!this.isDiffEditorType) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.instance.updateOptions({
 | 
				
			||||||
 | 
					      renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get isDiffEditorType() {
 | 
				
			||||||
 | 
					    return this.instance.getEditorType() === 'vs.editor.IDiffEditor';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static renderSideBySide(domElement) {
 | 
				
			||||||
 | 
					    return domElement.offsetWidth >= 700;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ export const defaultEditorOptions = {
 | 
				
			||||||
  minimap: {
 | 
					  minimap: {
 | 
				
			||||||
    enabled: false,
 | 
					    enabled: false,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  wordWrap: 'bounded',
 | 
					  wordWrap: 'on',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default [
 | 
					export default [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const closeAllFiles = ({ state, dispatch }) => {
 | 
					export const closeAllFiles = ({ state, dispatch }) => {
 | 
				
			||||||
  state.openFiles.forEach(file => dispatch('closeFile', file.path));
 | 
					  state.openFiles.forEach(file => dispatch('closeFile', file));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
 | 
					export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,24 +6,34 @@ import * as types from '../mutation_types';
 | 
				
			||||||
import router from '../../ide_router';
 | 
					import router from '../../ide_router';
 | 
				
			||||||
import { setPageTitle } from '../utils';
 | 
					import { setPageTitle } from '../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
 | 
					export const closeFile = ({ commit, state, dispatch }, file) => {
 | 
				
			||||||
  const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
 | 
					  const path = file.path;
 | 
				
			||||||
  const file = state.entries[path];
 | 
					  const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
 | 
				
			||||||
  const fileWasActive = file.active;
 | 
					  const fileWasActive = file.active;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  commit(types.TOGGLE_FILE_OPEN, path);
 | 
					  if (file.pending) {
 | 
				
			||||||
  commit(types.SET_FILE_ACTIVE, { path, active: false });
 | 
					    commit(types.REMOVE_PENDING_TAB, file);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    commit(types.TOGGLE_FILE_OPEN, path);
 | 
				
			||||||
 | 
					    commit(types.SET_FILE_ACTIVE, { path, active: false });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (state.openFiles.length > 0 && fileWasActive) {
 | 
					  if (state.openFiles.length > 0 && fileWasActive) {
 | 
				
			||||||
    const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
 | 
					    const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
 | 
				
			||||||
    const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
 | 
					    const nextFileToOpen = state.openFiles[nextIndexToOpen];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    router.push(`/project${nextFileToOpen.url}`);
 | 
					    if (nextFileToOpen.pending) {
 | 
				
			||||||
 | 
					      dispatch('updateViewer', 'diff');
 | 
				
			||||||
 | 
					      dispatch('openPendingTab', nextFileToOpen);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch('updateDelayViewerUpdated', true);
 | 
				
			||||||
 | 
					      router.push(`/project${nextFileToOpen.url}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  } else if (!state.openFiles.length) {
 | 
					  } else if (!state.openFiles.length) {
 | 
				
			||||||
    router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
 | 
					    router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  eventHub.$emit(`editor.update.model.dispose.${file.path}`);
 | 
					  eventHub.$emit(`editor.update.model.dispose.${file.key}`);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
 | 
					export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
 | 
				
			||||||
| 
						 | 
					@ -139,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
 | 
				
			||||||
 | 
					  commit(types.SET_FILE_VIEWMODE, { file, viewMode });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const discardFileChanges = ({ state, commit }, path) => {
 | 
					export const discardFileChanges = ({ state, commit }, path) => {
 | 
				
			||||||
  const file = state.entries[path];
 | 
					  const file = state.entries[path];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -151,3 +165,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
 | 
					  eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
 | 
				
			||||||
 | 
					  if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  commit(types.ADD_PENDING_TAB, { file });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dispatch('scrollToTab');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const removePendingTab = ({ commit }, file) => {
 | 
				
			||||||
 | 
					  commit(types.REMOVE_PENDING_TAB, file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  eventHub.$emit(`editor.update.model.dispose.${file.key}`);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,9 +37,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
 | 
				
			||||||
  const commitMsg = sprintf(
 | 
					  const commitMsg = sprintf(
 | 
				
			||||||
    __('Your changes have been committed. Commit %{commitId} %{commitStats}'),
 | 
					    __('Your changes have been committed. Commit %{commitId} %{commitStats}'),
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      commitId: `<a href="${currentProject.web_url}/commit/${
 | 
					      commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${
 | 
				
			||||||
        data.short_id
 | 
					        data.short_id
 | 
				
			||||||
      }" class="commit-sha">${data.short_id}</a>`,
 | 
					      }</a>`,
 | 
				
			||||||
      commitStats,
 | 
					      commitStats,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    false,
 | 
					    false,
 | 
				
			||||||
| 
						 | 
					@ -54,9 +54,7 @@ export const checkCommitStatus = ({ rootState }) =>
 | 
				
			||||||
    .then(({ data }) => {
 | 
					    .then(({ data }) => {
 | 
				
			||||||
      const { id } = data.commit;
 | 
					      const { id } = data.commit;
 | 
				
			||||||
      const selectedBranch =
 | 
					      const selectedBranch =
 | 
				
			||||||
        rootState.projects[rootState.currentProjectId].branches[
 | 
					        rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
 | 
				
			||||||
          rootState.currentBranchId
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (selectedBranch.workingReference !== id) {
 | 
					      if (selectedBranch.workingReference !== id) {
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
| 
						 | 
					@ -135,32 +133,15 @@ export const updateFilesAfterCommit = (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
 | 
					  if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
 | 
				
			||||||
    router.push(
 | 
					    router.push(
 | 
				
			||||||
      `/project/${rootState.currentProjectId}/blob/${branch}/${
 | 
					      `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
 | 
				
			||||||
        rootGetters.activeFile.path
 | 
					 | 
				
			||||||
      }`,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const commitChanges = ({
 | 
					export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
 | 
				
			||||||
  commit,
 | 
					 | 
				
			||||||
  state,
 | 
					 | 
				
			||||||
  getters,
 | 
					 | 
				
			||||||
  dispatch,
 | 
					 | 
				
			||||||
  rootState,
 | 
					 | 
				
			||||||
}) => {
 | 
					 | 
				
			||||||
  const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
 | 
					  const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
 | 
				
			||||||
  const payload = createCommitPayload(
 | 
					  const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
 | 
				
			||||||
    getters.branchName,
 | 
					  const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
 | 
				
			||||||
    newBranch,
 | 
					 | 
				
			||||||
    state,
 | 
					 | 
				
			||||||
    rootState,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const getCommitStatus = newBranch
 | 
					 | 
				
			||||||
    ? Promise.resolve(false)
 | 
					 | 
				
			||||||
    : dispatch('checkCommitStatus');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  commit(types.UPDATE_LOADING, true);
 | 
					  commit(types.UPDATE_LOADING, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -182,28 +163,29 @@ export const commitChanges = ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!data.short_id) {
 | 
					      if (!data.short_id) {
 | 
				
			||||||
        flash(data.message, 'alert', document, null, false, true);
 | 
					        flash(data.message, 'alert', document, null, false, true);
 | 
				
			||||||
        return;
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      dispatch('setLastCommitMessage', data);
 | 
					      dispatch('setLastCommitMessage', data);
 | 
				
			||||||
      dispatch('updateCommitMessage', '');
 | 
					      dispatch('updateCommitMessage', '');
 | 
				
			||||||
 | 
					      return dispatch('updateFilesAfterCommit', {
 | 
				
			||||||
      if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
 | 
					        data,
 | 
				
			||||||
        dispatch(
 | 
					        branch: getters.branchName,
 | 
				
			||||||
          'redirectToUrl',
 | 
					      })
 | 
				
			||||||
          createNewMergeRequestUrl(
 | 
					        .then(() => {
 | 
				
			||||||
            rootState.projects[rootState.currentProjectId].web_url,
 | 
					          if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
 | 
				
			||||||
            getters.branchName,
 | 
					            dispatch(
 | 
				
			||||||
            rootState.currentBranchId,
 | 
					              'redirectToUrl',
 | 
				
			||||||
          ),
 | 
					              createNewMergeRequestUrl(
 | 
				
			||||||
          { root: true },
 | 
					                rootState.projects[rootState.currentProjectId].web_url,
 | 
				
			||||||
        );
 | 
					                getters.branchName,
 | 
				
			||||||
      } else {
 | 
					                rootState.currentBranchId,
 | 
				
			||||||
        dispatch('updateFilesAfterCommit', {
 | 
					              ),
 | 
				
			||||||
          data,
 | 
					              { root: true },
 | 
				
			||||||
          branch: getters.branchName,
 | 
					            );
 | 
				
			||||||
        });
 | 
					          }
 | 
				
			||||||
      }
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .catch(err => {
 | 
					    .catch(err => {
 | 
				
			||||||
      let errMsg = __('Error committing changes. Please try again.');
 | 
					      let errMsg = __('Error committing changes. Please try again.');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
 | 
				
			||||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
 | 
					export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
 | 
				
			||||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
 | 
					export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
 | 
				
			||||||
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
 | 
					export const SET_FILE_POSITION = 'SET_FILE_POSITION';
 | 
				
			||||||
 | 
					export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
 | 
				
			||||||
export const SET_FILE_EOL = 'SET_FILE_EOL';
 | 
					export const SET_FILE_EOL = 'SET_FILE_EOL';
 | 
				
			||||||
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
 | 
					export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
 | 
				
			||||||
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
 | 
					export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
 | 
				
			||||||
| 
						 | 
					@ -49,3 +50,6 @@ export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
 | 
				
			||||||
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
 | 
					export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
 | 
				
			||||||
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
 | 
					export const UPDATE_VIEWER = 'UPDATE_VIEWER';
 | 
				
			||||||
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
 | 
					export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
 | 
				
			||||||
 | 
					export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,14 @@ export default {
 | 
				
			||||||
    Object.assign(state.entries[path], {
 | 
					    Object.assign(state.entries[path], {
 | 
				
			||||||
      active,
 | 
					      active,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (active && !state.entries[path].pending) {
 | 
				
			||||||
 | 
					      Object.assign(state, {
 | 
				
			||||||
 | 
					        openFiles: state.openFiles.map(f =>
 | 
				
			||||||
 | 
					          Object.assign(f, { active: f.pending ? false : f.active }),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [types.TOGGLE_FILE_OPEN](state, path) {
 | 
					  [types.TOGGLE_FILE_OPEN](state, path) {
 | 
				
			||||||
    Object.assign(state.entries[path], {
 | 
					    Object.assign(state.entries[path], {
 | 
				
			||||||
| 
						 | 
					@ -12,10 +20,14 @@ export default {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (state.entries[path].opened) {
 | 
					    if (state.entries[path].opened) {
 | 
				
			||||||
      state.openFiles.push(state.entries[path]);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      Object.assign(state, {
 | 
					      Object.assign(state, {
 | 
				
			||||||
        openFiles: state.openFiles.filter(f => f.path !== path),
 | 
					        openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const file = state.entries[path];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Object.assign(state, {
 | 
				
			||||||
 | 
					        openFiles: state.openFiles.filter(f => f.key !== file.key),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -30,6 +42,8 @@ export default {
 | 
				
			||||||
      renderError: data.render_error,
 | 
					      renderError: data.render_error,
 | 
				
			||||||
      raw: null,
 | 
					      raw: null,
 | 
				
			||||||
      baseRaw: null,
 | 
					      baseRaw: null,
 | 
				
			||||||
 | 
					      html: data.html,
 | 
				
			||||||
 | 
					      size: data.size,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  [types.SET_FILE_RAW_DATA](state, { file, raw }) {
 | 
					  [types.SET_FILE_RAW_DATA](state, { file, raw }) {
 | 
				
			||||||
| 
						 | 
					@ -71,6 +85,11 @@ export default {
 | 
				
			||||||
      mrChange,
 | 
					      mrChange,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  [types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
 | 
				
			||||||
 | 
					    Object.assign(state.entries[file.path], {
 | 
				
			||||||
 | 
					      viewMode,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  [types.DISCARD_FILE_CHANGES](state, path) {
 | 
					  [types.DISCARD_FILE_CHANGES](state, path) {
 | 
				
			||||||
    Object.assign(state.entries[path], {
 | 
					    Object.assign(state.entries[path], {
 | 
				
			||||||
      content: state.entries[path].raw,
 | 
					      content: state.entries[path].raw,
 | 
				
			||||||
| 
						 | 
					@ -92,4 +111,37 @@ export default {
 | 
				
			||||||
      changed,
 | 
					      changed,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
 | 
				
			||||||
 | 
					    const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
 | 
				
			||||||
 | 
					    let openFiles = state.openFiles.map(f =>
 | 
				
			||||||
 | 
					      Object.assign(f, { active: f.path === file.path, opened: false }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!pendingTab) {
 | 
				
			||||||
 | 
					      const openFile = openFiles.find(f => f.path === file.path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
 | 
				
			||||||
 | 
					        if (!f) return acc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (f.path === file.path) {
 | 
				
			||||||
 | 
					          return acc.concat({
 | 
				
			||||||
 | 
					            ...f,
 | 
				
			||||||
 | 
					            active: true,
 | 
				
			||||||
 | 
					            pending: true,
 | 
				
			||||||
 | 
					            opened: true,
 | 
				
			||||||
 | 
					            key: `${keyPrefix}-${f.key}`,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return acc.concat(f);
 | 
				
			||||||
 | 
					      }, []);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Object.assign(state, { openFiles });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  [types.REMOVE_PENDING_TAB](state, file) {
 | 
				
			||||||
 | 
					    Object.assign(state, {
 | 
				
			||||||
 | 
					      openFiles: state.openFiles.filter(f => f.key !== file.key),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
export const dataStructure = () => ({
 | 
					export const dataStructure = () => ({
 | 
				
			||||||
  id: '',
 | 
					  id: '',
 | 
				
			||||||
 | 
					  // Key will contain a mixture of ID and path
 | 
				
			||||||
 | 
					  // it can also contain a prefix `pending-` for files opened in review mode
 | 
				
			||||||
  key: '',
 | 
					  key: '',
 | 
				
			||||||
  type: '',
 | 
					  type: '',
 | 
				
			||||||
  projectId: '',
 | 
					  projectId: '',
 | 
				
			||||||
| 
						 | 
					@ -36,6 +38,9 @@ export const dataStructure = () => ({
 | 
				
			||||||
  editorColumn: 1,
 | 
					  editorColumn: 1,
 | 
				
			||||||
  fileLanguage: '',
 | 
					  fileLanguage: '',
 | 
				
			||||||
  eol: '',
 | 
					  eol: '',
 | 
				
			||||||
 | 
					  viewMode: 'edit',
 | 
				
			||||||
 | 
					  previewMode: null,
 | 
				
			||||||
 | 
					  size: 0,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const decorateData = entity => {
 | 
					export const decorateData = entity => {
 | 
				
			||||||
| 
						 | 
					@ -55,8 +60,9 @@ export const decorateData = entity => {
 | 
				
			||||||
    changed = false,
 | 
					    changed = false,
 | 
				
			||||||
    parentTreeUrl = '',
 | 
					    parentTreeUrl = '',
 | 
				
			||||||
    base64 = false,
 | 
					    base64 = false,
 | 
				
			||||||
 | 
					    previewMode,
 | 
				
			||||||
    file_lock,
 | 
					    file_lock,
 | 
				
			||||||
 | 
					    html,
 | 
				
			||||||
  } = entity;
 | 
					  } = entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
| 
						 | 
					@ -77,8 +83,9 @@ export const decorateData = entity => {
 | 
				
			||||||
    renderError,
 | 
					    renderError,
 | 
				
			||||||
    content,
 | 
					    content,
 | 
				
			||||||
    base64,
 | 
					    base64,
 | 
				
			||||||
 | 
					    previewMode,
 | 
				
			||||||
    file_lock,
 | 
					    file_lock,
 | 
				
			||||||
 | 
					    html,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,8 @@
 | 
				
			||||||
 | 
					import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
 | 
				
			||||||
import { decorateData, sortTree } from '../utils';
 | 
					import { decorateData, sortTree } from '../utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
self.addEventListener('message', e => {
 | 
					self.addEventListener('message', e => {
 | 
				
			||||||
  const {
 | 
					  const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
 | 
				
			||||||
    data,
 | 
					 | 
				
			||||||
    projectId,
 | 
					 | 
				
			||||||
    branchId,
 | 
					 | 
				
			||||||
    tempFile = false,
 | 
					 | 
				
			||||||
    content = '',
 | 
					 | 
				
			||||||
    base64 = false,
 | 
					 | 
				
			||||||
  } = e.data;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const treeList = [];
 | 
					  const treeList = [];
 | 
				
			||||||
  let file;
 | 
					  let file;
 | 
				
			||||||
| 
						 | 
					@ -19,9 +13,7 @@ self.addEventListener('message', e => {
 | 
				
			||||||
    if (pathSplit.length > 0) {
 | 
					    if (pathSplit.length > 0) {
 | 
				
			||||||
      pathSplit.reduce((pathAcc, folderName) => {
 | 
					      pathSplit.reduce((pathAcc, folderName) => {
 | 
				
			||||||
        const parentFolder = acc[pathAcc[pathAcc.length - 1]];
 | 
					        const parentFolder = acc[pathAcc[pathAcc.length - 1]];
 | 
				
			||||||
        const folderPath = `${
 | 
					        const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
 | 
				
			||||||
          parentFolder ? `${parentFolder.path}/` : ''
 | 
					 | 
				
			||||||
        }${folderName}`;
 | 
					 | 
				
			||||||
        const foundEntry = acc[folderPath];
 | 
					        const foundEntry = acc[folderPath];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!foundEntry) {
 | 
					        if (!foundEntry) {
 | 
				
			||||||
| 
						 | 
					@ -33,9 +25,7 @@ self.addEventListener('message', e => {
 | 
				
			||||||
            path: folderPath,
 | 
					            path: folderPath,
 | 
				
			||||||
            url: `/${projectId}/tree/${branchId}/${folderPath}/`,
 | 
					            url: `/${projectId}/tree/${branchId}/${folderPath}/`,
 | 
				
			||||||
            type: 'tree',
 | 
					            type: 'tree',
 | 
				
			||||||
            parentTreeUrl: parentFolder
 | 
					            parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
 | 
				
			||||||
              ? parentFolder.url
 | 
					 | 
				
			||||||
              : `/${projectId}/tree/${branchId}/`,
 | 
					 | 
				
			||||||
            tempFile,
 | 
					            tempFile,
 | 
				
			||||||
            changed: tempFile,
 | 
					            changed: tempFile,
 | 
				
			||||||
            opened: tempFile,
 | 
					            opened: tempFile,
 | 
				
			||||||
| 
						 | 
					@ -70,13 +60,12 @@ self.addEventListener('message', e => {
 | 
				
			||||||
        path,
 | 
					        path,
 | 
				
			||||||
        url: `/${projectId}/blob/${branchId}/${path}`,
 | 
					        url: `/${projectId}/blob/${branchId}/${path}`,
 | 
				
			||||||
        type: 'blob',
 | 
					        type: 'blob',
 | 
				
			||||||
        parentTreeUrl: fileFolder
 | 
					        parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
 | 
				
			||||||
          ? fileFolder.url
 | 
					 | 
				
			||||||
          : `/${projectId}/blob/${branchId}`,
 | 
					 | 
				
			||||||
        tempFile,
 | 
					        tempFile,
 | 
				
			||||||
        changed: tempFile,
 | 
					        changed: tempFile,
 | 
				
			||||||
        content,
 | 
					        content,
 | 
				
			||||||
        base64,
 | 
					        base64,
 | 
				
			||||||
 | 
					        previewMode: viewerInformationForPath(blobName),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Object.assign(acc, {
 | 
					      Object.assign(acc, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@
 | 
				
			||||||
        return `#${this.job.runner.id}`;
 | 
					        return `#${this.job.runner.id}`;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      hasTimeout() {
 | 
					      hasTimeout() {
 | 
				
			||||||
        return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
 | 
					        return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      timeout() {
 | 
					      timeout() {
 | 
				
			||||||
        if (this.job.metadata == null) {
 | 
					        if (this.job.metadata == null) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
 | 
				
			||||||
import DropdownUtils from './filtered_search/dropdown_utils';
 | 
					import DropdownUtils from './filtered_search/dropdown_utils';
 | 
				
			||||||
import CreateLabelDropdown from './create_label';
 | 
					import CreateLabelDropdown from './create_label';
 | 
				
			||||||
import flash from './flash';
 | 
					import flash from './flash';
 | 
				
			||||||
 | 
					import ModalStore from './boards/stores/modal_store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class LabelsSelect {
 | 
					export default class LabelsSelect {
 | 
				
			||||||
  constructor(els, options = {}) {
 | 
					  constructor(els, options = {}) {
 | 
				
			||||||
| 
						 | 
					@ -350,7 +351,7 @@ export default class LabelsSelect {
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if ($dropdown.closest('.add-issues-modal').length) {
 | 
					          if ($dropdown.closest('.add-issues-modal').length) {
 | 
				
			||||||
            boardsModel = gl.issueBoards.ModalStore.store.filter;
 | 
					            boardsModel = ModalStore.store.filter;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (boardsModel) {
 | 
					          if (boardsModel) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
 | 
					export const isInIssuePage = () => checkPageAndAction('issues', 'show');
 | 
				
			||||||
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
 | 
					export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
 | 
				
			||||||
 | 
					export const isInEpicPage = () => checkPageAndAction('epics', 'show');
 | 
				
			||||||
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
 | 
					export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
 | 
				
			||||||
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
 | 
					export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,12 @@
 | 
				
			||||||
/* eslint-disable import/prefer-default-export */
 | 
					import $ from 'jquery';
 | 
				
			||||||
 | 
					import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const addClassIfElementExists = (element, className) => {
 | 
					export const addClassIfElementExists = (element, className) => {
 | 
				
			||||||
  if (element) {
 | 
					  if (element) {
 | 
				
			||||||
    element.classList.add(className);
 | 
					    element.classList.add(className);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,8 @@
 | 
				
			||||||
 * @param {String} text
 | 
					 * @param {String} text
 | 
				
			||||||
 * @returns {String}
 | 
					 * @returns {String}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
 | 
					export const addDelimiter = text =>
 | 
				
			||||||
 | 
					  (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Returns '99+' for numbers bigger than 99.
 | 
					 * Returns '99+' for numbers bigger than 99.
 | 
				
			||||||
| 
						 | 
					@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count);
 | 
				
			||||||
 * @param {String} string
 | 
					 * @param {String} string
 | 
				
			||||||
 * @requires {String}
 | 
					 * @requires {String}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
 | 
					export const humanize = string =>
 | 
				
			||||||
 | 
					  string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Adds an 's' to the end of the string when count is bigger than 0
 | 
					 * Adds an 's' to the end of the string when count is bigger than 0
 | 
				
			||||||
| 
						 | 
					@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase();
 | 
				
			||||||
 * @param {Number} maxLength
 | 
					 * @param {Number} maxLength
 | 
				
			||||||
 * @returns {String}
 | 
					 * @returns {String}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
 | 
					export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Capitalizes first character
 | 
					 * Capitalizes first character
 | 
				
			||||||
| 
						 | 
					@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re
 | 
				
			||||||
 * @param {*} string
 | 
					 * @param {*} string
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
 | 
					export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Converts a sentence to lower case from the second word onwards
 | 
				
			||||||
 | 
					 * e.g. Hello World => Hello world
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param {*} string
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const convertToSentenceCase = string => {
 | 
				
			||||||
 | 
					  const splitWord = string.split(' ').map((word, index) => (index > 0 ? word.toLowerCase() : word));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return splitWord.join(' ');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,11 +7,7 @@ import flash from './flash';
 | 
				
			||||||
import BlobForkSuggestion from './blob/blob_fork_suggestion';
 | 
					import BlobForkSuggestion from './blob/blob_fork_suggestion';
 | 
				
			||||||
import initChangesDropdown from './init_changes_dropdown';
 | 
					import initChangesDropdown from './init_changes_dropdown';
 | 
				
			||||||
import bp from './breakpoints';
 | 
					import bp from './breakpoints';
 | 
				
			||||||
import {
 | 
					import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
 | 
				
			||||||
  parseUrlPathname,
 | 
					 | 
				
			||||||
  handleLocationHash,
 | 
					 | 
				
			||||||
  isMetaClick,
 | 
					 | 
				
			||||||
} from './lib/utils/common_utils';
 | 
					 | 
				
			||||||
import { getLocationHash } from './lib/utils/url_utility';
 | 
					import { getLocationHash } from './lib/utils/url_utility';
 | 
				
			||||||
import initDiscussionTab from './image_diff/init_discussion_tab';
 | 
					import initDiscussionTab from './image_diff/init_discussion_tab';
 | 
				
			||||||
import Diff from './diff';
 | 
					import Diff from './diff';
 | 
				
			||||||
| 
						 | 
					@ -69,11 +65,10 @@ import Notes from './notes';
 | 
				
			||||||
let location = window.location;
 | 
					let location = window.location;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class MergeRequestTabs {
 | 
					export default class MergeRequestTabs {
 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor({ action, setUrl, stubLocation } = {}) {
 | 
					  constructor({ action, setUrl, stubLocation } = {}) {
 | 
				
			||||||
    const mergeRequestTabs = document.querySelector('.js-tabs-affix');
 | 
					    const mergeRequestTabs = document.querySelector('.js-tabs-affix');
 | 
				
			||||||
    const navbar = document.querySelector('.navbar-gitlab');
 | 
					    const navbar = document.querySelector('.navbar-gitlab');
 | 
				
			||||||
    const peek = document.getElementById('peek');
 | 
					    const peek = document.getElementById('js-peek');
 | 
				
			||||||
    const paddingTop = 16;
 | 
					    const paddingTop = 16;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.diffsLoaded = false;
 | 
					    this.diffsLoaded = false;
 | 
				
			||||||
| 
						 | 
					@ -109,8 +104,7 @@ export default class MergeRequestTabs {
 | 
				
			||||||
      .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
 | 
					      .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
 | 
				
			||||||
      .on('click', '.js-show-tab', this.showTab);
 | 
					      .on('click', '.js-show-tab', this.showTab);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('.merge-request-tabs a[data-toggle="tab"]')
 | 
					    $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
 | 
				
			||||||
      .on('click', this.clickTab);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Used in tests
 | 
					  // Used in tests
 | 
				
			||||||
| 
						 | 
					@ -119,8 +113,7 @@ export default class MergeRequestTabs {
 | 
				
			||||||
      .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
 | 
					      .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
 | 
				
			||||||
      .off('click', '.js-show-tab', this.showTab);
 | 
					      .off('click', '.js-show-tab', this.showTab);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('.merge-request-tabs a[data-toggle="tab"]')
 | 
					    $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
 | 
				
			||||||
      .off('click', this.clickTab);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  destroyPipelinesView() {
 | 
					  destroyPipelinesView() {
 | 
				
			||||||
| 
						 | 
					@ -183,10 +176,7 @@ export default class MergeRequestTabs {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scrollToElement(container) {
 | 
					  scrollToElement(container) {
 | 
				
			||||||
    if (location.hash) {
 | 
					    if (location.hash) {
 | 
				
			||||||
      const offset = 0 - (
 | 
					      const offset = 0 - ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight());
 | 
				
			||||||
        $('.navbar-gitlab').outerHeight() +
 | 
					 | 
				
			||||||
        $('.js-tabs-affix').outerHeight()
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      const $el = $(`${container} ${location.hash}:not(.match)`);
 | 
					      const $el = $(`${container} ${location.hash}:not(.match)`);
 | 
				
			||||||
      if ($el.length) {
 | 
					      if ($el.length) {
 | 
				
			||||||
        $.scrollTo($el[0], { offset });
 | 
					        $.scrollTo($el[0], { offset });
 | 
				
			||||||
| 
						 | 
					@ -240,9 +230,13 @@ export default class MergeRequestTabs {
 | 
				
			||||||
    // Turbolinks' history.
 | 
					    // Turbolinks' history.
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    // See https://github.com/rails/turbolinks/issues/363
 | 
					    // See https://github.com/rails/turbolinks/issues/363
 | 
				
			||||||
    window.history.replaceState({
 | 
					    window.history.replaceState(
 | 
				
			||||||
      url: newState,
 | 
					      {
 | 
				
			||||||
    }, document.title, newState);
 | 
					        url: newState,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      document.title,
 | 
				
			||||||
 | 
					      newState,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return newState;
 | 
					    return newState;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -258,7 +252,8 @@ export default class MergeRequestTabs {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.toggleLoading(true);
 | 
					    this.toggleLoading(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    axios.get(`${source}.json`)
 | 
					    axios
 | 
				
			||||||
 | 
					      .get(`${source}.json`)
 | 
				
			||||||
      .then(({ data }) => {
 | 
					      .then(({ data }) => {
 | 
				
			||||||
        document.querySelector('div#commits').innerHTML = data.html;
 | 
					        document.querySelector('div#commits').innerHTML = data.html;
 | 
				
			||||||
        localTimeAgo($('.js-timeago', 'div#commits'));
 | 
					        localTimeAgo($('.js-timeago', 'div#commits'));
 | 
				
			||||||
| 
						 | 
					@ -303,7 +298,8 @@ export default class MergeRequestTabs {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.toggleLoading(true);
 | 
					    this.toggleLoading(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    axios.get(`${urlPathname}.json${location.search}`)
 | 
					    axios
 | 
				
			||||||
 | 
					      .get(`${urlPathname}.json${location.search}`)
 | 
				
			||||||
      .then(({ data }) => {
 | 
					      .then(({ data }) => {
 | 
				
			||||||
        const $container = $('#diffs');
 | 
					        const $container = $('#diffs');
 | 
				
			||||||
        $container.html(data.html);
 | 
					        $container.html(data.html);
 | 
				
			||||||
| 
						 | 
					@ -332,8 +328,7 @@ export default class MergeRequestTabs {
 | 
				
			||||||
            cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
 | 
					            cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'),
 | 
				
			||||||
            suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
 | 
					            suggestionSections: $(el).find('.js-file-fork-suggestion-section'),
 | 
				
			||||||
            actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
 | 
					            actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'),
 | 
				
			||||||
          })
 | 
					          }).init();
 | 
				
			||||||
            .init();
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Scroll any linked note into view
 | 
					        // Scroll any linked note into view
 | 
				
			||||||
| 
						 | 
					@ -388,8 +383,7 @@ export default class MergeRequestTabs {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  resetViewContainer() {
 | 
					  resetViewContainer() {
 | 
				
			||||||
    if (this.fixedLayoutPref !== null) {
 | 
					    if (this.fixedLayoutPref !== null) {
 | 
				
			||||||
      $('.content-wrapper .container-fluid')
 | 
					      $('.content-wrapper .container-fluid').toggleClass('container-limited', this.fixedLayoutPref);
 | 
				
			||||||
        .toggleClass('container-limited', this.fixedLayoutPref);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -438,12 +432,11 @@ export default class MergeRequestTabs {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const $diffTabs = $('#diff-notes-app');
 | 
					    const $diffTabs = $('#diff-notes-app');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $tabs.off('affix.bs.affix affix-top.bs.affix')
 | 
					    $tabs
 | 
				
			||||||
 | 
					      .off('affix.bs.affix affix-top.bs.affix')
 | 
				
			||||||
      .affix({
 | 
					      .affix({
 | 
				
			||||||
        offset: {
 | 
					        offset: {
 | 
				
			||||||
          top: () => (
 | 
					          top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
 | 
				
			||||||
            $diffTabs.offset().top - $tabs.height() - $fixedNav.height()
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
 | 
					      .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import $ from 'jquery';
 | 
					import $ from 'jquery';
 | 
				
			||||||
import axios from './lib/utils/axios_utils';
 | 
					import axios from './lib/utils/axios_utils';
 | 
				
			||||||
import flash from './flash';
 | 
					import flash from './flash';
 | 
				
			||||||
 | 
					import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Milestone {
 | 
					export default class Milestone {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
| 
						 | 
					@ -43,4 +44,25 @@ export default class Milestone {
 | 
				
			||||||
        .catch(() => flash('Error loading milestone tab'));
 | 
					        .catch(() => flash('Error loading milestone tab'));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static initDeprecationMessage() {
 | 
				
			||||||
 | 
					    const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!deprecationMesssageContainer) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML;
 | 
				
			||||||
 | 
					    const $popover = $('.js-popover-link', deprecationMesssageContainer);
 | 
				
			||||||
 | 
					    const hideOnScroll = togglePopover.bind($popover, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $popover.popover({
 | 
				
			||||||
 | 
					      content: deprecationMessage,
 | 
				
			||||||
 | 
					      html: true,
 | 
				
			||||||
 | 
					      placement: 'bottom',
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .on('mouseenter', mouseenter)
 | 
				
			||||||
 | 
					    .on('mouseleave', debouncedMouseleave())
 | 
				
			||||||
 | 
					    .on('show.bs.popover', () => {
 | 
				
			||||||
 | 
					      window.addEventListener('scroll', hideOnScroll, { once: true });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import $ from 'jquery';
 | 
				
			||||||
import _ from 'underscore';
 | 
					import _ from 'underscore';
 | 
				
			||||||
import axios from './lib/utils/axios_utils';
 | 
					import axios from './lib/utils/axios_utils';
 | 
				
			||||||
import { timeFor } from './lib/utils/datetime_utility';
 | 
					import { timeFor } from './lib/utils/datetime_utility';
 | 
				
			||||||
 | 
					import ModalStore from './boards/stores/modal_store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class MilestoneSelect {
 | 
					export default class MilestoneSelect {
 | 
				
			||||||
  constructor(currentProject, els, options = {}) {
 | 
					  constructor(currentProject, els, options = {}) {
 | 
				
			||||||
| 
						 | 
					@ -94,10 +95,10 @@ export default class MilestoneSelect {
 | 
				
			||||||
            if (showMenuAbove) {
 | 
					            if (showMenuAbove) {
 | 
				
			||||||
              $dropdown.data('glDropdown').positionMenuAbove();
 | 
					              $dropdown.data('glDropdown').positionMenuAbove();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
 | 
					            $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
        renderRow: milestone => `
 | 
					        renderRow: milestone => `
 | 
				
			||||||
          <li data-milestone-id="${milestone.name}">
 | 
					          <li data-milestone-id="${_.escape(milestone.name)}">
 | 
				
			||||||
            <a href='#' class='dropdown-menu-milestone-link'>
 | 
					            <a href='#' class='dropdown-menu-milestone-link'>
 | 
				
			||||||
              ${_.escape(milestone.title)}
 | 
					              ${_.escape(milestone.title)}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
| 
						 | 
					@ -125,7 +126,6 @@ export default class MilestoneSelect {
 | 
				
			||||||
            return milestone.id;
 | 
					            return milestone.id;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        isSelected: milestone => milestone.name === selectedMilestone,
 | 
					 | 
				
			||||||
        hidden: () => {
 | 
					        hidden: () => {
 | 
				
			||||||
          $selectBox.hide();
 | 
					          $selectBox.hide();
 | 
				
			||||||
          // display:block overrides the hide-collapse rule
 | 
					          // display:block overrides the hide-collapse rule
 | 
				
			||||||
| 
						 | 
					@ -137,7 +137,7 @@ export default class MilestoneSelect {
 | 
				
			||||||
            selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
 | 
					            selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          $('a.is-active', $el).removeClass('is-active');
 | 
					          $('a.is-active', $el).removeClass('is-active');
 | 
				
			||||||
          $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
 | 
					          $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        vue: $dropdown.hasClass('js-issue-board-sidebar'),
 | 
					        vue: $dropdown.hasClass('js-issue-board-sidebar'),
 | 
				
			||||||
        clicked: (clickEvent) => {
 | 
					        clicked: (clickEvent) => {
 | 
				
			||||||
| 
						 | 
					@ -158,13 +158,14 @@ export default class MilestoneSelect {
 | 
				
			||||||
          const isMRIndex = (page === page && page === 'projects:merge_requests:index');
 | 
					          const isMRIndex = (page === page && page === 'projects:merge_requests:index');
 | 
				
			||||||
          const isSelecting = (selected.name !== selectedMilestone);
 | 
					          const isSelecting = (selected.name !== selectedMilestone);
 | 
				
			||||||
          selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
 | 
					          selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
 | 
					          if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
 | 
				
			||||||
            e.preventDefault();
 | 
					            e.preventDefault();
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if ($dropdown.closest('.add-issues-modal').length) {
 | 
					          if ($dropdown.closest('.add-issues-modal').length) {
 | 
				
			||||||
            boardsStore = gl.issueBoards.ModalStore.store.filter;
 | 
					            boardsStore = ModalStore.store.filter;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (boardsStore) {
 | 
					          if (boardsStore) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,10 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { scaleLinear, scaleTime } from 'd3-scale';
 | 
					import { scaleLinear, scaleTime } from 'd3-scale';
 | 
				
			||||||
import { axisLeft, axisBottom } from 'd3-axis';
 | 
					import { axisLeft, axisBottom } from 'd3-axis';
 | 
				
			||||||
 | 
					import _ from 'underscore';
 | 
				
			||||||
import { max, extent } from 'd3-array';
 | 
					import { max, extent } from 'd3-array';
 | 
				
			||||||
import { select } from 'd3-selection';
 | 
					import { select } from 'd3-selection';
 | 
				
			||||||
 | 
					import GraphAxis from './graph/axis.vue';
 | 
				
			||||||
import GraphLegend from './graph/legend.vue';
 | 
					import GraphLegend from './graph/legend.vue';
 | 
				
			||||||
import GraphFlag from './graph/flag.vue';
 | 
					import GraphFlag from './graph/flag.vue';
 | 
				
			||||||
import GraphDeployment from './graph/deployment.vue';
 | 
					import GraphDeployment from './graph/deployment.vue';
 | 
				
			||||||
| 
						 | 
					@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    GraphLegend,
 | 
					    GraphAxis,
 | 
				
			||||||
    GraphFlag,
 | 
					    GraphFlag,
 | 
				
			||||||
    GraphDeployment,
 | 
					    GraphDeployment,
 | 
				
			||||||
    GraphPath,
 | 
					    GraphPath,
 | 
				
			||||||
 | 
					    GraphLegend,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mixins: [MonitoringMixin],
 | 
					  mixins: [MonitoringMixin],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
| 
						 | 
					@ -138,7 +141,7 @@ export default {
 | 
				
			||||||
      this.legendTitle = query.label || 'Average';
 | 
					      this.legendTitle = query.label || 'Average';
 | 
				
			||||||
      this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
 | 
					      this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
 | 
				
			||||||
      this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
 | 
					      this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
 | 
				
			||||||
      this.baseGraphHeight = this.graphHeight;
 | 
					      this.baseGraphHeight = this.graphHeight - 50;
 | 
				
			||||||
      this.baseGraphWidth = this.graphWidth;
 | 
					      this.baseGraphWidth = this.graphWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // pixel offsets inside the svg and outside are not 1:1
 | 
					      // pixel offsets inside the svg and outside are not 1:1
 | 
				
			||||||
| 
						 | 
					@ -177,10 +180,8 @@ export default {
 | 
				
			||||||
        this.graphHeightOffset,
 | 
					        this.graphHeightOffset,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!this.showLegend) {
 | 
					      if (_.findWhere(this.timeSeries, { renderCanary: true })) {
 | 
				
			||||||
        this.baseGraphHeight -= 50;
 | 
					        this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
 | 
				
			||||||
      } else if (this.timeSeries.length > 3) {
 | 
					 | 
				
			||||||
        this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
 | 
					      const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
 | 
				
			||||||
| 
						 | 
					@ -251,17 +252,13 @@ export default {
 | 
				
			||||||
          class="y-axis"
 | 
					          class="y-axis"
 | 
				
			||||||
          transform="translate(70, 20)"
 | 
					          transform="translate(70, 20)"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <graph-legend
 | 
					        <graph-axis
 | 
				
			||||||
          :graph-width="graphWidth"
 | 
					          :graph-width="graphWidth"
 | 
				
			||||||
          :graph-height="graphHeight"
 | 
					          :graph-height="graphHeight"
 | 
				
			||||||
          :margin="margin"
 | 
					          :margin="margin"
 | 
				
			||||||
          :measurements="measurements"
 | 
					          :measurements="measurements"
 | 
				
			||||||
          :legend-title="legendTitle"
 | 
					 | 
				
			||||||
          :y-axis-label="yAxisLabel"
 | 
					          :y-axis-label="yAxisLabel"
 | 
				
			||||||
          :time-series="timeSeries"
 | 
					 | 
				
			||||||
          :unit-of-display="unitOfDisplay"
 | 
					          :unit-of-display="unitOfDisplay"
 | 
				
			||||||
          :current-data-index="currentDataIndex"
 | 
					 | 
				
			||||||
          :show-legend-group="showLegend"
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <svg
 | 
					        <svg
 | 
				
			||||||
          class="graph-data"
 | 
					          class="graph-data"
 | 
				
			||||||
| 
						 | 
					@ -306,5 +303,10 @@ export default {
 | 
				
			||||||
        :deployment-flag-data="deploymentFlagData"
 | 
					        :deployment-flag-data="deploymentFlagData"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <graph-legend
 | 
				
			||||||
 | 
					      v-if="showLegend"
 | 
				
			||||||
 | 
					      :legend-title="legendTitle"
 | 
				
			||||||
 | 
					      :time-series="timeSeries"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,142 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { convertToSentenceCase } from '~/lib/utils/text_utility';
 | 
				
			||||||
 | 
					import { s__ } from '~/locale';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    graphWidth: {
 | 
				
			||||||
 | 
					      type: Number,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    graphHeight: {
 | 
				
			||||||
 | 
					      type: Number,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    margin: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    measurements: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    yAxisLabel: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    unitOfDisplay: {
 | 
				
			||||||
 | 
					      type: String,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      yLabelWidth: 0,
 | 
				
			||||||
 | 
					      yLabelHeight: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    textTransform() {
 | 
				
			||||||
 | 
					      const yCoordinate =
 | 
				
			||||||
 | 
					        (this.graphHeight -
 | 
				
			||||||
 | 
					          this.margin.top +
 | 
				
			||||||
 | 
					          this.measurements.axisLabelLineOffset) /
 | 
				
			||||||
 | 
					          2 || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return `translate(15, ${yCoordinate}) rotate(-90)`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rectTransform() {
 | 
				
			||||||
 | 
					      const yCoordinate =
 | 
				
			||||||
 | 
					        (this.graphHeight -
 | 
				
			||||||
 | 
					          this.margin.top +
 | 
				
			||||||
 | 
					          this.measurements.axisLabelLineOffset) /
 | 
				
			||||||
 | 
					          2 +
 | 
				
			||||||
 | 
					          this.yLabelWidth / 2 || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return `translate(0, ${yCoordinate}) rotate(-90)`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    xPosition() {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 -
 | 
				
			||||||
 | 
					          this.margin.right || 0
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    yPosition() {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        this.graphHeight -
 | 
				
			||||||
 | 
					          this.margin.top +
 | 
				
			||||||
 | 
					          this.measurements.axisLabelLineOffset || 0
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    yAxisLabelSentenceCase() {
 | 
				
			||||||
 | 
					      return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    timeString() {
 | 
				
			||||||
 | 
					      return s__('PrometheusDashboard|Time');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.$nextTick(() => {
 | 
				
			||||||
 | 
					      const bbox = this.$refs.ylabel.getBBox();
 | 
				
			||||||
 | 
					      this.yLabelWidth = bbox.width + 10; // Added some padding
 | 
				
			||||||
 | 
					      this.yLabelHeight = bbox.height + 5;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <g class="axis-label-container">
 | 
				
			||||||
 | 
					    <line
 | 
				
			||||||
 | 
					      class="label-x-axis-line"
 | 
				
			||||||
 | 
					      stroke="#000000"
 | 
				
			||||||
 | 
					      stroke-width="1"
 | 
				
			||||||
 | 
					      x1="10"
 | 
				
			||||||
 | 
					      :y1="yPosition"
 | 
				
			||||||
 | 
					      :x2="graphWidth + 20"
 | 
				
			||||||
 | 
					      :y2="yPosition"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <line
 | 
				
			||||||
 | 
					      class="label-y-axis-line"
 | 
				
			||||||
 | 
					      stroke="#000000"
 | 
				
			||||||
 | 
					      stroke-width="1"
 | 
				
			||||||
 | 
					      x1="10"
 | 
				
			||||||
 | 
					      y1="0"
 | 
				
			||||||
 | 
					      :x2="10"
 | 
				
			||||||
 | 
					      :y2="yPosition"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <rect
 | 
				
			||||||
 | 
					      class="rect-axis-text"
 | 
				
			||||||
 | 
					      :transform="rectTransform"
 | 
				
			||||||
 | 
					      :width="yLabelWidth"
 | 
				
			||||||
 | 
					      :height="yLabelHeight"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <text
 | 
				
			||||||
 | 
					      class="label-axis-text y-label-text"
 | 
				
			||||||
 | 
					      text-anchor="middle"
 | 
				
			||||||
 | 
					      :transform="textTransform"
 | 
				
			||||||
 | 
					      ref="ylabel"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {{ yAxisLabelSentenceCase }}
 | 
				
			||||||
 | 
					    </text>
 | 
				
			||||||
 | 
					    <rect
 | 
				
			||||||
 | 
					      class="rect-axis-text"
 | 
				
			||||||
 | 
					      :x="xPosition + 60"
 | 
				
			||||||
 | 
					      :y="graphHeight - 80"
 | 
				
			||||||
 | 
					      width="35"
 | 
				
			||||||
 | 
					      height="50"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <text
 | 
				
			||||||
 | 
					      class="label-axis-text x-label-text"
 | 
				
			||||||
 | 
					      :x="xPosition + 60"
 | 
				
			||||||
 | 
					      :y="yPosition"
 | 
				
			||||||
 | 
					      dy=".35em"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {{ timeString }}
 | 
				
			||||||
 | 
					    </text>
 | 
				
			||||||
 | 
					  </g>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,13 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
 | 
					import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
 | 
				
			||||||
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
 | 
					import { formatRelevantDigits } from '../../../lib/utils/number_utils';
 | 
				
			||||||
import icon from '../../../vue_shared/components/icon.vue';
 | 
					import Icon from '../../../vue_shared/components/icon.vue';
 | 
				
			||||||
 | 
					import TrackLine from './track_line.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    icon,
 | 
					    Icon,
 | 
				
			||||||
 | 
					    TrackLine,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    currentXCoordinate: {
 | 
					    currentXCoordinate: {
 | 
				
			||||||
| 
						 | 
					@ -107,11 +109,6 @@ export default {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return `series ${index + 1}`;
 | 
					      return `series ${index + 1}`;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    strokeDashArray(type) {
 | 
					 | 
				
			||||||
      if (type === 'dashed') return '6, 3';
 | 
					 | 
				
			||||||
      if (type === 'dotted') return '3, 3';
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -160,28 +157,13 @@ export default {
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="popover-content">
 | 
					      <div class="popover-content">
 | 
				
			||||||
        <table>
 | 
					        <table class="prometheus-table">
 | 
				
			||||||
          <tr
 | 
					          <tr
 | 
				
			||||||
            v-for="(series, index) in timeSeries"
 | 
					            v-for="(series, index) in timeSeries"
 | 
				
			||||||
            :key="index"
 | 
					            :key="index"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <td>
 | 
					            <track-line :track="series"/>
 | 
				
			||||||
              <svg
 | 
					            <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
 | 
				
			||||||
                width="15"
 | 
					 | 
				
			||||||
                height="6"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <line
 | 
					 | 
				
			||||||
                  :stroke="series.lineColor"
 | 
					 | 
				
			||||||
                  :stroke-dasharray="strokeDashArray(series.lineStyle)"
 | 
					 | 
				
			||||||
                  stroke-width="4"
 | 
					 | 
				
			||||||
                  x1="0"
 | 
					 | 
				
			||||||
                  x2="15"
 | 
					 | 
				
			||||||
                  y1="2"
 | 
					 | 
				
			||||||
                  y2="2"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </svg>
 | 
					 | 
				
			||||||
            </td>
 | 
					 | 
				
			||||||
            <td>{{ seriesMetricLabel(index, series) }}</td>
 | 
					 | 
				
			||||||
            <td>
 | 
					            <td>
 | 
				
			||||||
              <strong>{{ seriesMetricValue(series) }}</strong>
 | 
					              <strong>{{ seriesMetricValue(series) }}</strong>
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,204 +1,72 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
 | 
					import TrackLine from './track_line.vue';
 | 
				
			||||||
 | 
					import TrackInfo from './track_info.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    TrackLine,
 | 
				
			||||||
 | 
					    TrackInfo,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    graphWidth: {
 | 
					 | 
				
			||||||
      type: Number,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    graphHeight: {
 | 
					 | 
				
			||||||
      type: Number,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    margin: {
 | 
					 | 
				
			||||||
      type: Object,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    measurements: {
 | 
					 | 
				
			||||||
      type: Object,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    legendTitle: {
 | 
					    legendTitle: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    yAxisLabel: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    timeSeries: {
 | 
					    timeSeries: {
 | 
				
			||||||
      type: Array,
 | 
					      type: Array,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    unitOfDisplay: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    currentDataIndex: {
 | 
					 | 
				
			||||||
      type: Number,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    showLegendGroup: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      required: false,
 | 
					 | 
				
			||||||
      default: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      yLabelWidth: 0,
 | 
					 | 
				
			||||||
      yLabelHeight: 0,
 | 
					 | 
				
			||||||
      seriesXPosition: 0,
 | 
					 | 
				
			||||||
      metricUsageXPosition: 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    textTransform() {
 | 
					 | 
				
			||||||
      const yCoordinate =
 | 
					 | 
				
			||||||
        (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return `translate(15, ${yCoordinate}) rotate(-90)`;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    rectTransform() {
 | 
					 | 
				
			||||||
      const yCoordinate =
 | 
					 | 
				
			||||||
        (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
 | 
					 | 
				
			||||||
          this.yLabelWidth / 2 || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return `translate(0, ${yCoordinate}) rotate(-90)`;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    xPosition() {
 | 
					 | 
				
			||||||
      return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    yPosition() {
 | 
					 | 
				
			||||||
      return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  mounted() {
 | 
					 | 
				
			||||||
    this.$nextTick(() => {
 | 
					 | 
				
			||||||
      const bbox = this.$refs.ylabel.getBBox();
 | 
					 | 
				
			||||||
      this.metricUsageXPosition = 0;
 | 
					 | 
				
			||||||
      this.seriesXPosition = 0;
 | 
					 | 
				
			||||||
      if (this.$refs.legendTitleSvg != null) {
 | 
					 | 
				
			||||||
        this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (this.$refs.seriesTitleSvg != null) {
 | 
					 | 
				
			||||||
        this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.yLabelWidth = bbox.width + 10; // Added some padding
 | 
					 | 
				
			||||||
      this.yLabelHeight = bbox.height + 5;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    translateLegendGroup(index) {
 | 
					    isStable(track) {
 | 
				
			||||||
      return `translate(0, ${12 * index})`;
 | 
					      return {
 | 
				
			||||||
    },
 | 
					        'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
 | 
				
			||||||
    formatMetricUsage(series) {
 | 
					      };
 | 
				
			||||||
      const value =
 | 
					 | 
				
			||||||
        series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
 | 
					 | 
				
			||||||
      if (isNaN(value)) {
 | 
					 | 
				
			||||||
        return '-';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    createSeriesString(index, series) {
 | 
					 | 
				
			||||||
      if (series.metricTag) {
 | 
					 | 
				
			||||||
        return `${series.metricTag} ${this.formatMetricUsage(series)}`;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    strokeDashArray(type) {
 | 
					 | 
				
			||||||
      if (type === 'dashed') return '6, 3';
 | 
					 | 
				
			||||||
      if (type === 'dotted') return '3, 3';
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <g class="axis-label-container">
 | 
					  <div class="prometheus-graph-legends prepend-left-10">
 | 
				
			||||||
    <line
 | 
					    <table class="prometheus-table">
 | 
				
			||||||
      class="label-x-axis-line"
 | 
					      <tr
 | 
				
			||||||
      stroke="#000000"
 | 
					 | 
				
			||||||
      stroke-width="1"
 | 
					 | 
				
			||||||
      x1="10"
 | 
					 | 
				
			||||||
      :y1="yPosition"
 | 
					 | 
				
			||||||
      :x2="graphWidth + 20"
 | 
					 | 
				
			||||||
      :y2="yPosition"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <line
 | 
					 | 
				
			||||||
      class="label-y-axis-line"
 | 
					 | 
				
			||||||
      stroke="#000000"
 | 
					 | 
				
			||||||
      stroke-width="1"
 | 
					 | 
				
			||||||
      x1="10"
 | 
					 | 
				
			||||||
      y1="0"
 | 
					 | 
				
			||||||
      :x2="10"
 | 
					 | 
				
			||||||
      :y2="yPosition"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <rect
 | 
					 | 
				
			||||||
      class="rect-axis-text"
 | 
					 | 
				
			||||||
      :transform="rectTransform"
 | 
					 | 
				
			||||||
      :width="yLabelWidth"
 | 
					 | 
				
			||||||
      :height="yLabelHeight"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <text
 | 
					 | 
				
			||||||
      class="label-axis-text y-label-text"
 | 
					 | 
				
			||||||
      text-anchor="middle"
 | 
					 | 
				
			||||||
      :transform="textTransform"
 | 
					 | 
				
			||||||
      ref="ylabel"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      {{ yAxisLabel }}
 | 
					 | 
				
			||||||
    </text>
 | 
					 | 
				
			||||||
    <rect
 | 
					 | 
				
			||||||
      class="rect-axis-text"
 | 
					 | 
				
			||||||
      :x="xPosition + 60"
 | 
					 | 
				
			||||||
      :y="graphHeight - 80"
 | 
					 | 
				
			||||||
      width="35"
 | 
					 | 
				
			||||||
      height="50"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <text
 | 
					 | 
				
			||||||
      class="label-axis-text x-label-text"
 | 
					 | 
				
			||||||
      :x="xPosition + 60"
 | 
					 | 
				
			||||||
      :y="yPosition"
 | 
					 | 
				
			||||||
      dy=".35em"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      Time
 | 
					 | 
				
			||||||
    </text>
 | 
					 | 
				
			||||||
    <template v-if="showLegendGroup">
 | 
					 | 
				
			||||||
      <g
 | 
					 | 
				
			||||||
        class="legend-group"
 | 
					 | 
				
			||||||
        v-for="(series, index) in timeSeries"
 | 
					        v-for="(series, index) in timeSeries"
 | 
				
			||||||
        :key="index"
 | 
					        :key="index"
 | 
				
			||||||
        :transform="translateLegendGroup(index)"
 | 
					        v-if="series.shouldRenderLegend"
 | 
				
			||||||
 | 
					        :class="isStable(series)"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <line
 | 
					        <td>
 | 
				
			||||||
          :stroke="series.lineColor"
 | 
					          <strong v-if="series.renderCanary">{{ series.trackName }}</strong>
 | 
				
			||||||
          :stroke-width="measurements.legends.height"
 | 
					        </td>
 | 
				
			||||||
          :stroke-dasharray="strokeDashArray(series.lineStyle)"
 | 
					        <track-line :track="series" />
 | 
				
			||||||
          :x1="measurements.legends.offsetX"
 | 
					        <td
 | 
				
			||||||
          :x2="measurements.legends.offsetX + measurements.legends.width"
 | 
					 | 
				
			||||||
          :y1="graphHeight - measurements.legends.offsetY"
 | 
					 | 
				
			||||||
          :y2="graphHeight - measurements.legends.offsetY"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <text
 | 
					 | 
				
			||||||
          v-if="timeSeries.length > 1"
 | 
					 | 
				
			||||||
          class="legend-metric-title"
 | 
					          class="legend-metric-title"
 | 
				
			||||||
          ref="legendTitleSvg"
 | 
					          v-if="timeSeries.length > 1">
 | 
				
			||||||
          x="38"
 | 
					          <track-info
 | 
				
			||||||
          :y="graphHeight - 30"
 | 
					            :track="series"
 | 
				
			||||||
        >
 | 
					            v-if="series.metricTag" />
 | 
				
			||||||
          {{ createSeriesString(index, series) }}
 | 
					          <track-info
 | 
				
			||||||
        </text>
 | 
					            v-else
 | 
				
			||||||
        <text
 | 
					            :track="series">
 | 
				
			||||||
          v-else
 | 
					            <strong>{{ legendTitle }}</strong> series {{ index + 1 }}
 | 
				
			||||||
          class="legend-metric-title"
 | 
					          </track-info>
 | 
				
			||||||
          ref="legendTitleSvg"
 | 
					        </td>
 | 
				
			||||||
          x="38"
 | 
					        <td v-else>
 | 
				
			||||||
          :y="graphHeight - 30"
 | 
					          <track-info :track="series">
 | 
				
			||||||
        >
 | 
					            <strong>{{ legendTitle }}</strong>
 | 
				
			||||||
          {{ legendTitle }} {{ formatMetricUsage(series) }}
 | 
					          </track-info>
 | 
				
			||||||
        </text>
 | 
					        </td>
 | 
				
			||||||
      </g>
 | 
					        <template v-for="(track, trackIndex) in series.tracksLegend">
 | 
				
			||||||
    </template>
 | 
					          <track-line
 | 
				
			||||||
  </g>
 | 
					            :track="track"
 | 
				
			||||||
 | 
					            :key="`track-line-${trackIndex}`"/>
 | 
				
			||||||
 | 
					          <td :key="`track-info-${trackIndex}`">
 | 
				
			||||||
 | 
					            <track-info
 | 
				
			||||||
 | 
					              class="legend-metric-title"
 | 
				
			||||||
 | 
					              :track="track" />
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import { formatRelevantDigits } from '~/lib/utils/number_utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'TrackInfo',
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    track: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    summaryMetrics() {
 | 
				
			||||||
 | 
					      return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
 | 
				
			||||||
 | 
					        this.track.max,
 | 
				
			||||||
 | 
					      )}`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <span>
 | 
				
			||||||
 | 
					    <slot>
 | 
				
			||||||
 | 
					      <strong> {{ track.metricTag }} </strong>
 | 
				
			||||||
 | 
					    </slot>
 | 
				
			||||||
 | 
					    {{ summaryMetrics }}
 | 
				
			||||||
 | 
					  </span>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'TrackLine',
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    track: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    stylizedLine() {
 | 
				
			||||||
 | 
					      if (this.track.lineStyle === 'dashed') return '6, 3';
 | 
				
			||||||
 | 
					      if (this.track.lineStyle === 'dotted') return '3, 3';
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <td>
 | 
				
			||||||
 | 
					    <svg
 | 
				
			||||||
 | 
					      width="15"
 | 
				
			||||||
 | 
					      height="6">
 | 
				
			||||||
 | 
					      <line
 | 
				
			||||||
 | 
					        :stroke-dasharray="stylizedLine"
 | 
				
			||||||
 | 
					        :stroke="track.lineColor"
 | 
				
			||||||
 | 
					        stroke-width="4"
 | 
				
			||||||
 | 
					        :x1="0"
 | 
				
			||||||
 | 
					        :x2="15"
 | 
				
			||||||
 | 
					        :y1="2"
 | 
				
			||||||
 | 
					        :y2="2"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  </td>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import _ from 'underscore';
 | 
					import _ from 'underscore';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function sortMetrics(metrics) {
 | 
					function sortMetrics(metrics) {
 | 
				
			||||||
  return _.chain(metrics).sortBy('weight').sortBy('title').value();
 | 
					  return _.chain(metrics).sortBy('title').sortBy('weight').value();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function normalizeMetrics(metrics) {
 | 
					function normalizeMetrics(metrics) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,21 @@
 | 
				
			||||||
import _ from 'underscore';
 | 
					import _ from 'underscore';
 | 
				
			||||||
import { scaleLinear, scaleTime } from 'd3-scale';
 | 
					import { scaleLinear, scaleTime } from 'd3-scale';
 | 
				
			||||||
import { line, area, curveLinear } from 'd3-shape';
 | 
					import { line, area, curveLinear } from 'd3-shape';
 | 
				
			||||||
import { extent, max } from 'd3-array';
 | 
					import { extent, max, sum } from 'd3-array';
 | 
				
			||||||
import { timeMinute } from 'd3-time';
 | 
					import { timeMinute } from 'd3-time';
 | 
				
			||||||
 | 
					import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
 | 
					const d3 = {
 | 
				
			||||||
 | 
					  scaleLinear,
 | 
				
			||||||
 | 
					  scaleTime,
 | 
				
			||||||
 | 
					  line,
 | 
				
			||||||
 | 
					  area,
 | 
				
			||||||
 | 
					  curveLinear,
 | 
				
			||||||
 | 
					  extent,
 | 
				
			||||||
 | 
					  max,
 | 
				
			||||||
 | 
					  timeMinute,
 | 
				
			||||||
 | 
					  sum,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultColorPalette = {
 | 
					const defaultColorPalette = {
 | 
				
			||||||
  blue: ['#1f78d1', '#8fbce8'],
 | 
					  blue: ['#1f78d1', '#8fbce8'],
 | 
				
			||||||
| 
						 | 
					@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
 | 
					function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
 | 
				
			||||||
  let usedColors = [];
 | 
					  let usedColors = [];
 | 
				
			||||||
 | 
					  let renderCanary = false;
 | 
				
			||||||
 | 
					  const timeSeriesParsed = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function pickColor(name) {
 | 
					  function pickColor(name) {
 | 
				
			||||||
    let pick;
 | 
					    let pick;
 | 
				
			||||||
| 
						 | 
					@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
 | 
				
			||||||
    return defaultColorPalette[pick];
 | 
					    return defaultColorPalette[pick];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return query.result.map((timeSeries, timeSeriesNumber) => {
 | 
					  query.result.forEach((timeSeries, timeSeriesNumber) => {
 | 
				
			||||||
    let metricTag = '';
 | 
					    let metricTag = '';
 | 
				
			||||||
    let lineColor = '';
 | 
					    let lineColor = '';
 | 
				
			||||||
    let areaColor = '';
 | 
					    let areaColor = '';
 | 
				
			||||||
 | 
					    let shouldRenderLegend = true;
 | 
				
			||||||
 | 
					    const timeSeriesValues = timeSeries.values.map(d => d.value);
 | 
				
			||||||
 | 
					    const maximumValue = d3.max(timeSeriesValues);
 | 
				
			||||||
 | 
					    const accum = d3.sum(timeSeriesValues);
 | 
				
			||||||
 | 
					    const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timeSeriesScaleX = d3.scaleTime()
 | 
					    if (trackName === 'Canary') {
 | 
				
			||||||
      .range([0, graphWidth - 70]);
 | 
					      renderCanary = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timeSeriesScaleY = d3.scaleLinear()
 | 
					    const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
 | 
				
			||||||
      .range([graphHeight - graphHeightOffset, 0]);
 | 
					
 | 
				
			||||||
 | 
					    const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timeSeriesScaleX.domain(xDom);
 | 
					    timeSeriesScaleX.domain(xDom);
 | 
				
			||||||
    timeSeriesScaleX.ticks(d3.timeMinute, 60);
 | 
					    timeSeriesScaleX.ticks(d3.timeMinute, 60);
 | 
				
			||||||
| 
						 | 
					@ -55,13 +75,15 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const defined = d => !isNaN(d.value) && d.value != null;
 | 
					    const defined = d => !isNaN(d.value) && d.value != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const lineFunction = d3.line()
 | 
					    const lineFunction = d3
 | 
				
			||||||
 | 
					      .line()
 | 
				
			||||||
      .defined(defined)
 | 
					      .defined(defined)
 | 
				
			||||||
      .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
 | 
					      .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
 | 
				
			||||||
      .x(d => timeSeriesScaleX(d.time))
 | 
					      .x(d => timeSeriesScaleX(d.time))
 | 
				
			||||||
      .y(d => timeSeriesScaleY(d.value));
 | 
					      .y(d => timeSeriesScaleY(d.value));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const areaFunction = d3.area()
 | 
					    const areaFunction = d3
 | 
				
			||||||
 | 
					      .area()
 | 
				
			||||||
      .defined(defined)
 | 
					      .defined(defined)
 | 
				
			||||||
      .curve(d3.curveLinear)
 | 
					      .curve(d3.curveLinear)
 | 
				
			||||||
      .x(d => timeSeriesScaleX(d.time))
 | 
					      .x(d => timeSeriesScaleX(d.time))
 | 
				
			||||||
| 
						 | 
					@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
 | 
				
			||||||
      .y1(d => timeSeriesScaleY(d.value));
 | 
					      .y1(d => timeSeriesScaleY(d.value));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
 | 
					    const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
 | 
				
			||||||
    const seriesCustomizationData = query.series != null &&
 | 
					    const seriesCustomizationData =
 | 
				
			||||||
      _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
 | 
					      query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (seriesCustomizationData) {
 | 
					    if (seriesCustomizationData) {
 | 
				
			||||||
      metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
 | 
					      metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
 | 
				
			||||||
      [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
 | 
					      [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
 | 
				
			||||||
 | 
					      shouldRenderLegend = false;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
 | 
					      metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
 | 
				
			||||||
      [lineColor, areaColor] = pickColor();
 | 
					      [lineColor, areaColor] = pickColor();
 | 
				
			||||||
 | 
					      if (timeSeriesParsed.length > 1) {
 | 
				
			||||||
 | 
					        shouldRenderLegend = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (query.track) {
 | 
					    if (!shouldRenderLegend) {
 | 
				
			||||||
      metricTag += ` - ${query.track}`;
 | 
					      if (!timeSeriesParsed[0].tracksLegend) {
 | 
				
			||||||
 | 
					        timeSeriesParsed[0].tracksLegend = [];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      timeSeriesParsed[0].tracksLegend.push({
 | 
				
			||||||
 | 
					        max: maximumValue,
 | 
				
			||||||
 | 
					        average: accum / timeSeries.values.length,
 | 
				
			||||||
 | 
					        lineStyle,
 | 
				
			||||||
 | 
					        lineColor,
 | 
				
			||||||
 | 
					        metricTag,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    timeSeriesParsed.push({
 | 
				
			||||||
      linePath: lineFunction(timeSeries.values),
 | 
					      linePath: lineFunction(timeSeries.values),
 | 
				
			||||||
      areaPath: areaFunction(timeSeries.values),
 | 
					      areaPath: areaFunction(timeSeries.values),
 | 
				
			||||||
      timeSeriesScaleX,
 | 
					      timeSeriesScaleX,
 | 
				
			||||||
      values: timeSeries.values,
 | 
					      values: timeSeries.values,
 | 
				
			||||||
 | 
					      max: maximumValue,
 | 
				
			||||||
 | 
					      average: accum / timeSeries.values.length,
 | 
				
			||||||
      lineStyle,
 | 
					      lineStyle,
 | 
				
			||||||
      lineColor,
 | 
					      lineColor,
 | 
				
			||||||
      areaColor,
 | 
					      areaColor,
 | 
				
			||||||
      metricTag,
 | 
					      metricTag,
 | 
				
			||||||
    };
 | 
					      trackName,
 | 
				
			||||||
 | 
					      shouldRenderLegend,
 | 
				
			||||||
 | 
					      renderCanary,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return timeSeriesParsed;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
 | 
					export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
 | 
				
			||||||
  const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
 | 
					  const allValues = queries.reduce(
 | 
				
			||||||
    query.result.reduce((allResults, result) => allResults.concat(result.values), []),
 | 
					    (allQueryResults, query) =>
 | 
				
			||||||
  ), []);
 | 
					      allQueryResults.concat(
 | 
				
			||||||
 | 
					        query.result.reduce((allResults, result) => allResults.concat(result.values), []),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const xDom = d3.extent(allValues, d => d.time);
 | 
					  const xDom = d3.extent(allValues, d => d.time);
 | 
				
			||||||
  const yDom = [0, d3.max(allValues.map(d => d.value))];
 | 
					  const yDom = [0, d3.max(allValues.map(d => d.value))];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,8 +13,11 @@ export default function initMrNotes() {
 | 
				
			||||||
    data() {
 | 
					    data() {
 | 
				
			||||||
      const notesDataset = document.getElementById('js-vue-mr-discussions')
 | 
					      const notesDataset = document.getElementById('js-vue-mr-discussions')
 | 
				
			||||||
        .dataset;
 | 
					        .dataset;
 | 
				
			||||||
 | 
					      const noteableData = JSON.parse(notesDataset.noteableData);
 | 
				
			||||||
 | 
					      noteableData.noteableType = notesDataset.noteableType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        noteableData: JSON.parse(notesDataset.noteableData),
 | 
					        noteableData,
 | 
				
			||||||
        currentUserData: JSON.parse(notesDataset.currentUserData),
 | 
					        currentUserData: JSON.parse(notesDataset.currentUserData),
 | 
				
			||||||
        notesData: JSON.parse(notesDataset.notesData),
 | 
					        notesData: JSON.parse(notesDataset.notesData),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -99,6 +99,10 @@ export default {
 | 
				
			||||||
        'js-note-target-reopen': !this.isOpen,
 | 
					        'js-note-target-reopen': !this.isOpen,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    supportQuickActions() {
 | 
				
			||||||
 | 
					      // Disable quick actions support for Epics
 | 
				
			||||||
 | 
					      return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    markdownDocsPath() {
 | 
					    markdownDocsPath() {
 | 
				
			||||||
      return this.getNotesData.markdownDocsPath;
 | 
					      return this.getNotesData.markdownDocsPath;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -313,10 +317,10 @@ Please check your network connection and try again.`;
 | 
				
			||||||
    <note-signed-out-widget v-if="!isLoggedIn" />
 | 
					    <note-signed-out-widget v-if="!isLoggedIn" />
 | 
				
			||||||
    <discussion-locked-widget
 | 
					    <discussion-locked-widget
 | 
				
			||||||
      issuable-type="issue"
 | 
					      issuable-type="issue"
 | 
				
			||||||
      v-else-if="!canCreateNote"
 | 
					      v-else-if="isLocked(getNoteableData) && !canCreateNote"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <ul
 | 
					    <ul
 | 
				
			||||||
      v-else
 | 
					      v-else-if="canCreateNote"
 | 
				
			||||||
      class="notes notes-form timeline">
 | 
					      class="notes notes-form timeline">
 | 
				
			||||||
      <li class="timeline-entry">
 | 
					      <li class="timeline-entry">
 | 
				
			||||||
        <div class="timeline-entry-inner">
 | 
					        <div class="timeline-entry-inner">
 | 
				
			||||||
| 
						 | 
					@ -355,7 +359,7 @@ Please check your network connection and try again.`;
 | 
				
			||||||
                  name="note[note]"
 | 
					                  name="note[note]"
 | 
				
			||||||
                  class="note-textarea js-vue-comment-form
 | 
					                  class="note-textarea js-vue-comment-form
 | 
				
			||||||
js-gfm-input js-autosize markdown-area js-vue-textarea"
 | 
					js-gfm-input js-autosize markdown-area js-vue-textarea"
 | 
				
			||||||
                  data-supports-quick-actions="true"
 | 
					                  :data-supports-quick-actions="supportQuickActions"
 | 
				
			||||||
                  aria-label="Description"
 | 
					                  aria-label="Description"
 | 
				
			||||||
                  v-model="note"
 | 
					                  v-model="note"
 | 
				
			||||||
                  ref="textarea"
 | 
					                  ref="textarea"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,10 @@ export default {
 | 
				
			||||||
      type: Boolean,
 | 
					      type: Boolean,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    canAwardEmoji: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    canDelete: {
 | 
					    canDelete: {
 | 
				
			||||||
      type: Boolean,
 | 
					      type: Boolean,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
| 
						 | 
					@ -74,9 +78,6 @@ export default {
 | 
				
			||||||
    shouldShowActionsDropdown() {
 | 
					    shouldShowActionsDropdown() {
 | 
				
			||||||
      return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
 | 
					      return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    canAddAwardEmoji() {
 | 
					 | 
				
			||||||
      return this.currentUserId;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isAuthoredByCurrentUser() {
 | 
					    isAuthoredByCurrentUser() {
 | 
				
			||||||
      return this.authorId === this.currentUserId;
 | 
					      return this.authorId === this.currentUserId;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -149,7 +150,7 @@ export default {
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      v-if="canAddAwardEmoji"
 | 
					      v-if="canAwardEmoji"
 | 
				
			||||||
      class="note-actions-item">
 | 
					      class="note-actions-item">
 | 
				
			||||||
      <a
 | 
					      <a
 | 
				
			||||||
        v-tooltip
 | 
					        v-tooltip
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,10 @@ export default {
 | 
				
			||||||
      type: Number,
 | 
					      type: Number,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    canAwardEmoji: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    ...mapGetters(['getUserData']),
 | 
					    ...mapGetters(['getUserData']),
 | 
				
			||||||
| 
						 | 
					@ -67,9 +71,6 @@ export default {
 | 
				
			||||||
    isAuthoredByMe() {
 | 
					    isAuthoredByMe() {
 | 
				
			||||||
      return this.noteAuthorId === this.getUserData.id;
 | 
					      return this.noteAuthorId === this.getUserData.id;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isLoggedIn() {
 | 
					 | 
				
			||||||
      return this.getUserData.id;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  created() {
 | 
					  created() {
 | 
				
			||||||
    this.emojiSmiling = emojiSmiling;
 | 
					    this.emojiSmiling = emojiSmiling;
 | 
				
			||||||
| 
						 | 
					@ -156,7 +157,7 @@ export default {
 | 
				
			||||||
      return title;
 | 
					      return title;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    handleAward(awardName) {
 | 
					    handleAward(awardName) {
 | 
				
			||||||
      if (!this.isLoggedIn) {
 | 
					      if (!this.canAwardEmoji) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -208,7 +209,7 @@ export default {
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        v-if="isLoggedIn"
 | 
					        v-if="canAwardEmoji"
 | 
				
			||||||
        class="award-menu-holder">
 | 
					        class="award-menu-holder">
 | 
				
			||||||
        <button
 | 
					        <button
 | 
				
			||||||
          v-tooltip
 | 
					          v-tooltip
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -112,6 +112,7 @@ export default {
 | 
				
			||||||
      :note-author-id="note.author.id"
 | 
					      :note-author-id="note.author.id"
 | 
				
			||||||
      :awards="note.award_emoji"
 | 
					      :awards="note.award_emoji"
 | 
				
			||||||
      :toggle-award-path="note.toggle_award_path"
 | 
					      :toggle-award-path="note.toggle_award_path"
 | 
				
			||||||
 | 
					      :can-award-emoji="note.current_user.can_award_emoji"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <note-attachment
 | 
					    <note-attachment
 | 
				
			||||||
      v-if="note.attachment"
 | 
					      v-if="note.attachment"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -177,6 +177,7 @@ export default {
 | 
				
			||||||
            :note-id="note.id"
 | 
					            :note-id="note.id"
 | 
				
			||||||
            :access-level="note.human_access"
 | 
					            :access-level="note.human_access"
 | 
				
			||||||
            :can-edit="note.current_user.can_edit"
 | 
					            :can-edit="note.current_user.can_edit"
 | 
				
			||||||
 | 
					            :can-award-emoji="note.current_user.can_award_emoji"
 | 
				
			||||||
            :can-delete="note.current_user.can_edit"
 | 
					            :can-delete="note.current_user.can_edit"
 | 
				
			||||||
            :can-report-as-abuse="canReportAsAbuse"
 | 
					            :can-report-as-abuse="canReportAsAbuse"
 | 
				
			||||||
            :report-abuse-path="note.report_abuse_path"
 | 
					            :report-abuse-path="note.report_abuse_path"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,12 +49,7 @@ export default {
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
 | 
					    ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
 | 
				
			||||||
    noteableType() {
 | 
					    noteableType() {
 | 
				
			||||||
      // FIXME -- @fatihacet Get this from JSON data.
 | 
					      return this.noteableData.noteableType;
 | 
				
			||||||
      const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return this.noteableData.merge_params
 | 
					 | 
				
			||||||
        ? MERGE_REQUEST_NOTEABLE_TYPE
 | 
					 | 
				
			||||||
        : ISSUE_NOTEABLE_TYPE;
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    allNotes() {
 | 
					    allNotes() {
 | 
				
			||||||
      if (this.isLoading) {
 | 
					      if (this.isLoading) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,13 @@ export const CLOSED = 'closed';
 | 
				
			||||||
export const EMOJI_THUMBSUP = 'thumbsup';
 | 
					export const EMOJI_THUMBSUP = 'thumbsup';
 | 
				
			||||||
export const EMOJI_THUMBSDOWN = 'thumbsdown';
 | 
					export const EMOJI_THUMBSDOWN = 'thumbsdown';
 | 
				
			||||||
export const ISSUE_NOTEABLE_TYPE = 'issue';
 | 
					export const ISSUE_NOTEABLE_TYPE = 'issue';
 | 
				
			||||||
 | 
					export const EPIC_NOTEABLE_TYPE = 'epic';
 | 
				
			||||||
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
 | 
					export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
 | 
				
			||||||
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
 | 
					export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
 | 
				
			||||||
export const RESOLVE_NOTE_METHOD_NAME = 'post';
 | 
					export const RESOLVE_NOTE_METHOD_NAME = 'post';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const NOTEABLE_TYPE_MAPPING = {
 | 
				
			||||||
 | 
					  Issue: ISSUE_NOTEABLE_TYPE,
 | 
				
			||||||
 | 
					  MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
 | 
				
			||||||
 | 
					  Epic: EPIC_NOTEABLE_TYPE,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,11 @@ document.addEventListener(
 | 
				
			||||||
      data() {
 | 
					      data() {
 | 
				
			||||||
        const notesDataset = document.getElementById('js-vue-notes').dataset;
 | 
					        const notesDataset = document.getElementById('js-vue-notes').dataset;
 | 
				
			||||||
        const parsedUserData = JSON.parse(notesDataset.currentUserData);
 | 
					        const parsedUserData = JSON.parse(notesDataset.currentUserData);
 | 
				
			||||||
 | 
					        const noteableData = JSON.parse(notesDataset.noteableData);
 | 
				
			||||||
        let currentUserData = {};
 | 
					        let currentUserData = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        noteableData.noteableType = notesDataset.noteableType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (parsedUserData) {
 | 
					        if (parsedUserData) {
 | 
				
			||||||
          currentUserData = {
 | 
					          currentUserData = {
 | 
				
			||||||
            id: parsedUserData.id,
 | 
					            id: parsedUserData.id,
 | 
				
			||||||
| 
						 | 
					@ -25,7 +28,7 @@ document.addEventListener(
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          noteableData: JSON.parse(notesDataset.noteableData),
 | 
					          noteableData,
 | 
				
			||||||
          currentUserData,
 | 
					          currentUserData,
 | 
				
			||||||
          notesData: JSON.parse(notesDataset.notesData),
 | 
					          notesData: JSON.parse(notesDataset.notesData),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue