Merge branch 'master' into 'stuartnelson3/gitlab-ce-stn/issue-due-email'
# Conflicts: # db/schema.rb
This commit is contained in:
commit
33a4439e8f
|
@ -10,12 +10,6 @@ engines:
|
|||
- javascript
|
||||
exclude_paths:
|
||||
- "lib/api/v3/*"
|
||||
eslint:
|
||||
enabled: true
|
||||
channel: "eslint-4"
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: "gitlab-rubocop-0-52-1"
|
||||
ratings:
|
||||
paths:
|
||||
- Gemfile.lock
|
||||
|
|
135
.gitlab-ci.yml
135
.gitlab-ci.yml
|
@ -1,4 +1,4 @@
|
|||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
|
||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
|
||||
|
||||
.dedicated-runner: &dedicated-runner
|
||||
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
|
||||
|
||||
.default-cache: &default-cache
|
||||
key: "ruby-2.3.6-with-yarn"
|
||||
key: "ruby-2.3.7-with-yarn"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
|
@ -78,6 +78,19 @@ stages:
|
|||
- mysql:latest
|
||||
- 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/'.
|
||||
# Used for commits including ONLY documentation changes.
|
||||
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
|
||||
|
@ -118,6 +131,7 @@ stages:
|
|||
<<: *dedicated-runner
|
||||
<<: *except-docs-and-qa
|
||||
<<: *pull-cache
|
||||
<<: *rails5-variables
|
||||
stage: test
|
||||
script:
|
||||
- JOB_NAME=( $CI_JOB_NAME )
|
||||
|
@ -148,14 +162,23 @@ stages:
|
|||
<<: *rspec-metadata
|
||||
<<: *use-pg
|
||||
|
||||
.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5
|
||||
<<: *rspec-metadata-pg
|
||||
<<: *rails5
|
||||
|
||||
.rspec-metadata-mysql: &rspec-metadata-mysql
|
||||
<<: *rspec-metadata
|
||||
<<: *use-mysql
|
||||
|
||||
.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5
|
||||
<<: *rspec-metadata-mysql
|
||||
<<: *rails5
|
||||
|
||||
.spinach-metadata: &spinach-metadata
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs-and-qa
|
||||
<<: *pull-cache
|
||||
<<: *rails5-variables
|
||||
stage: test
|
||||
script:
|
||||
- JOB_NAME=( $CI_JOB_NAME )
|
||||
|
@ -179,10 +202,18 @@ stages:
|
|||
<<: *spinach-metadata
|
||||
<<: *use-pg
|
||||
|
||||
.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
|
||||
<<: *spinach-metadata-pg
|
||||
<<: *rails5
|
||||
|
||||
.spinach-metadata-mysql: &spinach-metadata-mysql
|
||||
<<: *spinach-metadata
|
||||
<<: *use-mysql
|
||||
|
||||
.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
|
||||
<<: *spinach-metadata-mysql
|
||||
<<: *rails5
|
||||
|
||||
.only-canonical-masters: &only-canonical-masters
|
||||
only:
|
||||
- master@gitlab-org/gitlab-ce
|
||||
|
@ -264,8 +295,18 @@ package-and-qa:
|
|||
stage: build
|
||||
cache: {}
|
||||
when: manual
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
retry: 0
|
||||
before_script:
|
||||
# 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
|
||||
# deleted (when merging the MR).
|
||||
- apk add --update openssl
|
||||
- wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
|
||||
- chmod 755 trigger-build-omnibus
|
||||
script:
|
||||
- scripts/trigger-build-omnibus
|
||||
- ./trigger-build-omnibus
|
||||
only:
|
||||
- //@gitlab-org/gitlab-ce
|
||||
- //@gitlab-org/gitlab-ee
|
||||
|
@ -458,6 +499,70 @@ spinach-pg 1 2: *spinach-metadata-pg
|
|||
spinach-mysql 0 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:
|
||||
<<: *dedicated-no-docs-no-db-pull-cache-job
|
||||
dependencies:
|
||||
|
@ -466,7 +571,7 @@ static-analysis:
|
|||
script:
|
||||
- scripts/static-analysis
|
||||
cache:
|
||||
key: "ruby-2.3.6-with-yarn-and-rubocop"
|
||||
key: "ruby-2.3.7-with-yarn-and-rubocop"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
|
@ -608,21 +713,23 @@ karma:
|
|||
|
||||
codequality:
|
||||
<<: *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: []
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
DOCKER_DRIVER: overlay2
|
||||
CODECLIMATE_FORMAT: json
|
||||
cache: {}
|
||||
dependencies: []
|
||||
script:
|
||||
- apk update && apk add jq
|
||||
- ./scripts/codequality analyze -f json > raw_codeclimate.json || true
|
||||
# The following line keeps only the fields used in the MR widget, reducing the JSON artifact size
|
||||
- jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json
|
||||
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
|
||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
||||
- 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
|
||||
artifacts:
|
||||
paths: [codeclimate.json]
|
||||
expire_in: 1 week
|
||||
|
@ -655,7 +762,13 @@ qa:selectors:
|
|||
- bundle exec bin/qa Test::Sanity::Selectors
|
||||
|
||||
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
|
||||
script:
|
||||
- bundle exec scripts/merge-simplecov
|
||||
|
|
|
@ -33,13 +33,16 @@ When removing columns, tables, indexes or other structures:
|
|||
|
||||
## General Checklist
|
||||
|
||||
- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary
|
||||
- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
|
||||
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
|
||||
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
|
||||
- [ ] API support added
|
||||
- [ ] Tests added for this feature/bug
|
||||
- Review
|
||||
- [ ] Has been reviewed by Backend
|
||||
- [ ] 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 [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
|
||||
- [ ] 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-ee/blob/master/CONTRIBUTING.md#style-guides)
|
||||
- [ ] [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-qa` manual pipeline job)
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.3.6
|
||||
2.3.7
|
||||
|
|
|
@ -59,6 +59,8 @@ linters:
|
|||
# Reports when you define the same property twice in a single rule set.
|
||||
DuplicateProperty:
|
||||
enabled: true
|
||||
ignore_consecutive:
|
||||
- cursor
|
||||
|
||||
# Separate rule, function, and mixin declarations with empty lines.
|
||||
EmptyLineBetweenBlocks:
|
||||
|
|
43
CHANGELOG.md
43
CHANGELOG.md
|
@ -2,6 +2,32 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
### Fixed (2 changes, 1 of them is from the community)
|
||||
|
@ -191,7 +217,6 @@ entry.
|
|||
- Enable privileged mode for GitLab Runner. !17528
|
||||
- Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
|
||||
- Upgrade GitLab Workhorse to 4.0.0.
|
||||
- Allow CI/CD Jobs being grouped on version strings.
|
||||
- Add discussions API for Issues and Snippets.
|
||||
- Add one group board to Libre.
|
||||
- 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.
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
### Security (2 changes)
|
||||
|
@ -485,6 +518,14 @@ entry.
|
|||
- 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)
|
||||
|
||||
### Security (2 changes)
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.92.0
|
||||
0.95.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.7.1
|
||||
0.8.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
7.1.1
|
||||
7.1.2
|
||||
|
|
12
Gemfile
12
Gemfile
|
@ -6,7 +6,6 @@ end
|
|||
gem_versions = {}
|
||||
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
|
||||
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
|
||||
gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0'
|
||||
gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
|
||||
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
|
||||
# --- The end of special code for migrating to Rails 5.0 ---
|
||||
|
@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4'
|
|||
gem 'seed-fu', '~> 2.3.7'
|
||||
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', gem_versions['html-pipeline']
|
||||
gem 'html-pipeline', '~> 2.7.1'
|
||||
gem 'deckar01-task_list', '2.0.0'
|
||||
gem 'gitlab-markup', '~> 1.6.2'
|
||||
gem 'redcarpet', '~> 3.4'
|
||||
|
@ -310,7 +309,7 @@ end
|
|||
|
||||
group :development do
|
||||
gem 'foreman', '~> 0.84.0'
|
||||
gem 'brakeman', '~> 3.6.0', require: false
|
||||
gem 'brakeman', '~> 4.2', require: false
|
||||
|
||||
gem 'letter_opener_web', '~> 1.3.0'
|
||||
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
|
||||
|
@ -385,7 +384,8 @@ group :test do
|
|||
gem 'email_spec', '~> 1.6.0'
|
||||
gem 'json-schema', '~> 2.8.0'
|
||||
gem 'webmock', '~> 2.3.2'
|
||||
gem 'test_after_commit', '~> 1.1'
|
||||
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 'sham_rack', '~> 1.3.6'
|
||||
gem 'concurrent-ruby', '~> 1.0.5'
|
||||
gem 'test-prof', '~> 0.2.5'
|
||||
|
@ -422,7 +422,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.10.0'
|
||||
|
||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||
|
@ -441,3 +441,5 @@ gem 'grape_logging', '~> 1.7'
|
|||
|
||||
# Asset synchronization
|
||||
gem 'asset_sync', '~> 2.2.0'
|
||||
|
||||
gem 'goldiloader', '~> 2.0'
|
||||
|
|
22
Gemfile.lock
22
Gemfile.lock
|
@ -95,7 +95,7 @@ GEM
|
|||
autoprefixer-rails (>= 5.2.1)
|
||||
sass (>= 3.3.4)
|
||||
bootstrap_form (2.7.0)
|
||||
brakeman (3.6.1)
|
||||
brakeman (4.2.1)
|
||||
browser (2.2.0)
|
||||
builder (3.2.3)
|
||||
bullet (5.5.1)
|
||||
|
@ -120,7 +120,7 @@ GEM
|
|||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
cause (0.1)
|
||||
charlock_holmes (0.7.5)
|
||||
charlock_holmes (0.7.6)
|
||||
childprocess (0.7.0)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
chronic (0.10.2)
|
||||
|
@ -290,7 +290,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.91.0)
|
||||
gitaly-proto (0.94.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -320,6 +320,9 @@ GEM
|
|||
rubyntlm (~> 0.5)
|
||||
globalid (0.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
goldiloader (2.0.1)
|
||||
activerecord (>= 4.2, < 5.2)
|
||||
activesupport (>= 4.2, < 5.2)
|
||||
gollum-grit_adapter (1.0.1)
|
||||
gitlab-grit (~> 2.7, >= 2.7.1)
|
||||
gollum-lib (4.2.7)
|
||||
|
@ -399,9 +402,9 @@ GEM
|
|||
hipchat (1.5.2)
|
||||
httparty
|
||||
mimemagic
|
||||
html-pipeline (1.11.0)
|
||||
html-pipeline (2.7.1)
|
||||
activesupport (>= 2)
|
||||
nokogiri (~> 1.4)
|
||||
nokogiri (>= 1.4)
|
||||
html2text (0.2.0)
|
||||
nokogiri (~> 1.6)
|
||||
htmlentities (4.3.4)
|
||||
|
@ -587,7 +590,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.0.3)
|
||||
parser (2.5.0.5)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
|
@ -1012,7 +1015,7 @@ DEPENDENCIES
|
|||
binding_of_caller (~> 0.7.2)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
bootstrap_form (~> 2.7.0)
|
||||
brakeman (~> 3.6.0)
|
||||
brakeman (~> 4.2)
|
||||
browser (~> 2.2)
|
||||
bullet (~> 5.5.0)
|
||||
bundler-audit (~> 0.5.0)
|
||||
|
@ -1061,12 +1064,13 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.91.0)
|
||||
gitaly-proto (~> 0.94.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
gitlab-styles (~> 2.3)
|
||||
gitlab_omniauth-ldap (~> 2.0.4)
|
||||
goldiloader (~> 2.0)
|
||||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.4)
|
||||
gon (~> 6.1.0)
|
||||
|
@ -1083,7 +1087,7 @@ DEPENDENCIES
|
|||
hashie-forbidden_attributes
|
||||
health_check (~> 2.6.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 1.11.0)
|
||||
html-pipeline (~> 2.7.1)
|
||||
html2text
|
||||
httparty (~> 0.13.3)
|
||||
influxdb (~> 0.2)
|
||||
|
|
|
@ -60,7 +60,7 @@ GEM
|
|||
faraday_middleware-multi_json (~> 0.0)
|
||||
oauth2 (~> 1.0)
|
||||
asciidoctor (1.5.6.1)
|
||||
asciidoctor-plantuml (0.0.7)
|
||||
asciidoctor-plantuml (0.0.8)
|
||||
asciidoctor (~> 1.5)
|
||||
asset_sync (2.2.0)
|
||||
activemodel (>= 4.1.0)
|
||||
|
@ -97,7 +97,7 @@ GEM
|
|||
autoprefixer-rails (>= 5.2.1)
|
||||
sass (>= 3.3.4)
|
||||
bootstrap_form (2.7.0)
|
||||
brakeman (3.6.2)
|
||||
brakeman (4.2.1)
|
||||
browser (2.5.3)
|
||||
builder (3.2.3)
|
||||
bullet (5.5.1)
|
||||
|
@ -144,6 +144,7 @@ GEM
|
|||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.3)
|
||||
creole (0.5.0)
|
||||
css_parser (1.6.0)
|
||||
addressable
|
||||
|
@ -244,10 +245,11 @@ GEM
|
|||
builder
|
||||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
fog-google (0.6.0)
|
||||
fog-google (1.3.3)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
google-api-client (~> 0.19.1)
|
||||
fog-json (1.0.2)
|
||||
fog-core (~> 1.0)
|
||||
multi_json (~> 1.10)
|
||||
|
@ -289,7 +291,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.88.0)
|
||||
gitaly-proto (0.94.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -398,7 +400,7 @@ GEM
|
|||
hipchat (1.5.4)
|
||||
httparty
|
||||
mimemagic
|
||||
html-pipeline (2.6.0)
|
||||
html-pipeline (2.7.1)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
html2text (0.2.1)
|
||||
|
@ -484,7 +486,8 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.0.3)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.0)
|
||||
mini_mime (>= 0.1.1)
|
||||
|
@ -527,8 +530,8 @@ GEM
|
|||
omniauth (1.8.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-auth0 (1.4.2)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-auth0 (2.0.0)
|
||||
omniauth-oauth2 (~> 1.4)
|
||||
omniauth-authentiq (0.3.1)
|
||||
omniauth-oauth2 (~> 1.3, >= 1.3.1)
|
||||
omniauth-azure-oauth2 (0.0.9)
|
||||
|
@ -551,6 +554,9 @@ GEM
|
|||
jwt (>= 1.5)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.5)
|
||||
omniauth-jwt (0.0.2)
|
||||
jwt
|
||||
omniauth (~> 1.1)
|
||||
omniauth-kerberos (0.3.0)
|
||||
omniauth-multipassword
|
||||
timfel-krb5-auth (~> 0.8)
|
||||
|
@ -569,9 +575,9 @@ GEM
|
|||
ruby-saml (~> 1.7)
|
||||
omniauth-shibboleth (1.2.1)
|
||||
omniauth (>= 1.0.0)
|
||||
omniauth-twitter (1.2.1)
|
||||
json (~> 1.3)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
omniauth_crowd (2.2.3)
|
||||
activesupport
|
||||
nokogiri (>= 1.4.4)
|
||||
|
@ -581,7 +587,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.0.4)
|
||||
parser (2.5.0.5)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
|
@ -672,6 +678,10 @@ GEM
|
|||
bundler (>= 1.3.0)
|
||||
railties (= 5.0.6)
|
||||
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)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (2.0.3)
|
||||
|
@ -808,7 +818,7 @@ GEM
|
|||
rubyzip (1.2.1)
|
||||
rufus-scheduler (3.4.2)
|
||||
et-orbi (~> 1.0)
|
||||
rugged (0.26.0)
|
||||
rugged (0.27.0)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (2.1.0)
|
||||
nokogiri (>= 1.4.4)
|
||||
|
@ -907,8 +917,6 @@ GEM
|
|||
sysexits (1.2.0)
|
||||
temple (0.7.7)
|
||||
test-prof (0.2.5)
|
||||
test_after_commit (1.1.0)
|
||||
activerecord (>= 3.2)
|
||||
text (1.3.1)
|
||||
thin (1.7.2)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
|
@ -995,8 +1003,8 @@ DEPENDENCIES
|
|||
akismet (~> 2.0)
|
||||
allocations (~> 1.0)
|
||||
asana (~> 0.6.0)
|
||||
asciidoctor (~> 1.5.2)
|
||||
asciidoctor-plantuml (= 0.0.7)
|
||||
asciidoctor (~> 1.5.6)
|
||||
asciidoctor-plantuml (= 0.0.8)
|
||||
asset_sync (~> 2.2.0)
|
||||
attr_encrypted (~> 3.0.0)
|
||||
awesome_print (~> 1.2.0)
|
||||
|
@ -1009,7 +1017,7 @@ DEPENDENCIES
|
|||
binding_of_caller (~> 0.7.2)
|
||||
bootstrap-sass (~> 3.3.0)
|
||||
bootstrap_form (~> 2.7.0)
|
||||
brakeman (~> 3.6.0)
|
||||
brakeman (~> 4.2)
|
||||
browser (~> 2.2)
|
||||
bullet (~> 5.5.0)
|
||||
bundler-audit (~> 0.5.0)
|
||||
|
@ -1044,9 +1052,9 @@ DEPENDENCIES
|
|||
flipper-active_record (~> 0.13.0)
|
||||
flipper-active_support_cache_store (~> 0.13.0)
|
||||
fog-aliyun (~> 0.2.0)
|
||||
fog-aws (~> 2.0)
|
||||
fog-aws (~> 2.0.1)
|
||||
fog-core (~> 1.44)
|
||||
fog-google (~> 0.5)
|
||||
fog-google (~> 1.3.3)
|
||||
fog-local (~> 0.3)
|
||||
fog-openstack (~> 0.1)
|
||||
fog-rackspace (~> 0.1.1)
|
||||
|
@ -1058,7 +1066,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.88.0)
|
||||
gitaly-proto (~> 0.94.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
|
@ -1080,7 +1088,7 @@ DEPENDENCIES
|
|||
hashie-forbidden_attributes
|
||||
health_check (~> 2.6.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 2.6.0)
|
||||
html-pipeline (~> 2.7.1)
|
||||
html2text
|
||||
httparty (~> 0.13.3)
|
||||
influxdb (~> 0.2)
|
||||
|
@ -1095,7 +1103,7 @@ DEPENDENCIES
|
|||
license_finder (~> 3.1)
|
||||
licensee (~> 8.9)
|
||||
lograge (~> 0.5)
|
||||
loofah (~> 2.0.3)
|
||||
loofah (~> 2.2)
|
||||
mail_room (~> 0.9.1)
|
||||
method_source (~> 0.8)
|
||||
minitest (~> 5.7.0)
|
||||
|
@ -1107,19 +1115,20 @@ DEPENDENCIES
|
|||
oauth2 (~> 1.4)
|
||||
octokit (~> 4.8)
|
||||
omniauth (~> 1.8)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth-auth0 (~> 2.0.0)
|
||||
omniauth-authentiq (~> 0.3.1)
|
||||
omniauth-azure-oauth2 (~> 0.0.9)
|
||||
omniauth-cas3 (~> 1.1.4)
|
||||
omniauth-facebook (~> 4.0.0)
|
||||
omniauth-github (~> 1.1.1)
|
||||
omniauth-gitlab (~> 1.0.2)
|
||||
omniauth-google-oauth2 (~> 0.5.2)
|
||||
omniauth-google-oauth2 (~> 0.5.3)
|
||||
omniauth-jwt (~> 0.0.2)
|
||||
omniauth-kerberos (~> 0.3.0)
|
||||
omniauth-oauth2-generic (~> 0.2.2)
|
||||
omniauth-saml (~> 1.10)
|
||||
omniauth-shibboleth (~> 1.2.0)
|
||||
omniauth-twitter (~> 1.2.0)
|
||||
omniauth-twitter (~> 1.4)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
org-ruby (~> 0.9.12)
|
||||
peek (~> 1.0.1)
|
||||
|
@ -1140,6 +1149,7 @@ DEPENDENCIES
|
|||
rack-oauth2 (~> 1.2.1)
|
||||
rack-proxy (~> 0.6.0)
|
||||
rails (= 5.0.6)
|
||||
rails-controller-testing
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rails-i18n (~> 5.1)
|
||||
rainbow (~> 2.2)
|
||||
|
@ -1169,7 +1179,7 @@ DEPENDENCIES
|
|||
ruby-prof (~> 0.17.0)
|
||||
ruby_parser (~> 3.8)
|
||||
rufus-scheduler (~> 3.4)
|
||||
rugged (~> 0.26.0)
|
||||
rugged (~> 0.27)
|
||||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.6)
|
||||
scss_lint (~> 0.56.0)
|
||||
|
@ -1197,7 +1207,6 @@ DEPENDENCIES
|
|||
state_machines-activerecord (~> 0.5.1)
|
||||
sys-filesystem (~> 1.1.6)
|
||||
test-prof (~> 0.2.5)
|
||||
test_after_commit (~> 1.1)
|
||||
thin (~> 1.7.0)
|
||||
timecop (~> 0.8.0)
|
||||
toml-rb (~> 1.0.0)
|
||||
|
|
|
@ -10,6 +10,9 @@ const Api = {
|
|||
projectsPath: '/api/:version/projects.json',
|
||||
projectPath: '/api/:version/projects/:id',
|
||||
projectLabelsPath: '/:namespace_path/:project_path/labels',
|
||||
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
|
||||
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
|
||||
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
|
||||
groupLabelsPath: '/groups/:namespace_path/-/labels',
|
||||
licensePath: '/api/:version/templates/licenses/:key',
|
||||
gitignorePath: '/api/:version/templates/gitignores/:key',
|
||||
|
@ -22,25 +25,27 @@ const Api = {
|
|||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
|
||||
group(groupId, callback) {
|
||||
const url = Api.buildUrl(Api.groupPath)
|
||||
.replace(':id', groupId);
|
||||
return axios.get(url)
|
||||
.then(({ data }) => {
|
||||
callback(data);
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
return axios.get(url).then(({ data }) => {
|
||||
callback(data);
|
||||
|
||||
return data;
|
||||
});
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
// Return groups list. Filtered by query
|
||||
groups(query, options, callback = $.noop) {
|
||||
const url = Api.buildUrl(Api.groupsPath);
|
||||
return axios.get(url, {
|
||||
params: Object.assign({
|
||||
search: query,
|
||||
per_page: 20,
|
||||
}, options),
|
||||
})
|
||||
return axios
|
||||
.get(url, {
|
||||
params: Object.assign(
|
||||
{
|
||||
search: query,
|
||||
per_page: 20,
|
||||
},
|
||||
options,
|
||||
),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
callback(data);
|
||||
|
||||
|
@ -51,12 +56,13 @@ const Api = {
|
|||
// Return namespaces list. Filtered by query
|
||||
namespaces(query, callback) {
|
||||
const url = Api.buildUrl(Api.namespacesPath);
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
search: query,
|
||||
per_page: 20,
|
||||
},
|
||||
})
|
||||
return axios
|
||||
.get(url, {
|
||||
params: {
|
||||
search: query,
|
||||
per_page: 20,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
|
@ -73,9 +79,10 @@ const Api = {
|
|||
defaults.membership = true;
|
||||
}
|
||||
|
||||
return axios.get(url, {
|
||||
params: Object.assign(defaults, options),
|
||||
})
|
||||
return axios
|
||||
.get(url, {
|
||||
params: Object.assign(defaults, options),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
callback(data);
|
||||
|
||||
|
@ -85,8 +92,32 @@ const Api = {
|
|||
|
||||
// Return single project
|
||||
project(projectPath) {
|
||||
const url = Api.buildUrl(Api.projectPath)
|
||||
.replace(':id', encodeURIComponent(projectPath));
|
||||
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
// Return Merge Request for project
|
||||
mergeRequest(projectPath, mergeRequestId) {
|
||||
const url = Api.buildUrl(Api.mergeRequestPath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':mrid', mergeRequestId);
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
mergeRequestChanges(projectPath, mergeRequestId) {
|
||||
const url = Api.buildUrl(Api.mergeRequestChangesPath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':mrid', mergeRequestId);
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
mergeRequestVersions(projectPath, mergeRequestId) {
|
||||
const url = Api.buildUrl(Api.mergeRequestVersionsPath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':mrid', mergeRequestId);
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
@ -102,30 +133,30 @@ const Api = {
|
|||
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
|
||||
}
|
||||
|
||||
return axios.post(url, {
|
||||
label: data,
|
||||
})
|
||||
return axios
|
||||
.post(url, {
|
||||
label: data,
|
||||
})
|
||||
.then(res => callback(res.data))
|
||||
.catch(e => callback(e.response.data));
|
||||
},
|
||||
|
||||
// Return group projects list. Filtered by query
|
||||
groupProjects(groupId, query, callback) {
|
||||
const url = Api.buildUrl(Api.groupProjectsPath)
|
||||
.replace(':id', groupId);
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
search: query,
|
||||
per_page: 20,
|
||||
},
|
||||
})
|
||||
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
|
||||
return axios
|
||||
.get(url, {
|
||||
params: {
|
||||
search: query,
|
||||
per_page: 20,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
commitMultiple(id, data) {
|
||||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||
const url = Api.buildUrl(Api.commitPath)
|
||||
.replace(':id', encodeURIComponent(id));
|
||||
const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
|
||||
return axios.post(url, JSON.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
|
@ -136,39 +167,34 @@ const Api = {
|
|||
branchSingle(id, branch) {
|
||||
const url = Api.buildUrl(Api.branchSinglePath)
|
||||
.replace(':id', encodeURIComponent(id))
|
||||
.replace(':branch', branch);
|
||||
.replace(':branch', encodeURIComponent(branch));
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
// Return text for a specific license
|
||||
licenseText(key, data, callback) {
|
||||
const url = Api.buildUrl(Api.licensePath)
|
||||
.replace(':key', key);
|
||||
return axios.get(url, {
|
||||
params: data,
|
||||
})
|
||||
const url = Api.buildUrl(Api.licensePath).replace(':key', key);
|
||||
return axios
|
||||
.get(url, {
|
||||
params: data,
|
||||
})
|
||||
.then(res => callback(res.data));
|
||||
},
|
||||
|
||||
gitignoreText(key, callback) {
|
||||
const url = Api.buildUrl(Api.gitignorePath)
|
||||
.replace(':key', key);
|
||||
return axios.get(url)
|
||||
.then(({ data }) => callback(data));
|
||||
const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
|
||||
return axios.get(url).then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
gitlabCiYml(key, callback) {
|
||||
const url = Api.buildUrl(Api.gitlabCiYmlPath)
|
||||
.replace(':key', key);
|
||||
return axios.get(url)
|
||||
.then(({ data }) => callback(data));
|
||||
const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
|
||||
return axios.get(url).then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
dockerfileYml(key, callback) {
|
||||
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
|
||||
return axios.get(url)
|
||||
.then(({ data }) => callback(data));
|
||||
return axios.get(url).then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
issueTemplate(namespacePath, projectPath, key, type, callback) {
|
||||
|
@ -177,7 +203,8 @@ const Api = {
|
|||
.replace(':type', type)
|
||||
.replace(':project_path', projectPath)
|
||||
.replace(':namespace_path', namespacePath);
|
||||
return axios.get(url)
|
||||
return axios
|
||||
.get(url)
|
||||
.then(({ data }) => callback(null, data))
|
||||
.catch(callback);
|
||||
},
|
||||
|
@ -185,10 +212,13 @@ const Api = {
|
|||
users(query, options) {
|
||||
const url = Api.buildUrl(this.usersPath);
|
||||
return axios.get(url, {
|
||||
params: Object.assign({
|
||||
search: query,
|
||||
per_page: 20,
|
||||
}, options),
|
||||
params: Object.assign(
|
||||
{
|
||||
search: query,
|
||||
per_page: 20,
|
||||
},
|
||||
options,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ import $ from 'jquery';
|
|||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
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 axios from './lib/utils/axios_utils';
|
||||
|
||||
|
@ -243,7 +244,7 @@ class AwardsHandler {
|
|||
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
|
||||
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
|
||||
|
||||
if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
|
||||
if (isInVueNoteablePage() && !isMainAwardsBlock) {
|
||||
const id = votesBlock.attr('id').replace('note_', '');
|
||||
|
||||
this.hideMenuElement($('.emoji-menu'));
|
||||
|
@ -295,16 +296,8 @@ class AwardsHandler {
|
|||
}
|
||||
}
|
||||
|
||||
isVueMRDiscussions() {
|
||||
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
|
||||
}
|
||||
|
||||
isInVueNoteablePage() {
|
||||
return isInIssuePage() || this.isVueMRDiscussions();
|
||||
}
|
||||
|
||||
getVotesBlock() {
|
||||
if (this.isInVueNoteablePage()) {
|
||||
if (isInVueNoteablePage()) {
|
||||
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
|
||||
|
||||
if ($el.length) {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import Tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'Badge',
|
||||
components: {
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
Tooltip,
|
||||
},
|
||||
directives: {
|
||||
Tooltip,
|
||||
},
|
||||
props: {
|
||||
imageUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
linkUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasError: false,
|
||||
isLoading: true,
|
||||
numRetries: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageUrlWithRetries() {
|
||||
if (this.numRetries === 0) {
|
||||
return this.imageUrl;
|
||||
}
|
||||
|
||||
return `${this.imageUrl}#retries=${this.numRetries}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
imageUrl() {
|
||||
this.hasError = false;
|
||||
this.isLoading = true;
|
||||
this.numRetries = 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onError() {
|
||||
this.isLoading = false;
|
||||
this.hasError = true;
|
||||
},
|
||||
onLoad() {
|
||||
this.isLoading = false;
|
||||
},
|
||||
reloadImage() {
|
||||
this.hasError = false;
|
||||
this.isLoading = true;
|
||||
this.numRetries += 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
v-show="!isLoading && !hasError"
|
||||
:href="linkUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
class="project-badge"
|
||||
:src="imageUrlWithRetries"
|
||||
@load="onLoad"
|
||||
@error="onError"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<loading-icon
|
||||
v-show="isLoading"
|
||||
:inline="true"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-show="hasError"
|
||||
class="btn-group"
|
||||
>
|
||||
<div class="btn btn-default btn-xs disabled">
|
||||
<icon
|
||||
class="prepend-left-8 append-right-8"
|
||||
name="doc_image"
|
||||
:size="16"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-default btn-xs disabled"
|
||||
>
|
||||
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-show="hasError"
|
||||
class="btn btn-transparent btn-xs text-primary"
|
||||
type="button"
|
||||
v-tooltip
|
||||
:title="s__('Badges|Reload badge image')"
|
||||
@click="reloadImage"
|
||||
>
|
||||
<icon
|
||||
name="retry"
|
||||
:size="16"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,219 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import createFlash from '~/flash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import createEmptyBadge from '../empty_badge';
|
||||
import Badge from './badge.vue';
|
||||
|
||||
const badgePreviewDelayInMilliseconds = 1500;
|
||||
|
||||
export default {
|
||||
name: 'BadgeForm',
|
||||
components: {
|
||||
Badge,
|
||||
LoadingButton,
|
||||
LoadingIcon,
|
||||
},
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'badgeInAddForm',
|
||||
'badgeInEditForm',
|
||||
'docsUrl',
|
||||
'isRendering',
|
||||
'isSaving',
|
||||
'renderedBadge',
|
||||
]),
|
||||
badge() {
|
||||
if (this.isEditing) {
|
||||
return this.badgeInEditForm;
|
||||
}
|
||||
|
||||
return this.badgeInAddForm;
|
||||
},
|
||||
canSubmit() {
|
||||
return (
|
||||
this.badge !== null &&
|
||||
this.badge.imageUrl &&
|
||||
this.badge.imageUrl.trim() !== '' &&
|
||||
this.badge.linkUrl &&
|
||||
this.badge.linkUrl.trim() !== '' &&
|
||||
!this.isSaving
|
||||
);
|
||||
},
|
||||
helpText() {
|
||||
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
|
||||
.map(placeholder => `<code>%{${placeholder}}</code>`)
|
||||
.join(', ');
|
||||
return sprintf(
|
||||
s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'),
|
||||
{
|
||||
docsLinkEnd: '</a>',
|
||||
docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`,
|
||||
placeholders,
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
renderedImageUrl() {
|
||||
return this.renderedBadge ? this.renderedBadge.renderedImageUrl : '';
|
||||
},
|
||||
renderedLinkUrl() {
|
||||
return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
|
||||
},
|
||||
imageUrl: {
|
||||
get() {
|
||||
return this.badge ? this.badge.imageUrl : '';
|
||||
},
|
||||
set(imageUrl) {
|
||||
const badge = this.badge || createEmptyBadge();
|
||||
this.updateBadgeInForm({
|
||||
...badge,
|
||||
imageUrl,
|
||||
});
|
||||
},
|
||||
},
|
||||
linkUrl: {
|
||||
get() {
|
||||
return this.badge ? this.badge.linkUrl : '';
|
||||
},
|
||||
set(linkUrl) {
|
||||
const badge = this.badge || createEmptyBadge();
|
||||
this.updateBadgeInForm({
|
||||
...badge,
|
||||
linkUrl,
|
||||
});
|
||||
},
|
||||
},
|
||||
submitButtonLabel() {
|
||||
if (this.isEditing) {
|
||||
return s__('Badges|Save changes');
|
||||
}
|
||||
return s__('Badges|Add badge');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
|
||||
debouncedPreview: _.debounce(function preview() {
|
||||
this.renderBadge();
|
||||
}, badgePreviewDelayInMilliseconds),
|
||||
onCancel() {
|
||||
this.stopEditing();
|
||||
},
|
||||
onSubmit() {
|
||||
if (!this.canSubmit) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.isEditing) {
|
||||
return this.saveBadge()
|
||||
.then(() => {
|
||||
createFlash(s__('Badges|The badge was saved.'), 'notice');
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash(
|
||||
s__('Badges|Saving the badge failed, please check the entered URLs and try again.'),
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return this.addBadge()
|
||||
.then(() => {
|
||||
createFlash(s__('Badges|A new badge was added.'), 'notice');
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash(
|
||||
s__('Badges|Adding the badge failed, please check the entered URLs and try again.'),
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
},
|
||||
badgeImageUrlPlaceholder:
|
||||
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
|
||||
badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="prepend-top-default append-bottom-default"
|
||||
@submit.prevent.stop="onSubmit"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
|
||||
<input
|
||||
id="badge-link-url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="linkUrl"
|
||||
:placeholder="$options.badgeLinkUrlPlaceholder"
|
||||
@input="debouncedPreview"
|
||||
/>
|
||||
<span
|
||||
class="help-block"
|
||||
v-html="helpText"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
|
||||
<input
|
||||
id="badge-image-url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="imageUrl"
|
||||
:placeholder="$options.badgeImageUrlPlaceholder"
|
||||
@input="debouncedPreview"
|
||||
/>
|
||||
<span
|
||||
class="help-block"
|
||||
v-html="helpText"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
|
||||
<badge
|
||||
id="badge-preview"
|
||||
v-show="renderedBadge && !isRendering"
|
||||
:image-url="renderedImageUrl"
|
||||
:link-url="renderedLinkUrl"
|
||||
/>
|
||||
<p v-show="isRendering">
|
||||
<loading-icon
|
||||
:inline="true"
|
||||
/>
|
||||
</p>
|
||||
<p
|
||||
v-show="!renderedBadge && !isRendering"
|
||||
class="disabled-content"
|
||||
>{{ s__('Badges|No image to preview') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="row-content-block">
|
||||
<loading-button
|
||||
type="submit"
|
||||
container-class="btn btn-success"
|
||||
:disabled="!canSubmit"
|
||||
:loading="isSaving"
|
||||
:label="submitButtonLabel"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-cancel"
|
||||
type="button"
|
||||
v-if="isEditing"
|
||||
@click="onCancel"
|
||||
>{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import BadgeListRow from './badge_list_row.vue';
|
||||
import { GROUP_BADGE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'BadgeList',
|
||||
components: {
|
||||
BadgeListRow,
|
||||
LoadingIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['badges', 'isLoading', 'kind']),
|
||||
hasNoBadges() {
|
||||
return !this.isLoading && (!this.badges || !this.badges.length);
|
||||
},
|
||||
isGroupBadge() {
|
||||
return this.kind === GROUP_BADGE;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ s__('Badges|Your badges') }}
|
||||
<span
|
||||
v-show="!isLoading"
|
||||
class="badge"
|
||||
>{{ badges.length }}</span>
|
||||
</div>
|
||||
<loading-icon
|
||||
v-show="isLoading"
|
||||
class="panel-body"
|
||||
size="2"
|
||||
/>
|
||||
<div
|
||||
v-if="hasNoBadges"
|
||||
class="panel-body"
|
||||
>
|
||||
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
|
||||
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="panel-body"
|
||||
>
|
||||
<badge-list-row
|
||||
v-for="badge in badges"
|
||||
:key="badge.id"
|
||||
:badge="badge"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import { PROJECT_BADGE } from '../constants';
|
||||
import Badge from './badge.vue';
|
||||
|
||||
export default {
|
||||
name: 'BadgeListRow',
|
||||
components: {
|
||||
Badge,
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
},
|
||||
props: {
|
||||
badge: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['kind']),
|
||||
badgeKindText() {
|
||||
if (this.badge.kind === PROJECT_BADGE) {
|
||||
return s__('Badges|Project Badge');
|
||||
}
|
||||
|
||||
return s__('Badges|Group Badge');
|
||||
},
|
||||
canEditBadge() {
|
||||
return this.badge.kind === this.kind;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['editBadge', 'updateBadgeInModal']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
|
||||
<badge
|
||||
class="table-section section-30"
|
||||
:image-url="badge.renderedImageUrl"
|
||||
:link-url="badge.renderedLinkUrl"
|
||||
/>
|
||||
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
|
||||
<div class="table-section section-10">
|
||||
<span class="badge">{{ badgeKindText }}</span>
|
||||
</div>
|
||||
<div class="table-section section-10 table-button-footer">
|
||||
<div
|
||||
v-if="canEditBadge"
|
||||
class="table-action-buttons">
|
||||
<button
|
||||
class="btn btn-default append-right-8"
|
||||
type="button"
|
||||
:disabled="badge.isDeleting"
|
||||
@click="editBadge(badge)"
|
||||
>
|
||||
<icon
|
||||
name="pencil"
|
||||
:size="16"
|
||||
:aria-label="__('Edit')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
type="button"
|
||||
data-toggle="modal"
|
||||
data-target="#delete-badge-modal"
|
||||
:disabled="badge.isDeleting"
|
||||
@click="updateBadgeInModal(badge)"
|
||||
>
|
||||
<icon
|
||||
name="remove"
|
||||
:size="16"
|
||||
:aria-label="__('Delete')"
|
||||
/>
|
||||
</button>
|
||||
<loading-icon
|
||||
v-show="badge.isDeleting"
|
||||
:inline="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import createFlash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
import Badge from './badge.vue';
|
||||
import BadgeForm from './badge_form.vue';
|
||||
import BadgeList from './badge_list.vue';
|
||||
|
||||
export default {
|
||||
name: 'BadgeSettings',
|
||||
components: {
|
||||
Badge,
|
||||
BadgeForm,
|
||||
BadgeList,
|
||||
GlModal,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['badgeInModal', 'isEditing']),
|
||||
deleteModalText() {
|
||||
return s__(
|
||||
'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['deleteBadge']),
|
||||
onSubmitModal() {
|
||||
this.deleteBadge(this.badgeInModal)
|
||||
.then(() => {
|
||||
createFlash(s__('Badges|The badge was deleted.'), 'notice');
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash(s__('Badges|Deleting the badge failed, please try again.'));
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="badge-settings">
|
||||
<gl-modal
|
||||
id="delete-badge-modal"
|
||||
:header-title-text="s__('Badges|Delete badge?')"
|
||||
footer-primary-button-variant="danger"
|
||||
:footer-primary-button-text="s__('Badges|Delete badge')"
|
||||
@submit="onSubmitModal">
|
||||
<div class="well">
|
||||
<badge
|
||||
:image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''"
|
||||
:link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
|
||||
/>
|
||||
</div>
|
||||
<p v-html="deleteModalText"></p>
|
||||
</gl-modal>
|
||||
|
||||
<badge-form
|
||||
v-show="isEditing"
|
||||
:is-editing="true"
|
||||
/>
|
||||
|
||||
<badge-form
|
||||
v-show="!isEditing"
|
||||
:is-editing="false"
|
||||
/>
|
||||
<badge-list v-show="!isEditing" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,2 @@
|
|||
export const GROUP_BADGE = 'group';
|
||||
export const PROJECT_BADGE = 'project';
|
|
@ -0,0 +1,7 @@
|
|||
export default () => ({
|
||||
imageUrl: '',
|
||||
isDeleting: false,
|
||||
linkUrl: '',
|
||||
renderedImageUrl: '',
|
||||
renderedLinkUrl: '',
|
||||
});
|
|
@ -0,0 +1,167 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import types from './mutation_types';
|
||||
|
||||
export const transformBackendBadge = badge => ({
|
||||
id: badge.id,
|
||||
imageUrl: badge.image_url,
|
||||
kind: badge.kind,
|
||||
linkUrl: badge.link_url,
|
||||
renderedImageUrl: badge.rendered_image_url,
|
||||
renderedLinkUrl: badge.rendered_link_url,
|
||||
isDeleting: false,
|
||||
});
|
||||
|
||||
export default {
|
||||
requestNewBadge({ commit }) {
|
||||
commit(types.REQUEST_NEW_BADGE);
|
||||
},
|
||||
receiveNewBadge({ commit }, newBadge) {
|
||||
commit(types.RECEIVE_NEW_BADGE, newBadge);
|
||||
},
|
||||
receiveNewBadgeError({ commit }) {
|
||||
commit(types.RECEIVE_NEW_BADGE_ERROR);
|
||||
},
|
||||
addBadge({ dispatch, state }) {
|
||||
const newBadge = state.badgeInAddForm;
|
||||
const endpoint = state.apiEndpointUrl;
|
||||
dispatch('requestNewBadge');
|
||||
return axios
|
||||
.post(endpoint, {
|
||||
image_url: newBadge.imageUrl,
|
||||
link_url: newBadge.linkUrl,
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch('receiveNewBadgeError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveNewBadge', transformBackendBadge(res.data));
|
||||
});
|
||||
},
|
||||
requestDeleteBadge({ commit }, badgeId) {
|
||||
commit(types.REQUEST_DELETE_BADGE, badgeId);
|
||||
},
|
||||
receiveDeleteBadge({ commit }, badgeId) {
|
||||
commit(types.RECEIVE_DELETE_BADGE, badgeId);
|
||||
},
|
||||
receiveDeleteBadgeError({ commit }, badgeId) {
|
||||
commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId);
|
||||
},
|
||||
deleteBadge({ dispatch, state }, badge) {
|
||||
const badgeId = badge.id;
|
||||
dispatch('requestDeleteBadge', badgeId);
|
||||
const endpoint = `${state.apiEndpointUrl}/${badgeId}`;
|
||||
return axios
|
||||
.delete(endpoint)
|
||||
.catch(error => {
|
||||
dispatch('receiveDeleteBadgeError', badgeId);
|
||||
throw error;
|
||||
})
|
||||
.then(() => {
|
||||
dispatch('receiveDeleteBadge', badgeId);
|
||||
});
|
||||
},
|
||||
|
||||
editBadge({ commit }, badge) {
|
||||
commit(types.START_EDITING, badge);
|
||||
},
|
||||
|
||||
requestLoadBadges({ commit }, data) {
|
||||
commit(types.REQUEST_LOAD_BADGES, data);
|
||||
},
|
||||
receiveLoadBadges({ commit }, badges) {
|
||||
commit(types.RECEIVE_LOAD_BADGES, badges);
|
||||
},
|
||||
receiveLoadBadgesError({ commit }) {
|
||||
commit(types.RECEIVE_LOAD_BADGES_ERROR);
|
||||
},
|
||||
|
||||
loadBadges({ dispatch, state }, data) {
|
||||
dispatch('requestLoadBadges', data);
|
||||
const endpoint = state.apiEndpointUrl;
|
||||
return axios
|
||||
.get(endpoint)
|
||||
.catch(error => {
|
||||
dispatch('receiveLoadBadgesError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveLoadBadges', res.data.map(transformBackendBadge));
|
||||
});
|
||||
},
|
||||
|
||||
requestRenderedBadge({ commit }) {
|
||||
commit(types.REQUEST_RENDERED_BADGE);
|
||||
},
|
||||
receiveRenderedBadge({ commit }, renderedBadge) {
|
||||
commit(types.RECEIVE_RENDERED_BADGE, renderedBadge);
|
||||
},
|
||||
receiveRenderedBadgeError({ commit }) {
|
||||
commit(types.RECEIVE_RENDERED_BADGE_ERROR);
|
||||
},
|
||||
|
||||
renderBadge({ dispatch, state }) {
|
||||
const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm;
|
||||
const { linkUrl, imageUrl } = badge;
|
||||
if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') {
|
||||
return Promise.resolve(badge);
|
||||
}
|
||||
|
||||
dispatch('requestRenderedBadge');
|
||||
|
||||
const parameters = [
|
||||
`link_url=${encodeURIComponent(linkUrl)}`,
|
||||
`image_url=${encodeURIComponent(imageUrl)}`,
|
||||
].join('&');
|
||||
const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`;
|
||||
return axios
|
||||
.get(renderEndpoint)
|
||||
.catch(error => {
|
||||
dispatch('receiveRenderedBadgeError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveRenderedBadge', transformBackendBadge(res.data));
|
||||
});
|
||||
},
|
||||
|
||||
requestUpdatedBadge({ commit }) {
|
||||
commit(types.REQUEST_UPDATED_BADGE);
|
||||
},
|
||||
receiveUpdatedBadge({ commit }, updatedBadge) {
|
||||
commit(types.RECEIVE_UPDATED_BADGE, updatedBadge);
|
||||
},
|
||||
receiveUpdatedBadgeError({ commit }) {
|
||||
commit(types.RECEIVE_UPDATED_BADGE_ERROR);
|
||||
},
|
||||
|
||||
saveBadge({ dispatch, state }) {
|
||||
const badge = state.badgeInEditForm;
|
||||
const endpoint = `${state.apiEndpointUrl}/${badge.id}`;
|
||||
dispatch('requestUpdatedBadge');
|
||||
return axios
|
||||
.put(endpoint, {
|
||||
image_url: badge.imageUrl,
|
||||
link_url: badge.linkUrl,
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch('receiveUpdatedBadgeError');
|
||||
throw error;
|
||||
})
|
||||
.then(res => {
|
||||
dispatch('receiveUpdatedBadge', transformBackendBadge(res.data));
|
||||
});
|
||||
},
|
||||
|
||||
stopEditing({ commit }) {
|
||||
commit(types.STOP_EDITING);
|
||||
},
|
||||
|
||||
updateBadgeInForm({ commit }, badge) {
|
||||
commit(types.UPDATE_BADGE_IN_FORM, badge);
|
||||
},
|
||||
|
||||
updateBadgeInModal({ commit }, badge) {
|
||||
commit(types.UPDATE_BADGE_IN_MODAL, badge);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import createState from './state';
|
||||
import actions from './actions';
|
||||
import mutations from './mutations';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: createState(),
|
||||
actions,
|
||||
mutations,
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
export default {
|
||||
RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE',
|
||||
RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR',
|
||||
RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES',
|
||||
RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR',
|
||||
RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE',
|
||||
RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR',
|
||||
RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE',
|
||||
RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR',
|
||||
RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE',
|
||||
RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR',
|
||||
REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE',
|
||||
REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES',
|
||||
REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE',
|
||||
REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE',
|
||||
REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE',
|
||||
START_EDITING: 'START_EDITING',
|
||||
STOP_EDITING: 'STOP_EDITING',
|
||||
UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM',
|
||||
UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL',
|
||||
};
|
|
@ -0,0 +1,158 @@
|
|||
import types from './mutation_types';
|
||||
import { PROJECT_BADGE } from '../constants';
|
||||
|
||||
const reorderBadges = badges =>
|
||||
badges.sort((a, b) => {
|
||||
if (a.kind !== b.kind) {
|
||||
return a.kind === PROJECT_BADGE ? 1 : -1;
|
||||
}
|
||||
|
||||
return a.id - b.id;
|
||||
});
|
||||
|
||||
export default {
|
||||
[types.RECEIVE_NEW_BADGE](state, newBadge) {
|
||||
Object.assign(state, {
|
||||
badgeInAddForm: null,
|
||||
badges: reorderBadges(state.badges.concat(newBadge)),
|
||||
isSaving: false,
|
||||
renderedBadge: null,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_NEW_BADGE_ERROR](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: false,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_NEW_BADGE](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: true,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_UPDATED_BADGE](state, updatedBadge) {
|
||||
const badges = state.badges.map(badge => {
|
||||
if (badge.id === updatedBadge.id) {
|
||||
return updatedBadge;
|
||||
}
|
||||
return badge;
|
||||
});
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: null,
|
||||
badges,
|
||||
isEditing: false,
|
||||
isSaving: false,
|
||||
renderedBadge: null,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_UPDATED_BADGE_ERROR](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: false,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_UPDATED_BADGE](state) {
|
||||
Object.assign(state, {
|
||||
isSaving: true,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_LOAD_BADGES](state, badges) {
|
||||
Object.assign(state, {
|
||||
badges: reorderBadges(badges),
|
||||
isLoading: false,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_LOAD_BADGES_ERROR](state) {
|
||||
Object.assign(state, {
|
||||
isLoading: false,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_LOAD_BADGES](state, data) {
|
||||
Object.assign(state, {
|
||||
kind: data.kind, // project or group
|
||||
apiEndpointUrl: data.apiEndpointUrl,
|
||||
docsUrl: data.docsUrl,
|
||||
isLoading: true,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_DELETE_BADGE](state, badgeId) {
|
||||
const badges = state.badges.filter(badge => badge.id !== badgeId);
|
||||
Object.assign(state, {
|
||||
badges,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) {
|
||||
const badges = state.badges.map(badge => {
|
||||
if (badge.id === badgeId) {
|
||||
return {
|
||||
...badge,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
return badge;
|
||||
});
|
||||
Object.assign(state, {
|
||||
badges,
|
||||
});
|
||||
},
|
||||
[types.REQUEST_DELETE_BADGE](state, badgeId) {
|
||||
const badges = state.badges.map(badge => {
|
||||
if (badge.id === badgeId) {
|
||||
return {
|
||||
...badge,
|
||||
isDeleting: true,
|
||||
};
|
||||
}
|
||||
|
||||
return badge;
|
||||
});
|
||||
Object.assign(state, {
|
||||
badges,
|
||||
});
|
||||
},
|
||||
|
||||
[types.RECEIVE_RENDERED_BADGE](state, renderedBadge) {
|
||||
Object.assign(state, { isRendering: false, renderedBadge });
|
||||
},
|
||||
[types.RECEIVE_RENDERED_BADGE_ERROR](state) {
|
||||
Object.assign(state, { isRendering: false });
|
||||
},
|
||||
[types.REQUEST_RENDERED_BADGE](state) {
|
||||
Object.assign(state, { isRendering: true });
|
||||
},
|
||||
|
||||
[types.START_EDITING](state, badge) {
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: { ...badge },
|
||||
isEditing: true,
|
||||
renderedBadge: { ...badge },
|
||||
});
|
||||
},
|
||||
[types.STOP_EDITING](state) {
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: null,
|
||||
isEditing: false,
|
||||
renderedBadge: null,
|
||||
});
|
||||
},
|
||||
|
||||
[types.UPDATE_BADGE_IN_FORM](state, badge) {
|
||||
if (state.isEditing) {
|
||||
Object.assign(state, {
|
||||
badgeInEditForm: badge,
|
||||
});
|
||||
} else {
|
||||
Object.assign(state, {
|
||||
badgeInAddForm: badge,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[types.UPDATE_BADGE_IN_MODAL](state, badge) {
|
||||
Object.assign(state, {
|
||||
badgeInModal: badge,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
export default () => ({
|
||||
apiEndpointUrl: null,
|
||||
badgeInAddForm: null,
|
||||
badgeInEditForm: null,
|
||||
badgeInModal: null,
|
||||
badges: [],
|
||||
docsUrl: null,
|
||||
renderedBadge: null,
|
||||
isEditing: false,
|
||||
isLoading: false,
|
||||
isRendering: false,
|
||||
isSaving: false,
|
||||
});
|
|
@ -94,7 +94,7 @@ export default class FileTemplateMediator {
|
|||
const hash = urlPieces[1];
|
||||
if (hash === 'preview') {
|
||||
this.hideTemplateSelectorMenu();
|
||||
} else if (hash === 'editor') {
|
||||
} else if (hash === 'editor' && !this.typeSelector.isHidden()) {
|
||||
this.showTemplateSelectorMenu();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -32,6 +32,10 @@ export default class FileTemplateSelector {
|
|||
}
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
return this.$wrapper.hasClass('hidden');
|
||||
}
|
||||
|
||||
getToggleText() {
|
||||
return this.$dropdownToggleText.text();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
|
|||
import Vue from 'vue';
|
||||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
import boardList from './board_list.vue';
|
||||
import boardBlankState from './board_blank_state';
|
||||
import BoardBlankState from './board_blank_state.vue';
|
||||
import './board_delete';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
|
|||
components: {
|
||||
boardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
boardBlankState,
|
||||
BoardBlankState,
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
|
|
|
@ -1,42 +1,11 @@
|
|||
<script>
|
||||
/* global ListLabel */
|
||||
|
||||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
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() {
|
||||
return {
|
||||
predefinedLabels: [
|
||||
|
@ -89,3 +58,41 @@ export default {
|
|||
clearBlankState: Store.removeBlankState.bind(Store),
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="board-blank-state">
|
||||
<p>
|
||||
Add the following default lists to your Issue Board with one click:
|
||||
</p>
|
||||
<ul class="board-blank-state-list">
|
||||
<li
|
||||
v-for="(label, index) in predefinedLabels"
|
||||
:key="index"
|
||||
>
|
||||
<span
|
||||
class="label-color"
|
||||
:style="{ backgroundColor: label.color }">
|
||||
</span>
|
||||
{{ label.title }}
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Starting out with the default set of lists will get you
|
||||
right on the way to making the most of your board.
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-create btn-inverted btn-block"
|
||||
type="button"
|
||||
@click.stop="addDefaultLists">
|
||||
Add default lists
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
type="button"
|
||||
@click.stop="clearBlankState">
|
||||
Nevermind, I'll use my own
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
|
|||
|
||||
this.issue = this.detail.issue;
|
||||
this.list = this.detail.list;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
|
||||
});
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
|
|||
saveAssignees () {
|
||||
this.loadingAssignees = true;
|
||||
|
||||
gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
|
||||
gl.issueBoards.BoardsStore.detail.issue.update()
|
||||
.then(() => {
|
||||
this.loadingAssignees = false;
|
||||
})
|
||||
|
|
|
@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
|
||||
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() {
|
||||
if (this.issue.iid) {
|
||||
return `#${this.issue.iid}`;
|
||||
|
@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
/>
|
||||
<a
|
||||
class="js-no-trigger"
|
||||
:href="cardUrl"
|
||||
:href="issue.path"
|
||||
:title="issue.title">{{ issue.title }}</a>
|
||||
<span
|
||||
class="card-number"
|
||||
v-if="issueId"
|
||||
>
|
||||
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
|
||||
{{ issue.referencePath }}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="card-assignee">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalEmptyState = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
|
|
|
@ -3,11 +3,11 @@ import Flash from '../../../flash';
|
|||
import { __ } from '../../../locale';
|
||||
import './lists_dropdown';
|
||||
import { pluralize } from '../../../lib/utils/text_utility';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalFooter = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import modalFilters from './filters';
|
||||
import './tabs';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalHeader = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
|
|
|
@ -7,8 +7,7 @@ import './header';
|
|||
import './list';
|
||||
import './footer';
|
||||
import './empty_state';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.IssuesModal = Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
import bp from '../../../breakpoints';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
|
||||
data() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalTabs = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
|
|
|
@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issueUpdate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updateUrl() {
|
||||
return this.issueUpdate.replace(':project_path', this.issue.project.path);
|
||||
return this.issue.path;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
|
|||
constructor(store, updateUrl = false, cantEdit = []) {
|
||||
super({
|
||||
page: 'boards',
|
||||
isGroupDecendent: true,
|
||||
stateFiltersSelector: '.issues-state-filters',
|
||||
});
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@ import './models/milestone';
|
|||
import './models/project';
|
||||
import './models/assignee';
|
||||
import './stores/boards_store';
|
||||
import './stores/modal_store';
|
||||
import ModalStore from './stores/modal_store';
|
||||
import BoardService from './services/board_service';
|
||||
import './mixins/modal_mixins';
|
||||
import modalMixin from './mixins/modal_mixins';
|
||||
import './mixins/sortable_default_options';
|
||||
import './filters/due_date_filters';
|
||||
import './components/board';
|
||||
|
@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
|
|||
export default () => {
|
||||
const $boardApp = document.getElementById('board-app');
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
|
||||
|
@ -176,7 +175,7 @@ export default () => {
|
|||
|
||||
gl.IssueBoardsModalAddBtn = new Vue({
|
||||
el: document.getElementById('js-add-issues-btn'),
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalMixins = {
|
||||
export default {
|
||||
methods: {
|
||||
toggleModal(toggle) {
|
||||
ModalStore.store.showAddIssuesModal = toggle;
|
||||
|
|
|
@ -23,6 +23,8 @@ class ListIssue {
|
|||
};
|
||||
this.isLoading = {};
|
||||
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
|
||||
this.referencePath = obj.reference_path;
|
||||
this.path = obj.real_path;
|
||||
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
|
||||
this.milestone_id = obj.milestone_id;
|
||||
this.project_id = obj.project_id;
|
||||
|
@ -98,7 +100,7 @@ class ListIssue {
|
|||
this.isLoading[key] = value;
|
||||
}
|
||||
|
||||
update (url) {
|
||||
update () {
|
||||
const data = {
|
||||
issue: {
|
||||
milestone_id: this.milestone ? this.milestone.id : null,
|
||||
|
@ -113,7 +115,7 @@ class ListIssue {
|
|||
}
|
||||
|
||||
const projectPath = this.project ? this.project.path : '';
|
||||
return Vue.http.patch(url.replace(':project_path', projectPath), data);
|
||||
return Vue.http.patch(`${this.path}.json`, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class BoardService {
|
|||
}
|
||||
|
||||
static generateIssuePath(boardId, id) {
|
||||
return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
|
||||
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
|
||||
}
|
||||
|
||||
all() {
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
class ModalStore {
|
||||
constructor() {
|
||||
this.store = {
|
||||
|
@ -95,4 +92,4 @@ class ModalStore {
|
|||
}
|
||||
}
|
||||
|
||||
gl.issueBoards.ModalStore = new ModalStore();
|
||||
export default new ModalStore();
|
||||
|
|
|
@ -55,22 +55,20 @@
|
|||
},
|
||||
methods: {
|
||||
successCallback(resp) {
|
||||
return resp.json().then((response) => {
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
const pipelines = response.pipelines || response;
|
||||
this.setCommonData(pipelines);
|
||||
// depending of the endpoint the response can either bring a `pipelines` key or not.
|
||||
const pipelines = resp.data.pipelines || resp.data;
|
||||
this.setCommonData(pipelines);
|
||||
|
||||
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
|
||||
detail: {
|
||||
pipelines: response,
|
||||
},
|
||||
});
|
||||
|
||||
// notifiy to update the count in tabs
|
||||
if (this.$el.parentElement) {
|
||||
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
|
||||
}
|
||||
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
|
||||
detail: {
|
||||
pipelines: resp.data,
|
||||
},
|
||||
});
|
||||
|
||||
// notifiy to update the count in tabs
|
||||
if (this.$el.parentElement) {
|
||||
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
|
|||
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
||||
this.page = page;
|
||||
this.groupsOnly = isGroup;
|
||||
this.groupAncestor = isGroupAncestor;
|
||||
this.isGroupDecendent = isGroupDecendent;
|
||||
this.includeAncestorGroups = isGroupAncestor;
|
||||
this.includeDescendantGroups = isGroupDecendent;
|
||||
|
||||
this.setupMapping();
|
||||
|
||||
|
@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class FilteredSearchManager {
|
|||
constructor({
|
||||
page,
|
||||
isGroup = false,
|
||||
isGroupAncestor = false,
|
||||
isGroupAncestor = true,
|
||||
isGroupDecendent = false,
|
||||
filteredSearchTokenKeys = FilteredSearchTokenKeys,
|
||||
stateFiltersSelector = '.issues-state-filters',
|
||||
|
@ -86,6 +86,7 @@ export default class FilteredSearchManager {
|
|||
page: this.page,
|
||||
isGroup: this.isGroup,
|
||||
isGroupAncestor: this.isGroupAncestor,
|
||||
isGroupDecendent: this.isGroupDecendent,
|
||||
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
changedIcon() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
computed: {
|
||||
changedIcon() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
changedIconClass() {
|
||||
return `multi-${this.changedIcon}`;
|
||||
},
|
||||
changedIconClass() {
|
||||
return `multi-${this.changedIcon}`;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,38 +1,36 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import router from '../../ide_router';
|
||||
import { mapActions } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'discardFileChanges',
|
||||
'updateViewer',
|
||||
]),
|
||||
openFileInEditor(file) {
|
||||
this.updateViewer('diff');
|
||||
|
||||
router.push(`/project${file.url}`);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
|
||||
openFileInEditor(file) {
|
||||
return this.openPendingTab(file).then(changeViewer => {
|
||||
if (changeViewer) {
|
||||
this.updateViewer('diff');
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,31 +1,44 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
props: {
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showShadow: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
mergeRequestId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
methods: {
|
||||
changeMode(mode) {
|
||||
this.$emit('click', mode);
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
showShadow: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
mergeReviewLine() {
|
||||
return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
|
||||
mergeRequestId: this.mergeRequestId,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeMode(mode) {
|
||||
this.$emit('click', mode);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -43,7 +56,10 @@
|
|||
}"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<template v-if="viewer === 'editor'">
|
||||
<template v-if="viewer === 'mrdiff' && mergeRequestId">
|
||||
{{ mergeReviewLine }}
|
||||
</template>
|
||||
<template v-else-if="viewer === 'editor'">
|
||||
{{ __('Editing') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -57,6 +73,29 @@
|
|||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
|
||||
<ul>
|
||||
<template v-if="mergeRequestId">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeMode('mrdiff')"
|
||||
:class="{
|
||||
'is-active': viewer === 'mrdiff',
|
||||
}"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title">
|
||||
{{ mergeReviewLine }}
|
||||
</strong>
|
||||
<span class="dropdown-menu-inner-content">
|
||||
{{ __('Compare changes with the merge request target branch') }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
role="separator"
|
||||
class="divider"
|
||||
>
|
||||
</li>
|
||||
</template>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
|
|
|
@ -1,51 +1,49 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import repoFileButtons from './repo_file_buttons.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
repoFileButtons,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'openFiles', 'viewer']),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = e => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = e => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -60,17 +58,16 @@
|
|||
v-if="activeFile"
|
||||
>
|
||||
<repo-tabs
|
||||
:active-file="activeFile"
|
||||
:files="openFiles"
|
||||
:viewer="viewer"
|
||||
:has-changes="hasChanges"
|
||||
:merge-request-id="currentMergeRequestId"
|
||||
/>
|
||||
<repo-editor
|
||||
class="multi-file-edit-pane-content"
|
||||
:file="activeFile"
|
||||
/>
|
||||
<repo-file-buttons
|
||||
:file="activeFile"
|
||||
/>
|
||||
<ide-status-bar
|
||||
:file="activeFile"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showButtons() {
|
||||
return (
|
||||
this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
|
||||
);
|
||||
},
|
||||
rawDownloadButtonLabel() {
|
||||
return this.file.binary ? __('Download') : __('Raw');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="pull-right ide-btn-group"
|
||||
>
|
||||
<a
|
||||
v-tooltip
|
||||
v-if="!file.binary"
|
||||
:href="file.blamePath"
|
||||
:title="__('Blame')"
|
||||
class="btn btn-xs btn-transparent blame"
|
||||
>
|
||||
<icon
|
||||
name="blame"
|
||||
:size="16"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-tooltip
|
||||
:href="file.commitsPath"
|
||||
:title="__('History')"
|
||||
class="btn btn-xs btn-transparent history"
|
||||
>
|
||||
<icon
|
||||
name="history"
|
||||
:size="16"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-tooltip
|
||||
:href="file.permalink"
|
||||
:title="__('Permalink')"
|
||||
class="btn btn-xs btn-transparent permalink"
|
||||
>
|
||||
<icon
|
||||
name="link"
|
||||
:size="16"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-tooltip
|
||||
:href="file.rawPath"
|
||||
target="_blank"
|
||||
class="btn btn-xs btn-transparent prepend-left-10 raw"
|
||||
rel="noopener noreferrer"
|
||||
:title="rawDownloadButtonLabel">
|
||||
<icon
|
||||
name="download"
|
||||
:size="16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
|
@ -1,25 +1,23 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [timeAgoMixin],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [
|
||||
timeAgoMixin,
|
||||
],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -50,7 +48,9 @@
|
|||
<div class="text-right">
|
||||
{{ file.eol }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div
|
||||
class="text-right"
|
||||
v-if="!file.binary">
|
||||
{{ file.editorRow }}:{{ file.editorColumn }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<icon
|
||||
name="git-merge"
|
||||
v-tooltip
|
||||
title="__('Part of merge request changes')"
|
||||
css-classes="ide-file-changed-icon"
|
||||
:size="12"
|
||||
/>
|
||||
</template>
|
|
@ -1,11 +1,17 @@
|
|||
<script>
|
||||
/* global monaco */
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import flash from '~/flash';
|
||||
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
import IdeFileButtons from './ide_file_buttons.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentViewer,
|
||||
IdeFileButtons,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
|
@ -13,31 +19,40 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
'rightPanelCollapsed',
|
||||
'viewer',
|
||||
'delayViewerUpdated',
|
||||
]),
|
||||
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
|
||||
...mapGetters(['currentMergeRequest']),
|
||||
shouldHideEditor() {
|
||||
return this.file && this.file.binary && !this.file.raw;
|
||||
},
|
||||
editTabCSS() {
|
||||
return {
|
||||
active: this.file.viewMode === 'edit',
|
||||
};
|
||||
},
|
||||
previewTabCSS() {
|
||||
return {
|
||||
active: this.file.viewMode === 'preview',
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
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();
|
||||
}
|
||||
},
|
||||
leftPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
rightPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
viewer() {
|
||||
this.createEditorInstance();
|
||||
},
|
||||
panelResizing() {
|
||||
if (!this.panelResizing) {
|
||||
this.editor.updateDimensions();
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.editor.dispose();
|
||||
|
@ -59,6 +74,7 @@ export default {
|
|||
'changeFileContent',
|
||||
'setFileLanguage',
|
||||
'setEditorPosition',
|
||||
'setFileViewMode',
|
||||
'setFileEOL',
|
||||
'updateViewer',
|
||||
'updateDelayViewerUpdated',
|
||||
|
@ -68,9 +84,14 @@ export default {
|
|||
|
||||
this.editor.clearEditor();
|
||||
|
||||
this.getRawFileData(this.file)
|
||||
this.getRawFileData({
|
||||
path: this.file.path,
|
||||
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
|
||||
})
|
||||
.then(() => {
|
||||
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
|
||||
const viewerPromise = this.delayViewerUpdated
|
||||
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
|
||||
: Promise.resolve();
|
||||
|
||||
return viewerPromise;
|
||||
})
|
||||
|
@ -78,7 +99,7 @@ export default {
|
|||
this.updateDelayViewerUpdated(false);
|
||||
this.createEditorInstance();
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
|
||||
throw err;
|
||||
});
|
||||
|
@ -101,9 +122,13 @@ export default {
|
|||
|
||||
this.model = this.editor.createModel(this.file);
|
||||
|
||||
this.editor.attachModel(this.model);
|
||||
if (this.viewer === 'mrdiff') {
|
||||
this.editor.attachMergeRequestModel(this.model);
|
||||
} else {
|
||||
this.editor.attachModel(this.model);
|
||||
}
|
||||
|
||||
this.model.onChange((model) => {
|
||||
this.model.onChange(model => {
|
||||
const { file } = model;
|
||||
|
||||
if (file.active) {
|
||||
|
@ -146,16 +171,49 @@ export default {
|
|||
id="ide"
|
||||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div
|
||||
v-if="shouldHideEditor"
|
||||
v-html="file.html"
|
||||
>
|
||||
<div class="ide-mode-tabs clearfix">
|
||||
<ul
|
||||
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
|
||||
v-show="!shouldHideEditor"
|
||||
v-show="!shouldHideEditor && file.viewMode === 'edit'"
|
||||
ref="editor"
|
||||
class="multi-file-editor-holder"
|
||||
>
|
||||
</div>
|
||||
<content-viewer
|
||||
v-if="shouldHideEditor || file.viewMode === 'preview'"
|
||||
:content="file.content || file.raw"
|
||||
:path="file.rawPath"
|
||||
:file-size="file.size"
|
||||
:project-path="file.projectId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,7 @@ import router from '../ide_router';
|
|||
import newDropdown from './new_dropdown/index.vue';
|
||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||
import changedFileIcon from './changed_file_icon.vue';
|
||||
import mrFileIcon from './mr_file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepoFile',
|
||||
|
@ -15,6 +16,7 @@ export default {
|
|||
fileStatusIcon,
|
||||
fileIcon,
|
||||
changedFileIcon,
|
||||
mrFileIcon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
|
@ -56,18 +58,11 @@ export default {
|
|||
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
|
||||
clickFile() {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (
|
||||
this.isTree &&
|
||||
this.$router.currentRoute.path === `/project${this.file.url}`
|
||||
) {
|
||||
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
|
||||
this.toggleTreeOpen(this.file.path);
|
||||
}
|
||||
|
||||
const delayPromise = this.file.changed
|
||||
? Promise.resolve()
|
||||
: this.updateDelayViewerUpdated(true);
|
||||
|
||||
return delayPromise.then(() => {
|
||||
return this.updateDelayViewerUpdated(true).then(() => {
|
||||
router.push(`/project${this.file.url}`);
|
||||
});
|
||||
},
|
||||
|
@ -102,11 +97,15 @@ export default {
|
|||
:file="file"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
:file="file"
|
||||
v-if="file.changed || file.tempFile"
|
||||
class="prepend-top-5 pull-right"
|
||||
/>
|
||||
<span class="pull-right">
|
||||
<mr-file-icon
|
||||
v-if="file.mrChange"
|
||||
/>
|
||||
<changed-file-icon
|
||||
:file="file"
|
||||
v-if="file.changed || file.tempFile"
|
||||
/>
|
||||
</span>
|
||||
<new-dropdown
|
||||
v-if="isTree"
|
||||
:project-id="file.projectId"
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showButtons() {
|
||||
return this.file.rawPath ||
|
||||
this.file.blamePath ||
|
||||
this.file.commitsPath ||
|
||||
this.file.permalink;
|
||||
},
|
||||
rawDownloadButtonLabel() {
|
||||
return this.file.binary ? 'Download' : 'Raw';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="multi-file-editor-btn-group"
|
||||
>
|
||||
<a
|
||||
:href="file.rawPath"
|
||||
target="_blank"
|
||||
class="btn btn-default btn-sm raw"
|
||||
rel="noopener noreferrer">
|
||||
{{ rawDownloadButtonLabel }}
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
aria-label="File actions"
|
||||
>
|
||||
<a
|
||||
:href="file.blamePath"
|
||||
class="btn btn-default btn-sm blame"
|
||||
>
|
||||
Blame
|
||||
</a>
|
||||
<a
|
||||
:href="file.commitsPath"
|
||||
class="btn btn-default btn-sm history"
|
||||
>
|
||||
History
|
||||
</a>
|
||||
<a
|
||||
:href="file.permalink"
|
||||
class="btn btn-default btn-sm permalink"
|
||||
>
|
||||
Permalink
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,27 +1,27 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import '~/lib/utils/datetime_utility';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
computed: {
|
||||
lockTooltip() {
|
||||
return `Locked by ${this.file.file_lock.user.name}`;
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
lockTooltip() {
|
||||
return `Locked by ${this.file.file_lock.user.name}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,60 +1,64 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||
import changedFileIcon from './changed_file_icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileStatusIcon from './repo_file_status_icon.vue';
|
||||
import ChangedFileIcon from './changed_file_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
fileStatusIcon,
|
||||
fileIcon,
|
||||
icon,
|
||||
changedFileIcon,
|
||||
export default {
|
||||
components: {
|
||||
FileStatusIcon,
|
||||
FileIcon,
|
||||
Icon,
|
||||
ChangedFileIcon,
|
||||
},
|
||||
props: {
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabMouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
closeLabel() {
|
||||
if (this.tab.changed || this.tab.tempFile) {
|
||||
return `${this.tab.name} changed`;
|
||||
}
|
||||
return `Close ${this.tab.name}`;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
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;
|
||||
},
|
||||
showChangedIcon() {
|
||||
return this.tab.changed ? !this.tabMouseOver : false;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions([
|
||||
'closeFile',
|
||||
]),
|
||||
clickFile(tab) {
|
||||
methods: {
|
||||
...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
|
||||
clickFile(tab) {
|
||||
this.updateDelayViewerUpdated(true);
|
||||
|
||||
if (tab.pending) {
|
||||
this.openPendingTab(tab);
|
||||
} else {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -66,7 +70,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="multi-file-tab-close"
|
||||
@click.stop.prevent="closeFile(tab.path)"
|
||||
@click.stop.prevent="closeFile(tab)"
|
||||
:aria-label="closeLabel"
|
||||
>
|
||||
<icon
|
||||
|
@ -82,7 +86,9 @@
|
|||
|
||||
<div
|
||||
class="multi-file-tab"
|
||||
:class="{active : tab.active }"
|
||||
:class="{
|
||||
active: tab.active
|
||||
}"
|
||||
:title="tab.url"
|
||||
>
|
||||
<file-icon
|
||||
|
|
|
@ -1,42 +1,62 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
import EditorMode from './editor_mode_dropdown.vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
import EditorMode from './editor_mode_dropdown.vue';
|
||||
import router from '../ide_router';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RepoTab,
|
||||
EditorMode,
|
||||
export default {
|
||||
components: {
|
||||
RepoTab,
|
||||
EditorMode,
|
||||
},
|
||||
props: {
|
||||
activeFile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showShadow: false,
|
||||
};
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
updated() {
|
||||
if (!this.$refs.tabsScroller) return;
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
mergeRequestId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showShadow: false,
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
if (!this.$refs.tabsScroller) return;
|
||||
|
||||
this.showShadow =
|
||||
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
|
||||
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
|
||||
},
|
||||
methods: {
|
||||
...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;
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -55,7 +75,8 @@
|
|||
:viewer="viewer"
|
||||
:show-shadow="showShadow"
|
||||
:has-changes="hasChanges"
|
||||
@click="updateViewer"
|
||||
:merge-request-id="mergeRequestId"
|
||||
@click="openFileViewer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,67 +1,64 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PanelResizer,
|
||||
export default {
|
||||
components: {
|
||||
PanelResizer,
|
||||
},
|
||||
props: {
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
initialWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 200,
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: this.initialWidth,
|
||||
};
|
||||
minSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 340,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
collapsed(state) {
|
||||
return state[`${this.side}PanelCollapsed`];
|
||||
},
|
||||
}),
|
||||
panelStyle() {
|
||||
if (!this.collapsed) {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
};
|
||||
}
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: this.initialWidth,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
collapsed(state) {
|
||||
return state[`${this.side}PanelCollapsed`];
|
||||
},
|
||||
}),
|
||||
panelStyle() {
|
||||
if (!this.collapsed) {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setPanelCollapsedStatus',
|
||||
'setResizingStatus',
|
||||
]),
|
||||
toggleFullbarCollapsed() {
|
||||
if (this.collapsed && this.collapsible) {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: this.side,
|
||||
collapsed: !this.collapsed,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
|
||||
toggleFullbarCollapsed() {
|
||||
if (this.collapsed && this.collapsible) {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: this.side,
|
||||
collapsed: !this.collapsed,
|
||||
});
|
||||
}
|
||||
},
|
||||
maxSize: (window.innerWidth / 2),
|
||||
};
|
||||
},
|
||||
maxSize: window.innerWidth / 2,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -44,7 +44,7 @@ const router = new VueRouter({
|
|||
component: EmptyRouterComponent,
|
||||
},
|
||||
{
|
||||
path: 'mr/:mrid',
|
||||
path: 'merge_requests/:mrid',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
],
|
||||
|
@ -76,10 +76,12 @@ router.beforeEach((to, from, next) => {
|
|||
.then(() => {
|
||||
if (to.params[0]) {
|
||||
const path =
|
||||
to.params[0].slice(-1) === '/'
|
||||
? to.params[0].slice(0, -1)
|
||||
: to.params[0];
|
||||
const treeEntry = store.state.entries[path];
|
||||
to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
|
||||
const treeEntryKey = Object.keys(store.state.entries).find(
|
||||
key => key === path && !store.state.entries[key].pending,
|
||||
);
|
||||
const treeEntry = store.state.entries[treeEntryKey];
|
||||
|
||||
if (treeEntry) {
|
||||
store.dispatch('handleTreeEntryAction', treeEntry);
|
||||
}
|
||||
|
@ -96,6 +98,60 @@ router.beforeEach((to, from, next) => {
|
|||
);
|
||||
throw e;
|
||||
});
|
||||
} else if (to.params.mrid) {
|
||||
store.dispatch('updateViewer', 'mrdiff');
|
||||
|
||||
store
|
||||
.dispatch('getMergeRequestData', {
|
||||
projectId: fullProjectId,
|
||||
mergeRequestId: to.params.mrid,
|
||||
})
|
||||
.then(mr => {
|
||||
store.dispatch('getBranchData', {
|
||||
projectId: fullProjectId,
|
||||
branchId: mr.source_branch,
|
||||
});
|
||||
|
||||
return store.dispatch('getFiles', {
|
||||
projectId: fullProjectId,
|
||||
branchId: mr.source_branch,
|
||||
});
|
||||
})
|
||||
.then(() =>
|
||||
store.dispatch('getMergeRequestVersions', {
|
||||
projectId: fullProjectId,
|
||||
mergeRequestId: to.params.mrid,
|
||||
}),
|
||||
)
|
||||
.then(() =>
|
||||
store.dispatch('getMergeRequestChanges', {
|
||||
projectId: fullProjectId,
|
||||
mergeRequestId: to.params.mrid,
|
||||
}),
|
||||
)
|
||||
.then(mrChanges => {
|
||||
mrChanges.changes.forEach((change, ind) => {
|
||||
const changeTreeEntry = store.state.entries[change.new_path];
|
||||
|
||||
if (changeTreeEntry) {
|
||||
store.dispatch('setFileMrChange', {
|
||||
file: changeTreeEntry,
|
||||
mrChange: change,
|
||||
});
|
||||
|
||||
if (ind < 10) {
|
||||
store.dispatch('getFileData', {
|
||||
path: change.new_path,
|
||||
makeFileActive: ind === 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
flash('Error while loading the merge request. Please try again.');
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -13,25 +13,31 @@ export default class Model {
|
|||
(this.originalModel = this.monaco.editor.createModel(
|
||||
this.file.raw,
|
||||
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.content,
|
||||
undefined,
|
||||
new this.monaco.Uri(null, null, this.file.path),
|
||||
new this.monaco.Uri(null, null, this.file.key),
|
||||
)),
|
||||
);
|
||||
if (this.file.mrChange) {
|
||||
this.disposable.add(
|
||||
(this.baseModel = this.monaco.editor.createModel(
|
||||
this.file.baseRaw,
|
||||
undefined,
|
||||
new this.monaco.Uri(null, null, `target/${this.file.path}`),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
this.events = new Map();
|
||||
|
||||
this.updateContent = this.updateContent.bind(this);
|
||||
this.dispose = this.dispose.bind(this);
|
||||
|
||||
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
|
||||
eventHub.$on(
|
||||
`editor.update.model.content.${this.file.path}`,
|
||||
this.updateContent,
|
||||
);
|
||||
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
|
||||
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
|
||||
}
|
||||
|
||||
get url() {
|
||||
|
@ -47,7 +53,7 @@ export default class Model {
|
|||
}
|
||||
|
||||
get path() {
|
||||
return this.file.path;
|
||||
return this.file.key;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
|
@ -58,6 +64,10 @@ export default class Model {
|
|||
return this.originalModel;
|
||||
}
|
||||
|
||||
getBaseModel() {
|
||||
return this.baseModel;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.getModel().setValue(value);
|
||||
}
|
||||
|
@ -78,13 +88,7 @@ export default class Model {
|
|||
this.disposable.dispose();
|
||||
this.events.clear();
|
||||
|
||||
eventHub.$off(
|
||||
`editor.update.model.dispose.${this.file.path}`,
|
||||
this.dispose,
|
||||
);
|
||||
eventHub.$off(
|
||||
`editor.update.model.content.${this.file.path}`,
|
||||
this.updateContent,
|
||||
);
|
||||
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
|
||||
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,17 +9,17 @@ export default class ModelManager {
|
|||
this.models = new Map();
|
||||
}
|
||||
|
||||
hasCachedModel(path) {
|
||||
return this.models.has(path);
|
||||
hasCachedModel(key) {
|
||||
return this.models.has(key);
|
||||
}
|
||||
|
||||
getModel(path) {
|
||||
return this.models.get(path);
|
||||
getModel(key) {
|
||||
return this.models.get(key);
|
||||
}
|
||||
|
||||
addModel(file) {
|
||||
if (this.hasCachedModel(file.path)) {
|
||||
return this.getModel(file.path);
|
||||
if (this.hasCachedModel(file.key)) {
|
||||
return this.getModel(file.key);
|
||||
}
|
||||
|
||||
const model = new Model(this.monaco, file);
|
||||
|
@ -27,7 +27,7 @@ export default class ModelManager {
|
|||
this.disposable.add(model);
|
||||
|
||||
eventHub.$on(
|
||||
`editor.update.model.dispose.${file.path}`,
|
||||
`editor.update.model.dispose.${file.key}`,
|
||||
this.removeCachedModel.bind(this, file),
|
||||
);
|
||||
|
||||
|
@ -35,12 +35,9 @@ export default class ModelManager {
|
|||
}
|
||||
|
||||
removeCachedModel(file) {
|
||||
this.models.delete(file.path);
|
||||
this.models.delete(file.key);
|
||||
|
||||
eventHub.$off(
|
||||
`editor.update.model.dispose.${file.path}`,
|
||||
this.removeCachedModel,
|
||||
);
|
||||
eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
|
@ -69,6 +69,7 @@ export default class Editor {
|
|||
occurrencesHighlight: false,
|
||||
renderLineHighlight: 'none',
|
||||
hideCursorInOverviewRuler: true,
|
||||
renderSideBySide: Editor.renderSideBySide(domElement),
|
||||
})),
|
||||
);
|
||||
|
||||
|
@ -81,7 +82,7 @@ export default class Editor {
|
|||
}
|
||||
|
||||
attachModel(model) {
|
||||
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
|
||||
if (this.isDiffEditorType) {
|
||||
this.instance.setModel({
|
||||
original: model.getOriginalModel(),
|
||||
modified: model.getModel(),
|
||||
|
@ -109,11 +110,19 @@ export default class Editor {
|
|||
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
|
||||
}
|
||||
|
||||
attachMergeRequestModel(model) {
|
||||
this.instance.setModel({
|
||||
original: model.getBaseModel(),
|
||||
modified: model.getModel(),
|
||||
});
|
||||
|
||||
this.monaco.editor.createDiffNavigator(this.instance, {
|
||||
alwaysRevealFirst: true,
|
||||
});
|
||||
}
|
||||
|
||||
setupMonacoTheme() {
|
||||
this.monaco.editor.defineTheme(
|
||||
gitlabTheme.themeName,
|
||||
gitlabTheme.monacoTheme,
|
||||
);
|
||||
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
|
||||
|
||||
this.monaco.editor.setTheme('gitlab');
|
||||
}
|
||||
|
@ -145,6 +154,7 @@ export default class Editor {
|
|||
|
||||
updateDimensions() {
|
||||
this.instance.layout();
|
||||
this.updateDiffView();
|
||||
}
|
||||
|
||||
setPosition({ lineNumber, column }) {
|
||||
|
@ -161,8 +171,22 @@ export default class Editor {
|
|||
onPositionChange(cb) {
|
||||
if (!this.instance.onDidChangeCursorPosition) return;
|
||||
|
||||
this.disposable.add(
|
||||
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
|
||||
);
|
||||
this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
|
||||
}
|
||||
|
||||
updateDiffView() {
|
||||
if (!this.isDiffEditorType) return;
|
||||
|
||||
this.instance.updateOptions({
|
||||
renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()),
|
||||
});
|
||||
}
|
||||
|
||||
get isDiffEditorType() {
|
||||
return this.instance.getEditorType() === 'vs.editor.IDiffEditor';
|
||||
}
|
||||
|
||||
static renderSideBySide(domElement) {
|
||||
return domElement.offsetWidth >= 700;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export const defaultEditorOptions = {
|
|||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
wordWrap: 'bounded',
|
||||
wordWrap: 'on',
|
||||
};
|
||||
|
||||
export default [
|
||||
|
|
|
@ -20,12 +20,35 @@ export default {
|
|||
return Promise.resolve(file.raw);
|
||||
}
|
||||
|
||||
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
|
||||
return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
|
||||
},
|
||||
getBaseRawFileData(file, sha) {
|
||||
if (file.tempFile) {
|
||||
return Promise.resolve(file.baseRaw);
|
||||
}
|
||||
|
||||
if (file.baseRaw) {
|
||||
return Promise.resolve(file.baseRaw);
|
||||
}
|
||||
|
||||
return Vue.http
|
||||
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
|
||||
params: { format: 'json' },
|
||||
})
|
||||
.then(res => res.text());
|
||||
},
|
||||
getProjectData(namespace, project) {
|
||||
return Api.project(`${namespace}/${project}`);
|
||||
},
|
||||
getProjectMergeRequestData(projectId, mergeRequestId) {
|
||||
return Api.mergeRequest(projectId, mergeRequestId);
|
||||
},
|
||||
getProjectMergeRequestChanges(projectId, mergeRequestId) {
|
||||
return Api.mergeRequestChanges(projectId, mergeRequestId);
|
||||
},
|
||||
getProjectMergeRequestVersions(projectId, mergeRequestId) {
|
||||
return Api.mergeRequestVersions(projectId, mergeRequestId);
|
||||
},
|
||||
getBranchData(projectId, currentBranchId) {
|
||||
return Api.branchSingle(projectId, currentBranchId);
|
||||
},
|
||||
|
|
|
@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
|
|||
|
||||
export const redirectToUrl = (_, url) => visitUrl(url);
|
||||
|
||||
export const setInitialData = ({ commit }, data) =>
|
||||
commit(types.SET_INITIAL_DATA, data);
|
||||
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
|
||||
|
||||
export const discardAllChanges = ({ state, commit, dispatch }) => {
|
||||
state.changedFiles.forEach(file => {
|
||||
|
@ -22,7 +21,7 @@ export const discardAllChanges = ({ state, commit, 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 }) => {
|
||||
|
@ -43,14 +42,11 @@ export const createTempEntry = (
|
|||
) =>
|
||||
new Promise(resolve => {
|
||||
const worker = new FilesDecoratorWorker();
|
||||
const fullName =
|
||||
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
|
||||
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
|
||||
|
||||
if (state.entries[name]) {
|
||||
flash(
|
||||
`The name "${name
|
||||
.split('/')
|
||||
.pop()}" is already taken in this directory.`,
|
||||
`The name "${name.split('/').pop()}" is already taken in this directory.`,
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
|
@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
|
|||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
export * from './actions/merge_request';
|
||||
|
|
|
@ -6,24 +6,34 @@ import * as types from '../mutation_types';
|
|||
import router from '../../ide_router';
|
||||
import { setPageTitle } from '../utils';
|
||||
|
||||
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
|
||||
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
|
||||
const file = state.entries[path];
|
||||
export const closeFile = ({ commit, state, dispatch }, file) => {
|
||||
const path = file.path;
|
||||
const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
|
||||
const fileWasActive = file.active;
|
||||
|
||||
commit(types.TOGGLE_FILE_OPEN, path);
|
||||
commit(types.SET_FILE_ACTIVE, { path, active: false });
|
||||
if (file.pending) {
|
||||
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) {
|
||||
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) {
|
||||
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) => {
|
||||
|
@ -46,53 +56,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
|
|||
commit(types.SET_CURRENT_BRANCH, file.branchId);
|
||||
};
|
||||
|
||||
export const getFileData = ({ state, commit, dispatch }, file) => {
|
||||
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
|
||||
const file = state.entries[path];
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
|
||||
return service
|
||||
.getFileData(file.url)
|
||||
.then(res => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
|
||||
setPageTitle(pageTitle);
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
commit(types.SET_FILE_DATA, { data, file });
|
||||
commit(types.TOGGLE_FILE_OPEN, file.path);
|
||||
dispatch('setFileActive', file.path);
|
||||
commit(types.TOGGLE_FILE_OPEN, path);
|
||||
if (makeFileActive) dispatch('setFileActive', path);
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
flash(
|
||||
'Error loading file data. Please try again.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
|
||||
});
|
||||
};
|
||||
|
||||
export const getRawFileData = ({ commit, dispatch }, file) =>
|
||||
service
|
||||
.getRawFileData(file)
|
||||
.then(raw => {
|
||||
commit(types.SET_FILE_RAW_DATA, { file, raw });
|
||||
})
|
||||
.catch(() =>
|
||||
flash(
|
||||
'Error loading file content. Please try again.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
);
|
||||
export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
|
||||
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
|
||||
};
|
||||
|
||||
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
|
||||
const file = state.entries[path];
|
||||
return new Promise((resolve, reject) => {
|
||||
service
|
||||
.getRawFileData(file)
|
||||
.then(raw => {
|
||||
commit(types.SET_FILE_RAW_DATA, { file, raw });
|
||||
if (file.mrChange && file.mrChange.new_file === false) {
|
||||
service
|
||||
.getBaseRawFileData(file, baseSha)
|
||||
.then(baseRaw => {
|
||||
commit(types.SET_FILE_BASE_RAW_DATA, {
|
||||
file,
|
||||
baseRaw,
|
||||
});
|
||||
resolve(raw);
|
||||
})
|
||||
.catch(e => {
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
resolve(raw);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading file content. Please try again.');
|
||||
reject();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const changeFileContent = ({ state, commit }, { path, content }) => {
|
||||
const file = state.entries[path];
|
||||
|
@ -119,10 +139,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setEditorPosition = (
|
||||
{ getters, commit },
|
||||
{ editorRow, editorColumn },
|
||||
) => {
|
||||
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
|
||||
if (getters.activeFile) {
|
||||
commit(types.SET_FILE_POSITION, {
|
||||
file: getters.activeFile,
|
||||
|
@ -132,6 +149,10 @@ export const setEditorPosition = (
|
|||
}
|
||||
};
|
||||
|
||||
export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
|
||||
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
|
||||
};
|
||||
|
||||
export const discardFileChanges = ({ state, commit }, path) => {
|
||||
const file = state.entries[path];
|
||||
|
||||
|
@ -144,3 +165,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
|
|||
|
||||
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}`);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import flash from '~/flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
|
||||
export const getMergeRequestData = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, mergeRequestId, force = false } = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
|
||||
service
|
||||
.getProjectMergeRequestData(projectId, mergeRequestId)
|
||||
.then(res => res.data)
|
||||
.then(data => {
|
||||
commit(types.SET_MERGE_REQUEST, {
|
||||
projectPath: projectId,
|
||||
mergeRequestId,
|
||||
mergeRequest: data,
|
||||
});
|
||||
if (!state.currentMergeRequestId) {
|
||||
commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading merge request data. Please try again.');
|
||||
reject(new Error(`Merge Request not loaded ${projectId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
|
||||
}
|
||||
});
|
||||
|
||||
export const getMergeRequestChanges = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, mergeRequestId, force = false } = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
|
||||
service
|
||||
.getProjectMergeRequestChanges(projectId, mergeRequestId)
|
||||
.then(res => res.data)
|
||||
.then(data => {
|
||||
commit(types.SET_MERGE_REQUEST_CHANGES, {
|
||||
projectPath: projectId,
|
||||
mergeRequestId,
|
||||
changes: data,
|
||||
});
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading merge request changes. Please try again.');
|
||||
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
|
||||
}
|
||||
});
|
||||
|
||||
export const getMergeRequestVersions = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, mergeRequestId, force = false } = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
|
||||
service
|
||||
.getProjectMergeRequestVersions(projectId, mergeRequestId)
|
||||
.then(res => res.data)
|
||||
.then(data => {
|
||||
commit(types.SET_MERGE_REQUEST_VERSIONS, {
|
||||
projectPath: projectId,
|
||||
mergeRequestId,
|
||||
versions: data,
|
||||
});
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading merge request versions. Please try again.');
|
||||
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
|
||||
}
|
||||
});
|
|
@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
|
|||
import flash from '~/flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import {
|
||||
findEntry,
|
||||
} from '../utils';
|
||||
import { findEntry } from '../utils';
|
||||
import FilesDecoratorWorker from '../workers/files_decorator_worker';
|
||||
|
||||
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
|
||||
|
@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
|
|||
|
||||
dispatch('setFileActive', row.path);
|
||||
} else {
|
||||
dispatch('getFileData', row);
|
||||
dispatch('getFileData', { path: row.path });
|
||||
}
|
||||
};
|
||||
|
||||
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
||||
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
|
||||
|
||||
service.getTreeLastCommit(tree.lastCommitPath)
|
||||
.then((res) => {
|
||||
service
|
||||
.getTreeLastCommit(tree.lastCommitPath)
|
||||
.then(res => {
|
||||
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
||||
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((lastCommit) => {
|
||||
.then(data => {
|
||||
data.forEach(lastCommit => {
|
||||
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
|
||||
|
||||
if (entry) {
|
||||
|
@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
|
|||
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
|
||||
};
|
||||
|
||||
export const getFiles = (
|
||||
{ state, commit, dispatch },
|
||||
{ projectId, branchId } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if (!state.trees[`${projectId}/${branchId}`]) {
|
||||
const selectedProject = state.projects[projectId];
|
||||
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
|
||||
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!state.trees[`${projectId}/${branchId}`]) {
|
||||
const selectedProject = state.projects[projectId];
|
||||
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
|
||||
|
||||
service
|
||||
.getFiles(selectedProject.web_url, branchId)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
const worker = new FilesDecoratorWorker();
|
||||
worker.addEventListener('message', (e) => {
|
||||
const { entries, treeList } = e.data;
|
||||
const selectedTree = state.trees[`${projectId}/${branchId}`];
|
||||
service
|
||||
.getFiles(selectedProject.web_url, branchId)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const worker = new FilesDecoratorWorker();
|
||||
worker.addEventListener('message', e => {
|
||||
const { entries, treeList } = e.data;
|
||||
const selectedTree = state.trees[`${projectId}/${branchId}`];
|
||||
|
||||
commit(types.SET_ENTRIES, entries);
|
||||
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
|
||||
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
|
||||
commit(types.SET_ENTRIES, entries);
|
||||
commit(types.SET_DIRECTORY_DATA, {
|
||||
treePath: `${projectId}/${branchId}`,
|
||||
data: treeList,
|
||||
});
|
||||
commit(types.TOGGLE_LOADING, {
|
||||
entry: selectedTree,
|
||||
forceValue: false,
|
||||
});
|
||||
|
||||
worker.terminate();
|
||||
worker.terminate();
|
||||
|
||||
resolve();
|
||||
resolve();
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
data,
|
||||
projectId,
|
||||
branchId,
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
data,
|
||||
projectId,
|
||||
branchId,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
export const activeFile = state =>
|
||||
state.openFiles.find(file => file.active) || null;
|
||||
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||
|
||||
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
|
||||
|
||||
export const modifiedFiles = state =>
|
||||
state.changedFiles.filter(f => !f.tempFile);
|
||||
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
|
||||
|
||||
export const projectsWithTrees = state =>
|
||||
Object.keys(state.projects).map(projectId => {
|
||||
|
@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
|
|||
};
|
||||
});
|
||||
|
||||
export const currentMergeRequest = state => {
|
||||
if (state.projects[state.currentProjectId]) {
|
||||
return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
export const currentIcon = state =>
|
||||
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
|
||||
|
||||
export const hasChanges = state => !!state.changedFiles.length;
|
||||
|
||||
export const hasMergeRequest = state => !!state.currentMergeRequestId;
|
||||
|
|
|
@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
|
|||
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
|
||||
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
|
||||
|
||||
// Merge Request Mutation Types
|
||||
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
|
||||
export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
|
||||
export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
|
||||
export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
|
||||
|
||||
// Branch Mutation Types
|
||||
export const SET_BRANCH = 'SET_BRANCH';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
|
@ -28,9 +34,11 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
|
|||
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
|
||||
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
||||
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
||||
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
|
||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
||||
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 DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
|
||||
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
|
||||
|
@ -39,5 +47,9 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
|
|||
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
|
||||
export const SET_ENTRIES = 'SET_ENTRIES';
|
||||
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
|
||||
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
|
||||
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
|
||||
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';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as types from './mutation_types';
|
||||
import projectMutations from './mutations/project';
|
||||
import mergeRequestMutation from './mutations/merge_request';
|
||||
import fileMutations from './mutations/file';
|
||||
import treeMutations from './mutations/tree';
|
||||
import branchMutations from './mutations/branch';
|
||||
|
@ -11,10 +12,7 @@ export default {
|
|||
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
|
||||
if (entry.path) {
|
||||
Object.assign(state.entries[entry.path], {
|
||||
loading:
|
||||
forceValue !== undefined
|
||||
? forceValue
|
||||
: !state.entries[entry.path].loading,
|
||||
loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
|
||||
});
|
||||
} else {
|
||||
Object.assign(entry, {
|
||||
|
@ -83,9 +81,7 @@ export default {
|
|||
|
||||
if (!foundEntry) {
|
||||
Object.assign(state.trees[`${projectId}/${branchId}`], {
|
||||
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
|
||||
data.treeList,
|
||||
),
|
||||
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -100,6 +96,7 @@ export default {
|
|||
});
|
||||
},
|
||||
...projectMutations,
|
||||
...mergeRequestMutation,
|
||||
...fileMutations,
|
||||
...treeMutations,
|
||||
...branchMutations,
|
||||
|
|
|
@ -5,6 +5,14 @@ export default {
|
|||
Object.assign(state.entries[path], {
|
||||
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) {
|
||||
Object.assign(state.entries[path], {
|
||||
|
@ -12,10 +20,14 @@ export default {
|
|||
});
|
||||
|
||||
if (state.entries[path].opened) {
|
||||
state.openFiles.push(state.entries[path]);
|
||||
} else {
|
||||
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),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -28,6 +40,10 @@ export default {
|
|||
rawPath: data.raw_path,
|
||||
binary: data.binary,
|
||||
renderError: data.render_error,
|
||||
raw: null,
|
||||
baseRaw: null,
|
||||
html: data.html,
|
||||
size: data.size,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
|
||||
|
@ -35,6 +51,11 @@ export default {
|
|||
raw,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
baseRaw,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
|
||||
const changed = content !== state.entries[path].raw;
|
||||
|
||||
|
@ -59,6 +80,16 @@ export default {
|
|||
editorColumn,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
mrChange,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
viewMode,
|
||||
});
|
||||
},
|
||||
[types.DISCARD_FILE_CHANGES](state, path) {
|
||||
Object.assign(state.entries[path], {
|
||||
content: state.entries[path].raw,
|
||||
|
@ -80,4 +111,37 @@ export default {
|
|||
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),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
|
||||
Object.assign(state, {
|
||||
currentMergeRequestId,
|
||||
});
|
||||
},
|
||||
[types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
|
||||
Object.assign(state.projects[projectPath], {
|
||||
mergeRequests: {
|
||||
[mergeRequestId]: {
|
||||
...mergeRequest,
|
||||
active: true,
|
||||
changes: [],
|
||||
versions: [],
|
||||
baseCommitSha: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
|
||||
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
|
||||
changes,
|
||||
});
|
||||
},
|
||||
[types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
|
||||
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
|
||||
versions,
|
||||
baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -11,6 +11,7 @@ export default {
|
|||
Object.assign(project, {
|
||||
tree: [],
|
||||
branches: {},
|
||||
mergeRequests: {},
|
||||
active: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export default () => ({
|
||||
currentProjectId: '',
|
||||
currentBranchId: '',
|
||||
currentMergeRequestId: '',
|
||||
changedFiles: [],
|
||||
endpoints: {},
|
||||
lastCommitMsg: '',
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export const dataStructure = () => ({
|
||||
id: '',
|
||||
// Key will contain a mixture of ID and path
|
||||
// it can also contain a prefix `pending-` for files opened in review mode
|
||||
key: '',
|
||||
type: '',
|
||||
projectId: '',
|
||||
|
@ -36,9 +38,12 @@ export const dataStructure = () => ({
|
|||
editorColumn: 1,
|
||||
fileLanguage: '',
|
||||
eol: '',
|
||||
viewMode: 'edit',
|
||||
previewMode: null,
|
||||
size: 0,
|
||||
});
|
||||
|
||||
export const decorateData = (entity) => {
|
||||
export const decorateData = entity => {
|
||||
const {
|
||||
id,
|
||||
projectId,
|
||||
|
@ -55,9 +60,9 @@ export const decorateData = (entity) => {
|
|||
changed = false,
|
||||
parentTreeUrl = '',
|
||||
base64 = false,
|
||||
|
||||
previewMode,
|
||||
file_lock,
|
||||
|
||||
html,
|
||||
} = entity;
|
||||
|
||||
return {
|
||||
|
@ -78,19 +83,18 @@ export const decorateData = (entity) => {
|
|||
renderError,
|
||||
content,
|
||||
base64,
|
||||
|
||||
previewMode,
|
||||
file_lock,
|
||||
|
||||
html,
|
||||
};
|
||||
};
|
||||
|
||||
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
|
||||
f => f.type === type && f[prop] === name,
|
||||
);
|
||||
export const findEntry = (tree, type, name, prop = 'name') =>
|
||||
tree.find(f => f.type === type && f[prop] === name);
|
||||
|
||||
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
|
||||
|
||||
export const setPageTitle = (title) => {
|
||||
export const setPageTitle = title => {
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
|
@ -120,6 +124,11 @@ const sortTreesByTypeAndName = (a, b) => {
|
|||
return 0;
|
||||
};
|
||||
|
||||
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
|
||||
tree: entity.tree.length ? sortTree(entity.tree) : [],
|
||||
})).sort(sortTreesByTypeAndName);
|
||||
export const sortTree = sortedTree =>
|
||||
sortedTree
|
||||
.map(entity =>
|
||||
Object.assign(entity, {
|
||||
tree: entity.tree.length ? sortTree(entity.tree) : [],
|
||||
}),
|
||||
)
|
||||
.sort(sortTreesByTypeAndName);
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
|
||||
import { decorateData, sortTree } from '../utils';
|
||||
|
||||
self.addEventListener('message', e => {
|
||||
const {
|
||||
data,
|
||||
projectId,
|
||||
branchId,
|
||||
tempFile = false,
|
||||
content = '',
|
||||
base64 = false,
|
||||
} = e.data;
|
||||
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
|
||||
|
||||
const treeList = [];
|
||||
let file;
|
||||
|
@ -19,9 +13,7 @@ self.addEventListener('message', e => {
|
|||
if (pathSplit.length > 0) {
|
||||
pathSplit.reduce((pathAcc, folderName) => {
|
||||
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
|
||||
const folderPath = `${
|
||||
parentFolder ? `${parentFolder.path}/` : ''
|
||||
}${folderName}`;
|
||||
const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
|
||||
const foundEntry = acc[folderPath];
|
||||
|
||||
if (!foundEntry) {
|
||||
|
@ -33,9 +25,7 @@ self.addEventListener('message', e => {
|
|||
path: folderPath,
|
||||
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
|
||||
type: 'tree',
|
||||
parentTreeUrl: parentFolder
|
||||
? parentFolder.url
|
||||
: `/${projectId}/tree/${branchId}/`,
|
||||
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
opened: tempFile,
|
||||
|
@ -70,13 +60,12 @@ self.addEventListener('message', e => {
|
|||
path,
|
||||
url: `/${projectId}/blob/${branchId}/${path}`,
|
||||
type: 'blob',
|
||||
parentTreeUrl: fileFolder
|
||||
? fileFolder.url
|
||||
: `/${projectId}/blob/${branchId}`,
|
||||
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
content,
|
||||
base64,
|
||||
previewMode: viewerInformationForPath(blobName),
|
||||
});
|
||||
|
||||
Object.assign(acc, {
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
return `#${this.job.runner.id}`;
|
||||
},
|
||||
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() {
|
||||
if (this.job.metadata == null) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
|||
import DropdownUtils from './filtered_search/dropdown_utils';
|
||||
import CreateLabelDropdown from './create_label';
|
||||
import flash from './flash';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
|
||||
export default class LabelsSelect {
|
||||
constructor(els, options = {}) {
|
||||
|
@ -350,7 +351,7 @@ export default class LabelsSelect {
|
|||
}
|
||||
|
||||
if ($dropdown.closest('.add-issues-modal').length) {
|
||||
boardsModel = gl.issueBoards.ModalStore.store.filter;
|
||||
boardsModel = ModalStore.store.filter;
|
||||
}
|
||||
|
||||
if (boardsModel) {
|
||||
|
|
|
@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => {
|
|||
|
||||
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
|
||||
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
|
||||
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
|
||||
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
|
||||
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import $ from 'jquery';
|
||||
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
|
||||
|
||||
const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
|
||||
|
||||
export const addClassIfElementExists = (element, className) => {
|
||||
if (element) {
|
||||
element.classList.add(className);
|
||||
}
|
||||
};
|
||||
|
||||
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
* @param {String} text
|
||||
* @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.
|
||||
|
@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count);
|
|||
* @param {String} 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
|
||||
|
@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase();
|
|||
* @param {Number} maxLength
|
||||
* @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
|
||||
|
@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re
|
|||
* @param {*} string
|
||||
*/
|
||||
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(' ');
|
||||
};
|
||||
|
|
|
@ -51,7 +51,7 @@ export function removeParams(params) {
|
|||
const url = document.createElement('a');
|
||||
url.href = window.location.href;
|
||||
|
||||
params.forEach((param) => {
|
||||
params.forEach(param => {
|
||||
url.search = removeParamQueryString(url.search, param);
|
||||
});
|
||||
|
||||
|
@ -83,3 +83,11 @@ export function refreshCurrentPage() {
|
|||
export function redirectTo(url) {
|
||||
return window.location.assign(url);
|
||||
}
|
||||
|
||||
export function webIDEUrl(route = undefined) {
|
||||
let returnUrl = `${gon.relative_url_root}/-/ide/`;
|
||||
if (route) {
|
||||
returnUrl += `project${route}`;
|
||||
}
|
||||
return returnUrl;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import $ from 'jquery';
|
|||
import _ from 'underscore';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { timeFor } from './lib/utils/datetime_utility';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
|
||||
export default class MilestoneSelect {
|
||||
constructor(currentProject, els, options = {}) {
|
||||
|
@ -94,10 +95,10 @@ export default class MilestoneSelect {
|
|||
if (showMenuAbove) {
|
||||
$dropdown.data('glDropdown').positionMenuAbove();
|
||||
}
|
||||
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
|
||||
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
|
||||
}),
|
||||
renderRow: milestone => `
|
||||
<li data-milestone-id="${milestone.name}">
|
||||
<li data-milestone-id="${_.escape(milestone.name)}">
|
||||
<a href='#' class='dropdown-menu-milestone-link'>
|
||||
${_.escape(milestone.title)}
|
||||
</a>
|
||||
|
@ -125,7 +126,6 @@ export default class MilestoneSelect {
|
|||
return milestone.id;
|
||||
}
|
||||
},
|
||||
isSelected: milestone => milestone.name === selectedMilestone,
|
||||
hidden: () => {
|
||||
$selectBox.hide();
|
||||
// display:block overrides the hide-collapse rule
|
||||
|
@ -137,7 +137,7 @@ export default class MilestoneSelect {
|
|||
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
|
||||
}
|
||||
$('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'),
|
||||
clicked: (clickEvent) => {
|
||||
|
@ -158,13 +158,14 @@ export default class MilestoneSelect {
|
|||
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
|
||||
const isSelecting = (selected.name !== selectedMilestone);
|
||||
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
|
||||
|
||||
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dropdown.closest('.add-issues-modal').length) {
|
||||
boardsStore = gl.issueBoards.ModalStore.store.filter;
|
||||
boardsStore = ModalStore.store.filter;
|
||||
}
|
||||
|
||||
if (boardsStore) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import _ from 'underscore';
|
||||
import { max, extent } from 'd3-array';
|
||||
import { select } from 'd3-selection';
|
||||
import GraphAxis from './graph/axis.vue';
|
||||
import GraphLegend from './graph/legend.vue';
|
||||
import GraphFlag from './graph/flag.vue';
|
||||
import GraphDeployment from './graph/deployment.vue';
|
||||
|
@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
|
|||
|
||||
export default {
|
||||
components: {
|
||||
GraphLegend,
|
||||
GraphAxis,
|
||||
GraphFlag,
|
||||
GraphDeployment,
|
||||
GraphPath,
|
||||
GraphLegend,
|
||||
},
|
||||
mixins: [MonitoringMixin],
|
||||
props: {
|
||||
|
@ -138,7 +141,7 @@ export default {
|
|||
this.legendTitle = query.label || 'Average';
|
||||
this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
|
||||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||
this.baseGraphHeight = this.graphHeight;
|
||||
this.baseGraphHeight = this.graphHeight - 50;
|
||||
this.baseGraphWidth = this.graphWidth;
|
||||
|
||||
// pixel offsets inside the svg and outside are not 1:1
|
||||
|
@ -177,10 +180,8 @@ export default {
|
|||
this.graphHeightOffset,
|
||||
);
|
||||
|
||||
if (!this.showLegend) {
|
||||
this.baseGraphHeight -= 50;
|
||||
} else if (this.timeSeries.length > 3) {
|
||||
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
|
||||
if (_.findWhere(this.timeSeries, { renderCanary: true })) {
|
||||
this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
|
||||
}
|
||||
|
||||
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
|
||||
|
@ -251,17 +252,13 @@ export default {
|
|||
class="y-axis"
|
||||
transform="translate(70, 20)"
|
||||
/>
|
||||
<graph-legend
|
||||
<graph-axis
|
||||
:graph-width="graphWidth"
|
||||
:graph-height="graphHeight"
|
||||
:margin="margin"
|
||||
:measurements="measurements"
|
||||
:legend-title="legendTitle"
|
||||
:y-axis-label="yAxisLabel"
|
||||
:time-series="timeSeries"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
:current-data-index="currentDataIndex"
|
||||
:show-legend-group="showLegend"
|
||||
/>
|
||||
<svg
|
||||
class="graph-data"
|
||||
|
@ -306,5 +303,10 @@ export default {
|
|||
:deployment-flag-data="deploymentFlagData"
|
||||
/>
|
||||
</div>
|
||||
<graph-legend
|
||||
v-if="showLegend"
|
||||
:legend-title="legendTitle"
|
||||
:time-series="timeSeries"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
<script>
|
||||
import { convertToSentenceCase } from '~/lib/utils/text_utility';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
graphWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
margin: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
measurements: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
unitOfDisplay: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yLabelWidth: 0,
|
||||
yLabelHeight: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textTransform() {
|
||||
const yCoordinate =
|
||||
(this.graphHeight -
|
||||
this.margin.top +
|
||||
this.measurements.axisLabelLineOffset) /
|
||||
2 || 0;
|
||||
|
||||
return `translate(15, ${yCoordinate}) rotate(-90)`;
|
||||
},
|
||||
|
||||
rectTransform() {
|
||||
const yCoordinate =
|
||||
(this.graphHeight -
|
||||
this.margin.top +
|
||||
this.measurements.axisLabelLineOffset) /
|
||||
2 +
|
||||
this.yLabelWidth / 2 || 0;
|
||||
|
||||
return `translate(0, ${yCoordinate}) rotate(-90)`;
|
||||
},
|
||||
|
||||
xPosition() {
|
||||
return (
|
||||
(this.graphWidth + this.measurements.axisLabelLineOffset) / 2 -
|
||||
this.margin.right || 0
|
||||
);
|
||||
},
|
||||
|
||||
yPosition() {
|
||||
return (
|
||||
this.graphHeight -
|
||||
this.margin.top +
|
||||
this.measurements.axisLabelLineOffset || 0
|
||||
);
|
||||
},
|
||||
|
||||
yAxisLabelSentenceCase() {
|
||||
return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
|
||||
},
|
||||
|
||||
timeString() {
|
||||
return s__('PrometheusDashboard|Time');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const bbox = this.$refs.ylabel.getBBox();
|
||||
this.yLabelWidth = bbox.width + 10; // Added some padding
|
||||
this.yLabelHeight = bbox.height + 5;
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<g class="axis-label-container">
|
||||
<line
|
||||
class="label-x-axis-line"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
x1="10"
|
||||
:y1="yPosition"
|
||||
:x2="graphWidth + 20"
|
||||
:y2="yPosition"
|
||||
/>
|
||||
<line
|
||||
class="label-y-axis-line"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
x1="10"
|
||||
y1="0"
|
||||
:x2="10"
|
||||
:y2="yPosition"
|
||||
/>
|
||||
<rect
|
||||
class="rect-axis-text"
|
||||
:transform="rectTransform"
|
||||
:width="yLabelWidth"
|
||||
:height="yLabelHeight"
|
||||
/>
|
||||
<text
|
||||
class="label-axis-text y-label-text"
|
||||
text-anchor="middle"
|
||||
:transform="textTransform"
|
||||
ref="ylabel"
|
||||
>
|
||||
{{ yAxisLabelSentenceCase }}
|
||||
</text>
|
||||
<rect
|
||||
class="rect-axis-text"
|
||||
:x="xPosition + 60"
|
||||
:y="graphHeight - 80"
|
||||
width="35"
|
||||
height="50"
|
||||
/>
|
||||
<text
|
||||
class="label-axis-text x-label-text"
|
||||
:x="xPosition + 60"
|
||||
:y="yPosition"
|
||||
dy=".35em"
|
||||
>
|
||||
{{ timeString }}
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
|
||||
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 {
|
||||
components: {
|
||||
icon,
|
||||
Icon,
|
||||
TrackLine,
|
||||
},
|
||||
props: {
|
||||
currentXCoordinate: {
|
||||
|
@ -107,11 +109,6 @@ export default {
|
|||
}
|
||||
return `series ${index + 1}`;
|
||||
},
|
||||
strokeDashArray(type) {
|
||||
if (type === 'dashed') return '6, 3';
|
||||
if (type === 'dotted') return '3, 3';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -160,28 +157,13 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<div class="popover-content">
|
||||
<table>
|
||||
<table class="prometheus-table">
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
>
|
||||
<td>
|
||||
<svg
|
||||
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>
|
||||
<track-line :track="series"/>
|
||||
<td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
|
||||
<td>
|
||||
<strong>{{ seriesMetricValue(series) }}</strong>
|
||||
</td>
|
||||
|
|
|
@ -1,204 +1,72 @@
|
|||
<script>
|
||||
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
|
||||
import TrackLine from './track_line.vue';
|
||||
import TrackInfo from './track_info.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrackLine,
|
||||
TrackInfo,
|
||||
},
|
||||
props: {
|
||||
graphWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
margin: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
measurements: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
legendTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
timeSeries: {
|
||||
type: Array,
|
||||
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: {
|
||||
translateLegendGroup(index) {
|
||||
return `translate(0, ${12 * index})`;
|
||||
},
|
||||
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;
|
||||
isStable(track) {
|
||||
return {
|
||||
'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</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"
|
||||
>
|
||||
{{ 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"
|
||||
<div class="prometheus-graph-legends prepend-left-10">
|
||||
<table class="prometheus-table">
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
:transform="translateLegendGroup(index)"
|
||||
v-if="series.shouldRenderLegend"
|
||||
:class="isStable(series)"
|
||||
>
|
||||
<line
|
||||
:stroke="series.lineColor"
|
||||
:stroke-width="measurements.legends.height"
|
||||
:stroke-dasharray="strokeDashArray(series.lineStyle)"
|
||||
:x1="measurements.legends.offsetX"
|
||||
:x2="measurements.legends.offsetX + measurements.legends.width"
|
||||
:y1="graphHeight - measurements.legends.offsetY"
|
||||
:y2="graphHeight - measurements.legends.offsetY"
|
||||
/>
|
||||
<text
|
||||
v-if="timeSeries.length > 1"
|
||||
<td>
|
||||
<strong v-if="series.renderCanary">{{ series.trackName }}</strong>
|
||||
</td>
|
||||
<track-line :track="series" />
|
||||
<td
|
||||
class="legend-metric-title"
|
||||
ref="legendTitleSvg"
|
||||
x="38"
|
||||
:y="graphHeight - 30"
|
||||
>
|
||||
{{ createSeriesString(index, series) }}
|
||||
</text>
|
||||
<text
|
||||
v-else
|
||||
class="legend-metric-title"
|
||||
ref="legendTitleSvg"
|
||||
x="38"
|
||||
:y="graphHeight - 30"
|
||||
>
|
||||
{{ legendTitle }} {{ formatMetricUsage(series) }}
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
</g>
|
||||
v-if="timeSeries.length > 1">
|
||||
<track-info
|
||||
:track="series"
|
||||
v-if="series.metricTag" />
|
||||
<track-info
|
||||
v-else
|
||||
:track="series">
|
||||
<strong>{{ legendTitle }}</strong> series {{ index + 1 }}
|
||||
</track-info>
|
||||
</td>
|
||||
<td v-else>
|
||||
<track-info :track="series">
|
||||
<strong>{{ legendTitle }}</strong>
|
||||
</track-info>
|
||||
</td>
|
||||
<template v-for="(track, trackIndex) in series.tracksLegend">
|
||||
<track-line
|
||||
: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>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { formatRelevantDigits } from '~/lib/utils/number_utils';
|
||||
|
||||
export default {
|
||||
name: 'TrackInfo',
|
||||
props: {
|
||||
track: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
summaryMetrics() {
|
||||
return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
|
||||
this.track.max,
|
||||
)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<slot>
|
||||
<strong> {{ track.metricTag }} </strong>
|
||||
</slot>
|
||||
{{ summaryMetrics }}
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'TrackLine',
|
||||
props: {
|
||||
track: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
stylizedLine() {
|
||||
if (this.track.lineStyle === 'dashed') return '6, 3';
|
||||
if (this.track.lineStyle === 'dotted') return '3, 3';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<td>
|
||||
<svg
|
||||
width="15"
|
||||
height="6">
|
||||
<line
|
||||
:stroke-dasharray="stylizedLine"
|
||||
:stroke="track.lineColor"
|
||||
stroke-width="4"
|
||||
:x1="0"
|
||||
:x2="15"
|
||||
:y1="2"
|
||||
:y2="2"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'underscore';
|
||||
|
||||
function sortMetrics(metrics) {
|
||||
return _.chain(metrics).sortBy('weight').sortBy('title').value();
|
||||
return _.chain(metrics).sortBy('title').sortBy('weight').value();
|
||||
}
|
||||
|
||||
function normalizeMetrics(metrics) {
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
import _ from 'underscore';
|
||||
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||
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 { 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 = {
|
||||
blue: ['#1f78d1', '#8fbce8'],
|
||||
|
@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
|
|||
|
||||
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
|
||||
let usedColors = [];
|
||||
let renderCanary = false;
|
||||
const timeSeriesParsed = [];
|
||||
|
||||
function pickColor(name) {
|
||||
let pick;
|
||||
|
@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
return defaultColorPalette[pick];
|
||||
}
|
||||
|
||||
return query.result.map((timeSeries, timeSeriesNumber) => {
|
||||
query.result.forEach((timeSeries, timeSeriesNumber) => {
|
||||
let metricTag = '';
|
||||
let lineColor = '';
|
||||
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()
|
||||
.range([0, graphWidth - 70]);
|
||||
if (trackName === 'Canary') {
|
||||
renderCanary = true;
|
||||
}
|
||||
|
||||
const timeSeriesScaleY = d3.scaleLinear()
|
||||
.range([graphHeight - graphHeightOffset, 0]);
|
||||
const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
|
||||
|
||||
const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
|
||||
|
||||
timeSeriesScaleX.domain(xDom);
|
||||
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 lineFunction = d3.line()
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.defined(defined)
|
||||
.curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
|
||||
.x(d => timeSeriesScaleX(d.time))
|
||||
.y(d => timeSeriesScaleY(d.value));
|
||||
|
||||
const areaFunction = d3.area()
|
||||
const areaFunction = d3
|
||||
.area()
|
||||
.defined(defined)
|
||||
.curve(d3.curveLinear)
|
||||
.x(d => timeSeriesScaleX(d.time))
|
||||
|
@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
.y1(d => timeSeriesScaleY(d.value));
|
||||
|
||||
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
|
||||
const seriesCustomizationData = query.series != null &&
|
||||
_.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
|
||||
const seriesCustomizationData =
|
||||
query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
|
||||
|
||||
if (seriesCustomizationData) {
|
||||
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
|
||||
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
|
||||
shouldRenderLegend = false;
|
||||
} else {
|
||||
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
|
||||
[lineColor, areaColor] = pickColor();
|
||||
if (timeSeriesParsed.length > 1) {
|
||||
shouldRenderLegend = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (query.track) {
|
||||
metricTag += ` - ${query.track}`;
|
||||
if (!shouldRenderLegend) {
|
||||
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),
|
||||
areaPath: areaFunction(timeSeries.values),
|
||||
timeSeriesScaleX,
|
||||
values: timeSeries.values,
|
||||
max: maximumValue,
|
||||
average: accum / timeSeries.values.length,
|
||||
lineStyle,
|
||||
lineColor,
|
||||
areaColor,
|
||||
metricTag,
|
||||
};
|
||||
trackName,
|
||||
shouldRenderLegend,
|
||||
renderCanary,
|
||||
});
|
||||
});
|
||||
|
||||
return timeSeriesParsed;
|
||||
}
|
||||
|
||||
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
|
||||
const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
|
||||
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
|
||||
), []);
|
||||
const allValues = queries.reduce(
|
||||
(allQueryResults, query) =>
|
||||
allQueryResults.concat(
|
||||
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const xDom = d3.extent(allValues, d => d.time);
|
||||
const yDom = [0, d3.max(allValues.map(d => d.value))];
|
||||
|
|
|
@ -13,8 +13,11 @@ export default function initMrNotes() {
|
|||
data() {
|
||||
const notesDataset = document.getElementById('js-vue-mr-discussions')
|
||||
.dataset;
|
||||
const noteableData = JSON.parse(notesDataset.noteableData);
|
||||
noteableData.noteableType = notesDataset.noteableType;
|
||||
|
||||
return {
|
||||
noteableData: JSON.parse(notesDataset.noteableData),
|
||||
noteableData,
|
||||
currentUserData: JSON.parse(notesDataset.currentUserData),
|
||||
notesData: JSON.parse(notesDataset.notesData),
|
||||
};
|
||||
|
|
|
@ -1190,12 +1190,12 @@ export default class Notes {
|
|||
addForm = false;
|
||||
let lineTypeSelector = '';
|
||||
rowCssToAdd =
|
||||
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
|
||||
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>';
|
||||
// In parallel view, look inside the correct left/right pane
|
||||
if (this.isParallelView()) {
|
||||
lineTypeSelector = `.${lineType}`;
|
||||
rowCssToAdd =
|
||||
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
|
||||
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>';
|
||||
}
|
||||
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
|
||||
let notesContent = targetRow.find(notesContentSelector);
|
||||
|
|
|
@ -99,6 +99,10 @@ export default {
|
|||
'js-note-target-reopen': !this.isOpen,
|
||||
};
|
||||
},
|
||||
supportQuickActions() {
|
||||
// Disable quick actions support for Epics
|
||||
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
|
||||
},
|
||||
markdownDocsPath() {
|
||||
return this.getNotesData.markdownDocsPath;
|
||||
},
|
||||
|
@ -355,7 +359,7 @@ Please check your network connection and try again.`;
|
|||
name="note[note]"
|
||||
class="note-textarea js-vue-comment-form
|
||||
js-gfm-input js-autosize markdown-area js-vue-textarea"
|
||||
data-supports-quick-actions="true"
|
||||
:data-supports-quick-actions="supportQuickActions"
|
||||
aria-label="Description"
|
||||
v-model="note"
|
||||
ref="textarea"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue