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:
Lin Jen-Shin 2018-04-17 20:55:47 +08:00
commit 8d4ad0c7db
1316 changed files with 51772 additions and 19499 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -1 +1 @@
2.3.6 2.3.7

View File

@ -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:

View File

@ -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)

View File

@ -1 +1 @@
0.92.0 0.95.0

View File

@ -1 +1 @@
0.7.1 0.8.0

View File

@ -1 +1 @@
7.1.1 7.1.2

View File

@ -1 +1 @@
4.0.0 4.1.0

16
Gemfile
View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,2 @@
export const GROUP_BADGE = 'group';
export const PROJECT_BADGE = 'project';

View File

@ -0,0 +1,7 @@
export default () => ({
imageUrl: '',
isDeleting: false,
linkUrl: '',
renderedImageUrl: '',
renderedLinkUrl: '',
});

View File

@ -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);
},
};

View File

@ -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,
});

View File

@ -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',
};

View File

@ -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,
});
},
};

View File

@ -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,
});

View File

@ -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();
} }
}); });

View File

@ -32,6 +32,10 @@ export default class FileTemplateSelector {
} }
} }
isHidden() {
return this.$wrapper.hasClass('hidden');
}
getToggleText() { getToggleText() {
return this.$dropdownToggleText.text(); return this.$dropdownToggleText.text();
} }

View File

@ -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,

View File

@ -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>

View File

@ -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;
}) })

View File

@ -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">

View File

@ -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;
}, },

View File

@ -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,

View File

@ -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,

View File

@ -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: {

View File

@ -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: {

View File

@ -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() {

View File

@ -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;
}, },

View File

@ -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: {

View File

@ -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',
}); });

View File

@ -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,

View File

@ -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;

View File

@ -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);
} }
} }

View File

@ -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() {

View File

@ -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();

View File

@ -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);
}
}, },
}, },
}; };

View File

@ -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');

View File

@ -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;

View File

@ -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;
} }

View File

@ -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,
}); });

View File

@ -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>

View File

@ -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"
/> />

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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}`);
}); });
}, },

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);
} }

View File

@ -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);
} }
} }

View File

@ -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() {

View File

@ -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;
}
} }

View File

@ -6,7 +6,7 @@ export const defaultEditorOptions = {
minimap: { minimap: {
enabled: false, enabled: false,
}, },
wordWrap: 'bounded', wordWrap: 'on',
}; };
export default [ export default [

View File

@ -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 }) => {

View File

@ -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}`);
};

View File

@ -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.');

View File

@ -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';

View File

@ -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),
});
},
}; };

View File

@ -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,
}; };
}; };

View File

@ -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, {

View File

@ -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) {

View File

@ -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) {

View File

@ -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');

View File

@ -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();

View File

@ -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(' ');
};

View File

@ -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() }))

View File

@ -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 });
});
}
} }

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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))];

View File

@ -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),
}; };

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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) {

View File

@ -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,
};

View File

@ -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