New upstream version 10.6.0+dfsg

This commit is contained in:
Pirate Praveen 2018-03-27 19:54:05 +05:30
parent 2058b5ccaf
commit 514980f938
2165 changed files with 64683 additions and 31732 deletions

View file

@ -11,11 +11,11 @@ engines:
exclude_paths:
- "lib/api/v3/*"
eslint:
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4
enabled: false
enabled: true
channel: "eslint-4"
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52"
channel: "gitlab-rubocop-0-52-1"
ratings:
paths:
- Gemfile.lock
@ -45,3 +45,4 @@ exclude_paths:
- log/
- backups/
- coverage-javascript/
- plugins/

View file

@ -36,7 +36,7 @@
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}],
"no-underscore-dangle": ["error", { "allow": ["__", "_links"]}],
"vue/html-self-closing": ["error", {
"html": {
"void": "always",

1
.gitignore vendored
View file

@ -66,3 +66,4 @@ eslint-report.html
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
/plugins/*

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
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"
.dedicated-runner: &dedicated-runner
retry: 1
@ -35,8 +35,14 @@ variables:
before_script:
- bundle --version
- date
- source scripts/utils.sh
- date
- source scripts/prepare_build.sh
- date
after_script:
- date
stages:
- build
@ -88,6 +94,26 @@ stages:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
dependencies:
- setup-test-env
stage: test
# Jobs that do not need a DB
.dedicated-no-docs-no-db-pull-cache-job: &dedicated-no-docs-no-db-pull-cache-job
<<: *dedicated-no-docs-pull-cache-job
variables:
SETUP_DB: "false"
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
- bundle exec rake $CI_JOB_NAME
.rspec-metadata: &rspec-metadata
<<: *dedicated-runner
<<: *except-docs-and-qa
@ -164,21 +190,23 @@ stages:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
##
# Trigger a package build in omnibus-gitlab repository
#
package-qa:
<<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
stage: build
cache: {}
when: manual
.gitlab-setup: &gitlab-setup
<<: *dedicated-no-docs-pull-cache-job
<<: *use-pg
variables:
CREATE_DB_USER: "true"
script:
- scripts/trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- scripts/gitaly-test-spawn
- force=yes SIZE=1 FIXTURE_PATH="db/fixtures/development" bundle exec rake gitlab:setup
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
# Review docs base
.review-docs: &review-docs
@ -201,6 +229,47 @@ package-qa:
only:
- branches
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-pull-cache-job
script:
- bundle exec rake db:migrate:reset
.migration-paths: &migration-paths
<<: *dedicated-no-docs-pull-cache-job
variables:
CREATE_DB_USER: "true"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- git checkout $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
- date
- bundle exec rake db:migrate
##
# Trigger a package build in omnibus-gitlab repository
#
package-qa:
<<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
stage: build
cache: {}
when: manual
script:
- scripts/trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
# Trigger a docs build in gitlab-docs
# Useful to preview the docs changes live
review-docs-deploy:
@ -265,7 +334,7 @@ update-tests-metadata:
flaky-examples-check:
<<: *dedicated-runner
image: ruby:2.3-alpine
image: ruby:2.4-alpine
services: []
before_script: []
variables:
@ -299,7 +368,9 @@ compile-assets:
<<: *default-cache
script:
- node --version
- date
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- date
- bundle exec rake gitlab:assets:compile
artifacts:
expire_in: 7d
@ -323,90 +394,75 @@ setup-test-env:
- tmp/tests
- config/secrets.yml
rspec-pg 0 27: *rspec-metadata-pg
rspec-pg 1 27: *rspec-metadata-pg
rspec-pg 2 27: *rspec-metadata-pg
rspec-pg 3 27: *rspec-metadata-pg
rspec-pg 4 27: *rspec-metadata-pg
rspec-pg 5 27: *rspec-metadata-pg
rspec-pg 6 27: *rspec-metadata-pg
rspec-pg 7 27: *rspec-metadata-pg
rspec-pg 8 27: *rspec-metadata-pg
rspec-pg 9 27: *rspec-metadata-pg
rspec-pg 10 27: *rspec-metadata-pg
rspec-pg 11 27: *rspec-metadata-pg
rspec-pg 12 27: *rspec-metadata-pg
rspec-pg 13 27: *rspec-metadata-pg
rspec-pg 14 27: *rspec-metadata-pg
rspec-pg 15 27: *rspec-metadata-pg
rspec-pg 16 27: *rspec-metadata-pg
rspec-pg 17 27: *rspec-metadata-pg
rspec-pg 18 27: *rspec-metadata-pg
rspec-pg 19 27: *rspec-metadata-pg
rspec-pg 20 27: *rspec-metadata-pg
rspec-pg 21 27: *rspec-metadata-pg
rspec-pg 22 27: *rspec-metadata-pg
rspec-pg 23 27: *rspec-metadata-pg
rspec-pg 24 27: *rspec-metadata-pg
rspec-pg 25 27: *rspec-metadata-pg
rspec-pg 26 27: *rspec-metadata-pg
rspec-pg 0 28: *rspec-metadata-pg
rspec-pg 1 28: *rspec-metadata-pg
rspec-pg 2 28: *rspec-metadata-pg
rspec-pg 3 28: *rspec-metadata-pg
rspec-pg 4 28: *rspec-metadata-pg
rspec-pg 5 28: *rspec-metadata-pg
rspec-pg 6 28: *rspec-metadata-pg
rspec-pg 7 28: *rspec-metadata-pg
rspec-pg 8 28: *rspec-metadata-pg
rspec-pg 9 28: *rspec-metadata-pg
rspec-pg 10 28: *rspec-metadata-pg
rspec-pg 11 28: *rspec-metadata-pg
rspec-pg 12 28: *rspec-metadata-pg
rspec-pg 13 28: *rspec-metadata-pg
rspec-pg 14 28: *rspec-metadata-pg
rspec-pg 15 28: *rspec-metadata-pg
rspec-pg 16 28: *rspec-metadata-pg
rspec-pg 17 28: *rspec-metadata-pg
rspec-pg 18 28: *rspec-metadata-pg
rspec-pg 19 28: *rspec-metadata-pg
rspec-pg 20 28: *rspec-metadata-pg
rspec-pg 21 28: *rspec-metadata-pg
rspec-pg 22 28: *rspec-metadata-pg
rspec-pg 23 28: *rspec-metadata-pg
rspec-pg 24 28: *rspec-metadata-pg
rspec-pg 25 28: *rspec-metadata-pg
rspec-pg 26 28: *rspec-metadata-pg
rspec-pg 27 28: *rspec-metadata-pg
rspec-mysql 0 27: *rspec-metadata-mysql
rspec-mysql 1 27: *rspec-metadata-mysql
rspec-mysql 2 27: *rspec-metadata-mysql
rspec-mysql 3 27: *rspec-metadata-mysql
rspec-mysql 4 27: *rspec-metadata-mysql
rspec-mysql 5 27: *rspec-metadata-mysql
rspec-mysql 6 27: *rspec-metadata-mysql
rspec-mysql 7 27: *rspec-metadata-mysql
rspec-mysql 8 27: *rspec-metadata-mysql
rspec-mysql 9 27: *rspec-metadata-mysql
rspec-mysql 10 27: *rspec-metadata-mysql
rspec-mysql 11 27: *rspec-metadata-mysql
rspec-mysql 12 27: *rspec-metadata-mysql
rspec-mysql 13 27: *rspec-metadata-mysql
rspec-mysql 14 27: *rspec-metadata-mysql
rspec-mysql 15 27: *rspec-metadata-mysql
rspec-mysql 16 27: *rspec-metadata-mysql
rspec-mysql 17 27: *rspec-metadata-mysql
rspec-mysql 18 27: *rspec-metadata-mysql
rspec-mysql 19 27: *rspec-metadata-mysql
rspec-mysql 20 27: *rspec-metadata-mysql
rspec-mysql 21 27: *rspec-metadata-mysql
rspec-mysql 22 27: *rspec-metadata-mysql
rspec-mysql 23 27: *rspec-metadata-mysql
rspec-mysql 24 27: *rspec-metadata-mysql
rspec-mysql 25 27: *rspec-metadata-mysql
rspec-mysql 26 27: *rspec-metadata-mysql
rspec-mysql 0 28: *rspec-metadata-mysql
rspec-mysql 1 28: *rspec-metadata-mysql
rspec-mysql 2 28: *rspec-metadata-mysql
rspec-mysql 3 28: *rspec-metadata-mysql
rspec-mysql 4 28: *rspec-metadata-mysql
rspec-mysql 5 28: *rspec-metadata-mysql
rspec-mysql 6 28: *rspec-metadata-mysql
rspec-mysql 7 28: *rspec-metadata-mysql
rspec-mysql 8 28: *rspec-metadata-mysql
rspec-mysql 9 28: *rspec-metadata-mysql
rspec-mysql 10 28: *rspec-metadata-mysql
rspec-mysql 11 28: *rspec-metadata-mysql
rspec-mysql 12 28: *rspec-metadata-mysql
rspec-mysql 13 28: *rspec-metadata-mysql
rspec-mysql 14 28: *rspec-metadata-mysql
rspec-mysql 15 28: *rspec-metadata-mysql
rspec-mysql 16 28: *rspec-metadata-mysql
rspec-mysql 17 28: *rspec-metadata-mysql
rspec-mysql 18 28: *rspec-metadata-mysql
rspec-mysql 19 28: *rspec-metadata-mysql
rspec-mysql 20 28: *rspec-metadata-mysql
rspec-mysql 21 28: *rspec-metadata-mysql
rspec-mysql 22 28: *rspec-metadata-mysql
rspec-mysql 23 28: *rspec-metadata-mysql
rspec-mysql 24 28: *rspec-metadata-mysql
rspec-mysql 25 28: *rspec-metadata-mysql
rspec-mysql 26 28: *rspec-metadata-mysql
rspec-mysql 27 28: *rspec-metadata-mysql
spinach-pg 0 3: *spinach-metadata-pg
spinach-pg 1 3: *spinach-metadata-pg
spinach-pg 2 3: *spinach-metadata-pg
spinach-pg 0 2: *spinach-metadata-pg
spinach-pg 1 2: *spinach-metadata-pg
spinach-mysql 0 3: *spinach-metadata-mysql
spinach-mysql 1 3: *spinach-metadata-mysql
spinach-mysql 2 3: *spinach-metadata-mysql
# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
SETUP_DB: "false"
.rake-exec: &rake-exec
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *ruby-static-analysis
stage: test
script:
- bundle exec rake $CI_JOB_NAME
spinach-mysql 0 2: *spinach-metadata-mysql
spinach-mysql 1 2: *spinach-metadata-mysql
static-analysis:
<<: *dedicated-runner
<<: *except-docs
<<: *ruby-static-analysis
stage: test
<<: *dedicated-no-docs-no-db-pull-cache-job
dependencies:
- compile-assets
- setup-test-env
script:
- scripts/static-analysis
cache:
@ -463,15 +519,6 @@ ee_compat_check:
paths:
- ee_compat_check/patches/*.patch
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
stage: test
script:
- bundle exec rake db:migrate:reset
db:migrate:reset-pg:
<<: *db-migrate-reset
<<: *use-pg
@ -486,25 +533,6 @@ db:check-schema-pg:
script:
- source scripts/schema_changed.sh
.migration-paths: &migration-paths
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
stage: test
variables:
SETUP_DB: "false"
CREATE_DB_USER: "true"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- . scripts/prepare_build.sh
- bundle exec rake db:migrate
migration:path-pg:
<<: *migration-paths
<<: *use-pg
@ -514,10 +542,7 @@ migration:path-mysql:
<<: *use-mysql
.db-rollback: &db-rollback
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
stage: test
<<: *dedicated-no-docs-pull-cache-job
script:
- bundle exec rake db:rollback STEP=119
- bundle exec rake db:migrate
@ -530,27 +555,6 @@ db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
.gitlab-setup: &gitlab-setup
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
stage: test
variables:
SIZE: "1"
SETUP_DB: "false"
CREATE_DB_USER: "true"
FIXTURE_PATH: db/fixtures/development
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- scripts/gitaly-test-spawn
- force=yes bundle exec rake gitlab:setup
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
gitlab:setup-pg:
<<: *gitlab-setup
<<: *use-pg
@ -561,10 +565,7 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
stage: test
<<: *dedicated-no-docs-no-db-pull-cache-job
dependencies: []
variables:
NODE_ENV: "production"
@ -574,7 +575,9 @@ gitlab:assets:compile:
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
script:
- date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
@ -583,17 +586,16 @@ gitlab:assets:compile:
- webpack-report/
karma:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *dedicated-no-docs-pull-cache-job
<<: *use-pg
stage: test
variables:
BABEL_ENV: "coverage"
CHROME_LOG_FILE: "chrome_debug.log"
dependencies:
- compile-assets
- setup-test-env
script:
- export BABEL_ENV=coverage CHROME_LOG_FILE=chrome_debug.log
- date
- scripts/gitaly-test-spawn
- bundle exec rake gettext:po_to_json
- date
- bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
@ -601,28 +603,29 @@ karma:
expire_in: 31d
when: always
paths:
- chrome_debug.log
- coverage-javascript/
- chrome_debug.log
- coverage-javascript/
codequality:
<<: *except-docs
<<: *pull-cache
before_script: []
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:latest
stage: test
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay
before_script: []
services:
- docker:dind
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay2
CODECLIMATE_FORMAT: json
cache: {}
dependencies: []
script:
- cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-v2 analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml
- 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
artifacts:
paths: [codeclimate.json]
expire_in: 1 week
sast:
<<: *except-docs
@ -636,11 +639,7 @@ sast:
paths: [gl-sast-report.json]
qa:internal:
<<: *dedicated-runner
<<: *except-docs
stage: test
variables:
SETUP_DB: "false"
<<: *dedicated-no-docs-no-db-pull-cache-job
services: []
script:
- cd qa/
@ -648,11 +647,7 @@ qa:internal:
- bundle exec rspec
qa:selectors:
<<: *dedicated-runner
<<: *except-docs
stage: test
variables:
SETUP_DB: "false"
<<: *dedicated-no-docs-no-db-pull-cache-job
services: []
script:
- cd qa/
@ -660,14 +655,8 @@ qa:selectors:
- bundle exec bin/qa Test::Sanity::Selectors
coverage:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *dedicated-no-docs-no-db-pull-cache-job
stage: post-test
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
script:
- bundle exec scripts/merge-simplecov
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
@ -679,26 +668,25 @@ coverage:
- coverage/assets/
lint:javascript:report:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
<<: *dedicated-no-docs-no-db-pull-cache-job
stage: post-test
dependencies:
- compile-assets
- setup-test-env
before_script: []
script:
- date
- find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- date
- yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
expire_in: 31d
paths:
- eslint-report.html
- eslint-report.html
pages:
<<: *dedicated-runner
<<: *pull-cache
<<: *dedicated-no-docs-no-db-pull-cache-job
before_script: []
stage: pages
dependencies:
@ -723,10 +711,7 @@ pages:
# Insurance in case a gem needed by one of our releases gets yanked from
# rubygems.org in the future.
cache gems:
<<: *dedicated-runner
<<: *pull-cache
variables:
SETUP_DB: "false"
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
- bundle package --all --all-platforms
artifacts:

View file

@ -1,3 +1,4 @@
<!---
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
@ -14,10 +15,7 @@ For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate.
------
--->
### Summary

View file

@ -10,15 +10,27 @@ AllCops:
Exclude:
- 'vendor/**/*'
- 'node_modules/**/*'
- 'db/*'
- 'db/**/*'
- 'db/fixtures/**/*'
- 'db/geo/*'
- 'ee/db/**/*'
- 'tmp/**/*'
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
- 'plugins/**/*'
CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a
# mutable literal (e.g. array or hash).
Style/MutableConstant:
Enabled: true
Exclude:
- 'db/migrate/**/*'
- 'db/post_migrate/**/*'
- 'ee/db/migrate/**/*'
- 'ee/db/post_migrate/**/*'
- 'ee/db/geo/migrate/**/*'
# Gitlab ###################################################################
Gitlab/ModuleWithInstanceVariables:
@ -36,3 +48,16 @@ Gitlab/ModuleWithInstanceVariables:
Gitlab/HTTParty:
Enabled: true
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
- 'config/**/*'
- 'db/**/*'
- 'features/**/*'
- 'lib/**/*.rake'
- 'qa/**/*'
- 'spec/**/*'
- 'ee/db/**/*'
- 'ee/lib/**/*.rake'
- 'ee/spec/**/*'

View file

@ -124,8 +124,8 @@ Lint/DuplicateMethods:
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb'
- 'lib/gitlab/ldap/person.rb'
- 'lib/gitlab/o_auth/user.rb'
- 'lib/gitlab/auth/ldap/person.rb'
- 'lib/gitlab/auth/o_auth/user.rb'
# Offense count: 4
Lint/InterpolationCheck:
@ -812,7 +812,7 @@ Style/TrivialAccessors:
Exclude:
- 'app/models/external_issue.rb'
- 'app/serializers/base_serializer.rb'
- 'lib/gitlab/ldap/person.rb'
- 'lib/gitlab/auth/ldap/person.rb'
- 'lib/system_check/base_check.rb'
# Offense count: 4

View file

@ -2,47 +2,188 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.5.6 (2018-03-16)
## 10.6.0 (2018-03-22)
### Security (2 changes)
### Security (4 changes)
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
- Ensure that OTP backup codes are always invalidated.
- Add verification for GitLab Pages custom domains.
- Fix GitLab Auth0 integration signing in the wrong user.
### Fixed (75 changes, 17 of them are from the community)
## 10.5.5 (2018-03-15)
### Fixed (3 changes)
- Fix missing uploads after group transfer. !17658
- Fix code and wiki search results when filename is non-ASCII.
- Remove double caching of Repository#empty?.
### Performance (2 changes)
- Adding missing indexes on taggings table.
- Add index on section_name_id on ci_build_trace_sections table.
## 10.5.4 (2018-03-08)
### Fixed (11 changes)
- Ensure users cannot create environments with leading or trailing slashes (Fixes #39885). !15273
- Fix new project path input overlapping. !16755 (George Tsiolis)
- Respect description and visibility when creating project from template. !16820 (George Tsiolis)
- Remove user notification settings for groups and projects when user leaves. !16906 (Jacopo Beschi @jacopo-beschi)
- Fix Teleporting Emoji. !16963 (Jared Deckard <jared.deckard@gmail.com>)
- Fix duplicate system notes when merging a merge request. !17035
- Fix breadcrumb on labels page for groups. !17045 (Onuwa Nnachi Isaac)
- Fix user avatar's vertical align on the issues and merge requests pages. !17072 (Laszlo Karpati)
- Fix settings panels not expanding when fragment hash linked. !17074
- Fix 404 when listing archived projects in a group where all projects have been archived. !17077 (Ashley Dumaine)
- Allow to call PUT /projects/:id API with only ci_config_path specified. !17105 (Laszlo Karpati)
- Fix long list of recipients on group request membership email. !17121 (Jacopo Beschi @jacopo-beschi)
- Remove duplicated error message on duplicate variable validation. !17135
- Keep "Import project" tab/form active when validation fails trying to import "Repo by URL". !17136
- Fixed bug with unauthenticated requests through git ssh. !17149
- Allows project rename after validation error. !17150
- Fix "Remove source branch" button in Merge request widget during merge when pipeline succeeds state. !17192
- Add missing pagination on the commit diff endpoint. !17203 (Maxime Roussin-Bélanger)
- Fix get a single pages domain when project path contains a period. !17206 (Travis Miller)
- remove avater underline. !17219 (Ken Ding)
- Allows the usage of /milestone quick action for group milestones. !17239 (Jacopo Beschi @jacopo-beschi)
- Encode branch name as binary before creating a RPC request to copy attributes. !17291
- Restart Unicorn and Sidekiq when GRPC throws 14:Endpoint read failed. !17293
- Do not persist Google Project verification flash errors after a page reload. !17299
- Ensure group issues and merge requests pages show results from subgroups when there are no results from the current group. !17312
- Prevent trace artifact migration to incur data loss. !17313
- Fixes gpg popover layout. !17323
- Return a 404 instead of 403 if the repository does not exist on disk. !17341
- Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes. !17345
- Fix Group labels load failure when there are duplicate labels present. !17353
- Allow Prometheus application to be installed from Cluster applications. !17372
- Fixes Prometheus admin configuration page. !17377
- Enable filtering MR list based on clicked label in MR sidebar. !17390
- Fix code and wiki search results pages when non-ASCII text is displayed. !17413
- Count comments on diffs and discussions as contributions for the contributions calendar. !17418 (Riccardo Padovani)
- Add Assignees vue component missing data container. !17426 (George Tsiolis)
- Update tooltip on pipeline cancel to Stop (#42946). !17444
- Removing the two factor check when the user sets a new password. !17457
- Fix quick actions for users who cannot update issues and merge requests. !17482
- Stop loading spinner on error of milestone update on issue. !17507 (Takuya Noguchi)
- Set margins around dropdown dividers to 4px. !17517
- Fix pages flaky failure by reloading stale object. !17522
- Fixed issue edit shortcut not opening edit form.
- Remove extra breadcrumb on tags. !17562 (Takuya Noguchi)
- Fix missing uploads after group transfer. !17658
- Fix markdown table showing extra column. !17669
- Ensure the API returns https links when https is configured. !17681
- Sanitize extra blank spaces used when uploading a SSH key. !40552
- Render htmlentities correctly for links not supported by Rinku.
- Keep link when redacting unauthorized object links.
- Handle empty state in Pipelines page.
- Revert Project.public_or_visible_to_user changes and only apply to snippets.
- Release libgit2 cache and open file descriptors after `git gc` run.
- Fix project dashboard showing the wrong timestamps.
- Fix "Can't modify frozen hash" error when project is destroyed.
- Fix Error 500 when viewing a commit with a GPG signature in Geo.
- Don't error out in system hook if user has `nil` datetime columns.
- Remove double caching of Repository#empty?.
- Don't delete todos or unassign issues and MRs when a user leaves a project.
- Don't cache a nil repository root ref to prevent caching issues.
- Escape HTML entities in commit messages.
- Verify project import status again before marking as failed.
- [GitHub Import] Create an empty wiki if wiki import failed.
- Create empty wiki when import from GitLab and wiki is not there.
- Make sure wiki exists when it's enabled.
- Fix broken loading state for close issue button.
- Fix code and wiki search results when filename is non-ASCII.
- Fix file upload on project show page.
- Fix squashing when a file is renamed.
- Show loading button inline in refresh button in MR widget.
- Fix close button on issues not working on mobile.
- Adds tooltip in environment names to increase readability.
- Fixed issue edit shortcut not opening edit form.
- Fix 500 error being shown when diff has context marker with invalid encoding.
- Render modified icon for moved file in changes dropdown.
- Remember assignee when moving an issue.
### Performance (1 change)
### Changed (16 changes, 9 of them are from the community)
- Allow including custom attributes in API responses. !16526 (Markus Koller)
- Apply new default and inline label design. !16956 (George Tsiolis)
- Remove whitespace from the username/email sign in form field. !17020 (Peter lauck)
- CI charts now include the current day. !17032 (Dakkaron)
- Hide CI secret variable values after saving. !17044
- Add new modal Vue component. !17108
- Asciidoc now support inter-document cross references between files in repository. !17125 (Turo Soisenniemi)
- Update issue closing pattern to allow variations in punctuation. !17198 (Vicky Chijwani)
- Add a button to deploy a runner to a Kubernetes cluster in the settings page. !17278
- Pages custom domain: allow update of key/certificate. !17376 (rfwatson)
- Clear the Labels dropdown search filter after a selection is made. !17393 (Andrew Torres)
- Hook data for pipelines includes detailed_status. !17607
- Avoid showing unnecessary Trigger checkboxes for project Integrations with only one event. !17607
- Display a link to external issue tracker when enabled.
- Allow token authentication on go-get request.
- Update SSH key link to include existing keys. (Brendan O'Leary)
### Performance (24 changes, 5 of them are from the community)
- Add catch-up background migration to migrate pipeline stages. !15741
- Move BoardNewIssue vue component. !16947 (George Tsiolis)
- Move IssuableTimeTracker vue component. !16948 (George Tsiolis)
- Move RecentSearchesDropdownContent vue component. !16951 (George Tsiolis)
- Move Assignees vue component. !16952 (George Tsiolis)
- Improve performance of pipeline page by reducing DB queries. !17168
- Store sha256 checksum to job artifacts. !17354
- Move SidebarAssignees vue component. !17398 (George Tsiolis)
- Improve database response time for user activity listing. !17454
- Use persisted/memoized value for MRs shas instead of doing git lookups. !17555
- Cache MergeRequests can_be_resolved_in_ui? git operations. !17589
- Prevent the graphs page from generating unnecessary Gitaly requests. !37602
- Use a user object in ApplicationHelper#avatar_icon where possible to avoid N+1 queries. !42800
- Submit a single batch blob RPC to Gitaly per HTTP request when viewing diffs.
- Avoid re-fetching merge-base SHA from Gitaly unnecessarily.
- Don't use ProjectsFinder in TodosFinder.
- Adding missing indexes on taggings table.
- Add index on section_name_id on ci_build_trace_sections table.
- Cache column_exists? for application settings.
- Cache table_exists?('application_settings') to reduce repeated schema reloads.
- Make --prune a configurable parameter in fetching a git remote.
- Fix timeouts loading /admin/projects page.
- Add partial indexes on todos to handle users with many todos.
- Optimize search queries on the search page by setting a limit for matching records in project scope.
### Added (30 changes, 9 of them are from the community)
- Add CommonMark markdown engine (experimental). !14835 (blackst0ne)
- API: Get references a commit is pushed to. !15026 (Robert Schilling)
- Add overview of branches and a filter for active/stale branches. !15402 (Takuya Noguchi)
- Add project export API. !15860 (Travis Miller)
- expose more metrics in merge requests api. !16589 (haseebeqx)
- #28481: Display time tracking totals on milestone page. !16753 (Riccardo Padovani)
- Add a button on the project page to set up a Kubernetes cluster and enable Auto DevOps. !16900
- Include cycle time in usage ping data. !16973
- Add ability to use external plugins as an alternative to system hooks. !17003
- Add search param to Branches API. !17005 (bunufi)
- API endpoint for importing a project export. !17025
- Display ingress IP address in the Kubernetes page. !17052
- Implemented badge API endpoints. !17082
- Allow installation of GitLab Runner with a single click. !17134
- Allow commits endpoint to work over all commits of a repository. !17182
- Display Runner IP Address. !17286
- Add archive feature to trace. !17314
- Allow maintainers to push to forks of their projects when a merge request is open. !17395
- Foreground verification of uploads and LFS objects. !17402
- Adds updated_at filter to issues and merge_requests API. !17417 (Jacopo Beschi @jacopo-beschi)
- Port /wip quick action command to Merge Request creation (on description). !17463 (Adam Pahlevi)
- Add a paragraph about security implications on Cluster's page. !17486
- Add plugins list to the system hooks page. !17518
- 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.
### Other (14 changes, 3 of them are from the community)
- Update vue component naming guidelines. !17018 (George Tsiolis)
- Added new design for promotion modals. !17197
- Update to github-linguist 5.3.x. !17241 (Ken Ding)
- update toml-rb to 1.0.0. !17259 (Ken Ding)
- Keep track of projects a user interacted with. !17327
- Enables eslint in codeclimate job. !17392
- Port Labels Select dropdown to Vue. !17411
- Add NOT NULL constraint to projects.namespace_id. !17448
- Ensure foreign keys on clusters applications. !17488
- Started translation into Turkish, Indonesian and Filipino. !17526
- Add documentation for displayed K8s Ingress IP address (#44330). !17836
- Move Ruby endpoints to OPT_OUT.
- Upgrade Workhorse to version 3.8.0 to support structured logging.
- Use host URL to build JIRA remote link icon.
## 10.5.3 (2018-03-01)
@ -269,6 +410,32 @@ entry.
- Adds empty state illustration for pending job.
## 10.4.5 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.4.4 (2018-02-16)
### Security (1 change)
- Update nokogiri to 1.8.2. !16807
### Fixed (9 changes)
- Fix 500 error when loading a merge request with an invalid comment. !16795
- Cleanup new branch/merge request form in issues. !16854
- Fix GitLab import leaving group_id on ProjectLabel. !16877
- Fix forking projects when no restricted visibility levels are defined applicationwide. !16881
- Resolve PrepareUntrackedUploads PostgreSQL syntax error. !17019
- Fixed error 500 when removing an identity with synced attributes and visiting the profile page. !17054
- Validate user namespace before saving so that errors persist on model.
- LDAP Person no longer throws exception on invalid entry.
- Fix JIRA not working when a trailing slash is included.
## 10.4.3 (2018-02-05)
### Security (4 changes)
@ -474,6 +641,13 @@ entry.
- Use a background migration for issues.closed_at.
## 10.3.8 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.3.7 (2018-02-05)
### Security (4 changes)

View file

@ -174,7 +174,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
~Geo, ~Gitaly, ~Platform, ~Monitoring, ~Release, and ~"UX".
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
@ -196,6 +196,17 @@ release. There are two levels of priority labels:
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
### Severity labels (~S1, ~S2, etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Example |
|-------|------------------------------------------|---------|
| ~S1 | Feature broken, no workaround | Unable to create an issue |
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
### Label for community contributors (~"Accepting Merge Requests")
Issues that are beneficial to our users, 'nice to haves', that we currently do
@ -397,9 +408,9 @@ For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab
is a topic of discussion. For instance if GitLab Inc. is planning to remove
features from GitLab CE to make exclusive in GitLab EE, related issues
would be labelled with ~"stewardship".
is a topic of discussion. For instance if GitLab Inc. is planning to add
features from GitLab EE to GitLab CE, related issues would be labelled with
~"stewardship".
A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue].

View file

@ -1 +1 @@
0.81.0
0.91.0

View file

@ -1 +1 @@
0.6.1
0.7.1

View file

@ -1 +1 @@
6.0.3
6.0.4

View file

@ -1 +1 @@
3.6.0
4.0.0

10
Gemfile
View file

@ -81,7 +81,7 @@ gem 'gollum-lib', '~> 4.2', require: false
gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API
gem 'grape', '~> 1.0'
@ -126,6 +126,7 @@ gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
@ -401,6 +402,7 @@ gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support
gem 'net-ssh', '~> 4.1.0'
gem 'sshkey', '~> 1.9.0'
# Required for ED25519 SSH host key support
group :ed25519 do
@ -410,11 +412,13 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 0.3.15', require: false
gem 'toml-rb', '~> 1.0.0', require: false
# Feature toggles
gem 'flipper', '~> 0.11.0'

View file

@ -131,6 +131,8 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.7.7)
commonmarker (0.17.8)
ruby-enum (~> 0.5)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
@ -285,14 +287,14 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.84.0)
gitaly-proto (0.88.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
charlock_holmes (~> 0.7.3)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.23.0b)
rugged (>= 0.25.1)
github-markup (1.6.1)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
@ -343,9 +345,9 @@ GEM
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
googleauth (0.6.2)
faraday (~> 0.12)
jwt (~> 1.4)
jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
@ -369,7 +371,7 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.8.3)
grpc (1.10.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
@ -503,7 +505,7 @@ GEM
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.12.2)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (1.0.0)
@ -601,7 +603,7 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.3.0)
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
@ -646,7 +648,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.0)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.8)
rack-accept (0.4.5)
@ -797,6 +799,8 @@ GEM
rubocop (>= 0.51)
rubocop-rspec (1.22.1)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
@ -858,10 +862,10 @@ GEM
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
signet (0.7.3)
signet (0.8.1)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (~> 1.5)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simple_po_parser (1.1.2)
simplecov (0.14.1)
@ -895,6 +899,7 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sshkey (1.9.0)
stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
@ -923,7 +928,7 @@ GEM
timfel-krb5-auth (0.8.3)
toml (0.1.2)
parslet (~> 1.5.0)
toml-rb (0.3.15)
toml-rb (1.0.0)
citrus (~> 3.0, > 3.0)
truncato (0.7.10)
htmlentities (~> 4.3.1)
@ -1018,6 +1023,7 @@ DEPENDENCIES
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
commonmarker (~> 0.17)
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
@ -1056,8 +1062,8 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.84.0)
github-linguist (~> 4.7.0)
gitaly-proto (~> 0.88.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
@ -1072,6 +1078,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.10.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@ -1192,6 +1199,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
@ -1199,7 +1207,7 @@ DEPENDENCIES
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
toml-rb (~> 0.3.15)
toml-rb (~> 1.0.0)
truncato (~> 0.7.9)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)

View file

@ -71,11 +71,15 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature freeze on the 7th for the release on the 22nd
After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
After 7th at 23:59 (Pacific Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
Any release candidate that gets created after this date can become a final release,
hence the name release candidate.
### Between the 1st and the 7th
These types of merge requests for the upcoming release need special consideration:
@ -193,11 +197,10 @@ to be backported down to the `9.5` release, you will need to assign it the
### Asking for an exception
If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
you can ask for an exception to be made.
1. a Release Manager
2. an Engineering Lead
3. an Engineering Director, the VP of Engineering, or the CTO
Go to [Release tasks issue tracker](https://gitlab.com/gitlab-org/release/tasks/issues/new) and create an issue
using the `Exception-request` issue template.
You can find who is who on the [team page](https://about.gitlab.com/team/).

View file

@ -1 +1 @@
10.5.6
10.6.0

View file

@ -9,7 +9,7 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
@ -32,7 +32,7 @@ const Api = {
},
// Return groups list. Filtered by query
groups(query, options, callback) {
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, {
params: Object.assign({

View file

@ -3,10 +3,10 @@
import AccessorUtilities from './lib/utils/accessor';
export default class Autosave {
constructor(field, key, resource) {
constructor(field, key) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
if (key.join != null) {
key = key.join('/');
}
@ -17,31 +17,27 @@ export default class Autosave {
}
restore() {
var text;
if (!this.isLocalStorageAvailable) return;
if (!this.field.length) return;
text = window.localStorage.getItem(this.key);
const text = window.localStorage.getItem(this.key);
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
if (!this.resource && this.resource !== 'issue') {
this.field.trigger('input');
} else {
// v-model does not update with jQuery trigger
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
if (field) {
field.dispatchEvent(event);
}
}
this.field.trigger('input');
// v-model does not update with jQuery trigger
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
field.dispatchEvent(event);
}
save() {
var text;
text = this.field.val();
if (!this.field.length) return;
const text = this.field.val();
if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
return window.localStorage.setItem(this.key, text);

View file

@ -2,7 +2,7 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@ -50,10 +50,8 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', (e) => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu-content').length) {
$('.js-awards-block.current').removeClass('current');
}
if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu'));
@ -241,9 +239,9 @@ class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length;
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
if (isInIssuePage() && !isMainAwardsBlock) {
if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
@ -295,8 +293,16 @@ class AwardsHandler {
}
}
isVueMRDiscussions() {
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
}
isInVueNoteablePage() {
return isInIssuePage() || this.isVueMRDiscussions();
}
getVotesBlock() {
if (isInIssuePage()) {
if (this.isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
@ -314,7 +320,7 @@ class AwardsHandler {
}
getAwardUrl() {
return this.getVotesBlock().data('award-url');
return this.getVotesBlock().data('awardUrl');
}
checkMutuality(votesBlock, emoji) {

View file

@ -2,7 +2,7 @@ import Clipboard from 'clipboard';
function showTooltip(target, title) {
const $target = $(target);
const originalTitle = $target.data('original-title');
const originalTitle = $target.data('originalTitle');
if (!$target.data('hideTooltip')) {
$target

View file

@ -43,7 +43,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
const $form = $(e.target).closest('form');
const $submitButton = $form.find('input[type=submit], button[type=submit]').first();
if (!$submitButton.attr('disabled')) {
if (!$submitButton.prop('disabled')) {
$submitButton.trigger('click', [e]);
if (!isInIssuePage()) {

View file

@ -40,7 +40,7 @@ $.fn.requiresInput = function requiresInput() {
// based on the option selected
function hideOrShowHelpBlock(form) {
const selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('options-parent') === 'groups') {
if (selected.length && selected.data('optionsParent') === 'groups') {
form.find('.help-block').hide();
} else if (selected.length) {
form.find('.help-block').show();

View file

@ -12,7 +12,7 @@ $(() => {
const $container = $(container);
$container
.find('.js-toggle-button .fa')
.find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
@ -22,7 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.target.classList.toggle('open');
e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();

View file

@ -7,7 +7,7 @@ function onError() {
return flash;
}
function loadBalsamiqFile() {
export default function loadBalsamiqFile() {
const viewer = document.getElementById('js-balsamiq-viewer');
if (!(viewer instanceof Element)) return;
@ -17,5 +17,3 @@ function loadBalsamiqFile() {
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
}
$(loadBalsamiqFile);

View file

@ -1,3 +1,3 @@
import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook);
export default renderNotebook;

View file

@ -1,3 +1,3 @@
import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF);
export default renderPDF;

View file

@ -1,8 +1,8 @@
/* eslint-disable no-new */
import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
});
};

View file

@ -1,6 +1,6 @@
import Renderer from './3d_viewer';
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
@ -16,4 +16,4 @@ document.addEventListener('DOMContentLoaded', () => {
viewer.changeObjectMaterials(target.dataset.type);
});
});
});
};

View file

@ -5,6 +5,7 @@ import axios from '../../lib/utils/axios_utils';
export default class BlobViewer {
constructor() {
BlobViewer.initAuxiliaryViewer();
BlobViewer.initRichViewer();
this.initMainViewers();
}
@ -16,6 +17,38 @@ export default class BlobViewer {
BlobViewer.loadViewer(auxiliaryViewer);
}
static initRichViewer() {
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
if (!viewer || !viewer.dataset.richType) return;
const initViewer = promise => promise
.then(module => module.default(viewer))
.catch((error) => {
Flash('Error loading file viewer.');
throw error;
});
switch (viewer.dataset.richType) {
case 'balsamiq':
initViewer(import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer'));
break;
case 'notebook':
initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'));
break;
case 'pdf':
initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'));
break;
case 'sketch':
initViewer(import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer'));
break;
case 'stl':
initViewer(import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer'));
break;
default:
break;
}
}
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;

View file

@ -4,16 +4,16 @@ import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
$(() => {
export default () => {
const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language');
const currentAction = $('.js-file-title').data('current-action');
const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix');
const blobLanguage = editBlobForm.data('blobLanguage');
const currentAction = $('.js-file-title').data('currentAction');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
new NewCommitForm(editBlobForm);
@ -34,4 +34,4 @@ $(() => {
if (deleteBlobForm.length) {
new NewCommitForm(deleteBlobForm);
}
});
};

View file

@ -59,7 +59,7 @@ export default class EditBlob {
if (paneId === '#preview') {
this.$toggleButton.hide();
axios.post(currentLink.data('preview-url'), {
axios.post(currentLink.data('previewUrl'), {
content: this.editor.getValue(),
})
.then(({ data }) => {

View file

@ -1,4 +1,5 @@
<script>
/* eslint-disable vue/require-default-prop */
import './issue_card_inner';
import eventHub from '../eventhub';
@ -34,6 +35,9 @@ export default {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
data() {
return {
@ -88,6 +92,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true"
/>

View file

@ -1,6 +1,6 @@
<script>
import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
@ -15,6 +15,11 @@ export default {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: false,
default: 0,
},
disabled: {
type: Boolean,
required: true,
@ -170,6 +175,7 @@ export default {
<loading-icon />
</div>
<board-new-issue
:group-id="groupId"
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
@ -185,6 +191,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />

View file

@ -1,11 +1,21 @@
/* global ListIssue */
<script>
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
},
props: {
groupId: {
type: Number,
required: false,
default: 0,
},
list: {
type: Object,
required: true,
@ -15,8 +25,21 @@ export default {
return {
title: '',
error: false,
selectedProject: {},
};
},
computed: {
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
},
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
submit(e) {
e.preventDefault();
@ -30,6 +53,7 @@ export default {
labels,
subscribed: true,
assignees: [],
project_id: this.selectedProject.id,
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
@ -58,43 +82,62 @@ export default {
this.title = '';
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
setSelectedProject(selectedProject) {
this.selectedProject = selectedProject;
},
},
mounted() {
this.$refs.input.focus();
},
template: `
<div class="card board-new-issue-form">
};
</script>
<template>
<div class="board-new-issue-form">
<div class="card">
<form @submit="submit($event)">
<div class="flash-container"
v-if="error">
<div
class="flash-container"
v-if="error"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label class="label-light"
:for="list.id + '-title'">
<label
class="label-light"
:for="list.id + '-title'"
>
Title
</label>
<input class="form-control"
<input
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'" />
:id="list.id + '-title'"
/>
<project-select
v-if="groupId"
:group-id="groupId"
/>
<div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left"
<button
class="btn btn-success pull-left"
type="submit"
:disabled="title === ''"
ref="submit-button">
:disabled="disabled"
ref="submit-button"
>
Submit issue
</button>
<button class="btn btn-default pull-right"
<button
class="btn btn-default pull-right"
type="button"
@click="cancel">
@click="cancel"
>
Cancel
</button>
</div>
</form>
</div>
`,
};
</div>
</template>

View file

@ -2,10 +2,11 @@
import Vue from 'vue';
import Flash from '../../flash';
import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees';
import assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
@ -95,7 +96,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
})
.catch(() => {
this.loadingAssignees = false;
return new Flash('An error occurred while saving assignees');
Flash(__('An error occurred while saving assignees'));
});
},
},

View file

@ -31,6 +31,10 @@ gl.issueBoards.IssueCardInner = Vue.extend({
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
},
},
data() {
return {
@ -64,7 +68,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit;
},
cardUrl() {
return `${this.issueLinkBase}/${this.issue.iid}`;
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) {
@ -148,7 +158,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
class="card-number"
v-if="issueId"
>
{{ issueId }}
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
</span>
</h4>
<div class="card-assignee">

View file

@ -1,7 +1,6 @@
/* eslint-disable no-new */
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility';
@ -36,7 +35,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id],
}).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert');
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => {
list.removeIssue(issue);

View file

@ -1,5 +1,6 @@
/* eslint-disable func-names, no-new, space-before-function-paren, one-var,
promise/catch-or-return */
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
@ -24,13 +25,13 @@ $(document).off('created.label').on('created.label', (e, label) => {
gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () {
const $this = $(this);
new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath'));
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-list-labels-path'))
.then((resp) => {
callback(resp);
axios.get($this.attr('data-list-labels-path'))
.then(({ data }) => {
callback(data);
});
},
renderRow (label) {

View file

@ -0,0 +1,127 @@
<script>
/* global ListIssue */
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
components: {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
loading: true,
selectedProject: {},
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
},
mounted() {
$(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
},
clicked: ({ $el, e }) => {
e.preventDefault();
this.selectedProject = {
id: $el.data('project-id'),
name: $el.data('project-name'),
};
eventHub.$emit('setSelectedProject', this.selectedProject);
},
selectable: true,
data: (term, callback) => {
this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => {
this.loading = false;
callback(projects);
});
},
renderRow(project) {
return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
},
text: project => project.name,
});
},
};
</script>
<template>
<div>
<label class="label-light prepend-top-10">
Project
</label>
<div
ref="projectsDropdown"
class="dropdown"
>
<button
class="dropdown-menu-toggle wide"
type="button"
data-toggle="dropdown"
aria-expanded="false"
>
{{ selectedProjectName }}
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button
aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
</button>
</div>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
placeholder="Search projects"
/>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</template>

View file

@ -1,7 +1,6 @@
/* eslint-disable no-new */
import Vue from 'vue';
import Flash from '../../../flash';
import { __ } from '../../../locale';
const Store = gl.issueBoards.BoardsStore;
@ -25,7 +24,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
},
computed: {
updateUrl() {
return this.issueUpdate;
return this.issueUpdate.replace(':project_path', this.issue.project.path);
},
},
methods: {
@ -33,19 +32,23 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
const issue = this.issue;
const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id);
let labelIds = this.issue.labels
let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const data = {
issue: {
label_ids: labelIds,
},
};
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert');
Flash(__('Failed to remove issue from board, please try again.'));
lists.forEach((list) => {
list.addIssue(issue);

View file

@ -1,9 +1,13 @@
/* eslint-disable class-methods-use-this */
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
export default class FilteredSearchBoards extends gl.FilteredSearchManager {
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super('boards');
super({
page: 'boards',
stateFiltersSelector: '.issues-state-filters',
});
this.store = store;
this.updateUrl = updateUrl;

View file

@ -2,15 +2,18 @@
import _ from 'underscore';
import Vue from 'vue';
import Flash from '../flash';
import { __ } from '../locale';
import Flash from '~/flash';
import { __ } from '~/locale';
import '~/vue_shared/models/label';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue';
import './models/label';
import './models/list';
import './models/milestone';
import './models/project';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
@ -22,9 +25,9 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';
import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
$(() => {
export default () => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
@ -87,7 +90,7 @@ $(() => {
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup();
Store.disabled = this.disabled;
@ -177,6 +180,7 @@ $(() => {
return {
modal: ModalStore.store,
store: Store.state,
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
computed: {
@ -230,10 +234,11 @@ $(() => {
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
v-if="canAdminList"
@click="openModal">
Add issues
</button>
</div>
`,
});
});
};

View file

@ -1,6 +1,8 @@
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
import sortableConfig from '../../sortable/sortable_config';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@ -18,19 +20,14 @@ gl.issueBoards.onEnd = () => {
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
const defaultSortOptions = {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
};
onEnd: gl.issueBoards.onEnd,
});
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
return defaultSortOptions;

View file

@ -4,6 +4,7 @@
/* global ListAssignee */
import Vue from 'vue';
import IssueProject from './project';
class ListIssue {
constructor (obj, defaultAvatar) {
@ -23,6 +24,12 @@ class ListIssue {
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
if (obj.project) {
this.project = new IssueProject(obj.project);
}
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@ -105,8 +112,11 @@ class ListIssue {
data.issue.label_ids = [''];
}
return Vue.http.patch(url, data);
const projectPath = this.project ? this.project.path : '';
return Vue.http.patch(url.replace(':project_path', projectPath), data);
}
}
window.ListIssue = ListIssue;
export default ListIssue;

View file

@ -0,0 +1,6 @@
export default class IssueProject {
constructor(obj) {
this.id = obj.id;
this.path = obj.path;
}
}

View file

@ -2,7 +2,7 @@
/* global List */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { getUrlParamsArray } from '../../lib/utils/common_utils';
import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};

View file

@ -75,6 +75,7 @@ export default class AjaxVariableList {
if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables);
this.variableList.hideValues();
} else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data);

View file

@ -178,6 +178,10 @@ export default class VariableList {
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
}
hideValues() {
this.secretValues.updateDom(false);
}
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.

View file

@ -37,10 +37,11 @@ export default class Clusters {
clusterStatusReason,
helpPath,
ingressHelpPath,
ingressDnsHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
@ -98,6 +99,7 @@ export default class Clusters {
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
},
});
},

View file

@ -36,10 +36,6 @@
type: String,
required: false,
},
description: {
type: String,
required: true,
},
status: {
type: String,
required: false,
@ -148,7 +144,7 @@
class="table-section section-wrap"
role="gridcell"
>
<div v-html="description"></div>
<slot name="description"></slot>
</div>
<div
class="table-section table-button-footer section-align-top"

View file

@ -2,10 +2,16 @@
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import {
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
export default {
components: {
applicationRow,
clipboardButton,
},
props: {
applications: {
@ -23,6 +29,11 @@
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
@ -43,19 +54,16 @@
false,
);
},
helmTillerDescription() {
return _.escape(s__(
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const descriptionParagraph = _.escape(s__(
`ClusterIntegration|Ingress gives you a way to route requests to services based on the
request host or path, centralizing a number of services into a single entrypoint.`,
));
const extraCostParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
@ -83,9 +91,6 @@
);
return `
<p>
${descriptionParagraph}
</p>
<p>
${extraCostParagraph}
</p>
@ -94,12 +99,6 @@
</p>
`;
},
gitlabRunnerDescription() {
return _.escape(s__(
`ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
and send the results back to GitLab.`,
));
},
prometheusDescription() {
return sprintf(
_.escape(s__(
@ -118,7 +117,10 @@
</script>
<template>
<section class="settings no-animate expanded">
<section
id="cluster-applications"
class="settings no-animate expanded"
>
<div class="settings-header">
<h4>
{{ s__('ClusterIntegration|Applications') }}
@ -136,33 +138,137 @@
id="helm"
:title="applications.helm.title"
title-link="https://docs.helm.sh/"
:description="helmTillerDescription"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
/>
>
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) }}
</div>
</application-row>
<application-row
id="ingress"
:id="ingressId"
:title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:description="ingressDescription"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
/>
>
<div slot="description">
<p>
{{ s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`) }}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }}
</label>
<div
v-if="ingressExternalIp"
class="input-group"
>
<input
type="text"
id="ingress-ip-address"
class="form-control js-ip-address"
:value="ingressExternalIp"
readonly
/>
<span class="input-group-btn">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
class="js-clipboard-btn"
/>
</span>
</div>
<input
v-else
type="text"
class="form-control js-ip-address"
readonly
value="?"
/>
</div>
<p
v-if="!ingressExternalIp"
class="settings-message js-no-ip-message"
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
<a
:href="ingressHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
<div
v-else
v-html="ingressDescription"
>
</div>
</div>
</application-row>
<application-row
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
/>
>
<div
slot="description"
v-html="prometheusDescription"
>
</div>
</application-row>
<application-row
id="runner"
:title="applications.runner.title"
title-link="https://docs.gitlab.com/runner/"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
>
<div slot="description">
{{ s__(`ClusterIntegration|GitLab Runner connects to this
project's repository and executes CI/CD jobs,
pushing results back and deploying,
applications to production.`) }}
</div>
</application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests

View file

@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored';
export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';

View file

@ -1,4 +1,5 @@
import { s__ } from '../../locale';
import { INGRESS } from '../constants';
export default class ClusterStore {
constructor() {
@ -21,6 +22,7 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
externalIp: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
@ -40,9 +42,10 @@ export default class ClusterStore {
};
}
setHelpPaths(helpPath, ingressHelpPath) {
setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
this.state.ingressDnsHelpPath = ingressDnsHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
@ -64,6 +67,7 @@ export default class ClusterStore {
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
@ -76,6 +80,10 @@ export default class ClusterStore {
status,
statusReason,
};
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
}
});
}
}

View file

@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
// Width where images must fits in, for 2-up this gets divided by 2
const availWidth = 900;

View file

@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => {
pipelineTableViewEl.appendChild(table.$el);
}
}
});
};

View file

@ -7,7 +7,6 @@
mixins: [
pipelinesMixin,
],
props: {
endpoint: {
type: String,
@ -21,10 +20,6 @@
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
@ -46,23 +41,14 @@
},
computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
@ -93,25 +79,22 @@
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
:label="s__('Pipelines|Loading Pipelines')"
size="3"
v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath"
/>
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
<svg-blank-state
v-else-if="shouldRenderErrorState"
:svg-path="errorStateSvgPath"
:message="s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)"
/>
<div
class="table-holder"
v-if="shouldRenderTable"
v-else-if="shouldRenderTable"
>
<pipelines-table-component
:pipelines="state.pipelines"

View file

@ -1,52 +1,36 @@
/* eslint-disable func-names, wrap-iife, consistent-return,
no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
prefer-template, object-shorthand, prefer-arrow-callback */
import { pluralize } from './lib/utils/text_utility';
import { localTimeAgo } from './lib/utils/datetime_utility';
import Pager from './pager';
import axios from './lib/utils/axios_utils';
export default (function () {
const CommitsList = {};
export default class CommitsList {
constructor(limit = 0) {
this.timer = null;
CommitsList.timer = null;
CommitsList.init = function (limit) {
this.$contentList = $('.content_list');
$('body').on('click', '.day-commits-table li.commit', function (e) {
if (e.target.nodeName !== 'A') {
location.href = $(this).attr('url');
e.stopPropagation();
return false;
}
});
Pager.init(parseInt(limit, 10), false, false, this.processCommits);
Pager.init(parseInt(limit, 10), false, false, this.processCommits.bind(this));
this.content = $('#commits-list');
this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val();
return this.initSearch();
};
this.initSearch();
}
CommitsList.initSearch = function () {
initSearch() {
this.timer = null;
return this.searchField.keyup((function (_this) {
return function () {
clearTimeout(_this.timer);
return _this.timer = setTimeout(_this.filterResults, 500);
};
})(this));
};
this.searchField.on('keyup', () => {
clearTimeout(this.timer);
this.timer = setTimeout(this.filterResults.bind(this), 500);
});
}
CommitsList.filterResults = function () {
filterResults() {
const form = $('.commits-search-form');
const search = CommitsList.searchField.val();
if (search === CommitsList.lastSearch) return Promise.resolve();
const commitsUrl = form.attr('action') + '?' + form.serialize();
CommitsList.content.fadeTo('fast', 0.5);
const search = this.searchField.val();
if (search === this.lastSearch) return Promise.resolve();
const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
this.content.fadeTo('fast', 0.5);
const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, {
[obj.name]: obj.value,
}), {});
@ -55,9 +39,9 @@ export default (function () {
params,
})
.then(({ data }) => {
CommitsList.lastSearch = search;
CommitsList.content.html(data.html);
CommitsList.content.fadeTo('fast', 1.0);
this.lastSearch = search;
this.content.html(data.html);
this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
history.replaceState({
@ -65,16 +49,16 @@ export default (function () {
}, document.title, commitsUrl);
})
.catch(() => {
CommitsList.content.fadeTo('fast', 1.0);
CommitsList.lastSearch = null;
this.content.fadeTo('fast', 1.0);
this.lastSearch = null;
});
};
}
// Prepare loaded data.
CommitsList.processCommits = (data) => {
processCommits(data) {
let processedData = data;
const $processedData = $(processedData);
const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last();
const $commitsHeadersLast = this.$contentList.find('li.js-commit-header').last();
const lastShownDay = $commitsHeadersLast.data('day');
const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first();
const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day');
@ -97,7 +81,5 @@ export default (function () {
localTimeAgo($processedData.find('.js-timeago'));
return processedData;
};
return CommitsList;
})();
}
}

View file

@ -13,6 +13,6 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
// custom jQuery functions
$.fn.extend({
disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); },
enable() { return $(this).removeAttr('disabled').removeClass('disabled'); },
disable() { return $(this).prop('disabled', true).addClass('disabled'); },
enable() { return $(this).prop('disabled', false).removeClass('disabled'); },
});

View file

@ -6,5 +6,5 @@ import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'vendor/jquery.waitforimages';
import 'jquery.waitforimages';
import 'select2/select2';

View file

@ -1,4 +1,5 @@
import Vue from 'vue';
import '../vue_shared/vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false;

View file

@ -13,7 +13,7 @@ export default class Compare {
$dropdown = $(dropdown);
return $dropdown.glDropdown({
selectable: true,
fieldName: $dropdown.data('field-name'),
fieldName: $dropdown.data('fieldName'),
filterable: true,
id: function(obj, $el) {
return $el.data('id');

View file

@ -9,7 +9,7 @@ export default function initCompareAutocomplete() {
$dropdown = $(this);
selected = $dropdown.data('selected');
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer);
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
@ -25,7 +25,7 @@ export default function initCompareAutocomplete() {
selectable: true,
filterable: true,
filterRemote: true,
fieldName: $dropdown.data('field-name'),
fieldName: $dropdown.data('fieldName'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;

View file

@ -37,7 +37,7 @@
>
<div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}

View file

@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate);
$(() => {
export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
gl.cycleAnalyticsApp = new Vue({
new Vue({ // eslint-disable-line no-new
el: '#cycle-analytics',
name: 'CycleAnalytics',
components: {
@ -132,4 +132,4 @@ $(() => {
},
},
});
});
};

View file

@ -1,7 +1,7 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});

View file

@ -1,3 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
@ -65,11 +68,13 @@ export default class Diff {
}
const file = $target.parents('.diff-file');
const link = file.data('blob-diff-path');
const link = file.data('blobDiffPath');
const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view };
$.get(link, params, response => $target.parent().replaceWith(response));
axios.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() => flash(__('An error occurred while loading diff')));
}
openAnchoredDiff(cb) {
@ -116,7 +121,7 @@ export default class Diff {
}
// eslint-disable-next-line class-methods-use-this
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
return $('.inline-parallel-buttons a.active').data('viewType');
}
// eslint-disable-next-line class-methods-use-this
lineNumbers(line) {

View file

@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
}
$.scrollTo($target, {
offset: 0
offset: -150
});
}
},

View file

@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
document.dispatchEvent(new CustomEvent('refreshVueNotes'));
this.updateTooltip();
})

View file

@ -14,8 +14,9 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
$(() => {
export default () => {
const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
const projectPath = projectPathHolder.dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
@ -67,12 +68,14 @@ $(() => {
gl.diffNotesCompileComponents();
new Vue({
el: '#resolve-count-app',
components: {
'resolve-count': ResolveCount
}
});
if (!hasVueMRDiscussionsCookie()) {
new Vue({
el: '#resolve-count-app',
components: {
'resolve-count': ResolveCount
},
});
}
$(window).trigger('resize.nav');
});
};

View file

@ -8,8 +8,8 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
}
resolve(noteId) {
@ -45,6 +45,7 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
}

View file

@ -1,648 +1,85 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import MergeRequest from './merge_request';
import Flash from './flash';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import initNotes from './init_notes';
import initIssuableSidebar from './init_issuable_sidebar';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import ShortcutsIssuable from './shortcuts_issuable';
import Diff from './diff';
import SearchAutocomplete from './search_autocomplete';
var Dispatcher;
(function() {
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
this.initFieldErrors();
this.initPageScripts();
}
Dispatcher.prototype.initPageScripts = function() {
var path, shortcut_handler;
const page = $('body').attr('data-page');
if (!page) {
return false;
}
const fail = () => Flash('Error loading dynamic module');
const callDefault = m => m.default();
path = page.split(':');
shortcut_handler = null;
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
switch (page) {
case 'projects:environments:metrics':
import('./pages/projects/environments/metrics')
.then(callDefault)
.catch(fail);
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
case 'projects:issues:show':
shortcut_handler = true;
break;
case 'projects:milestones:index':
import('./pages/projects/milestones/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:show':
import('./pages/projects/milestones/show')
.then(callDefault)
.catch(fail);
break;
case 'groups:milestones:show':
import('./pages/groups/milestones/show')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:milestones:show':
import('./pages/dashboard/milestones/show')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:issues':
import('./pages/dashboard/issues')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:merge_requests':
import('./pages/dashboard/merge_requests')
.then(callDefault)
.catch(fail);
break;
case 'groups:issues':
import('./pages/groups/issues')
.then(callDefault)
.catch(fail);
break;
case 'groups:merge_requests':
import('./pages/groups/merge_requests')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:todos:index':
import('./pages/dashboard/todos/index')
.then(callDefault)
.catch(fail);
break;
case 'admin:jobs:index':
import('./pages/admin/jobs/index')
.then(callDefault)
.catch(fail);
break;
case 'admin:projects:index':
import('./pages/admin/projects/index/index')
.then(callDefault)
.catch(fail);
break;
case 'admin:users:index':
import('./pages/admin/users/shared')
.then(callDefault)
.catch(fail);
break;
case 'admin:users:show':
import('./pages/admin/users/shared')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:projects:index':
case 'dashboard:projects:starred':
import('./pages/dashboard/projects')
.then(callDefault)
.catch(fail);
break;
case 'explore:projects:index':
case 'explore:projects:trending':
case 'explore:projects:starred':
import('./pages/explore/projects')
.then(callDefault)
.catch(fail);
break;
case 'explore:groups:index':
import('./pages/explore/groups')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:new':
case 'projects:milestones:create':
import('./pages/projects/milestones/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:edit':
case 'projects:milestones:update':
import('./pages/projects/milestones/edit')
.then(callDefault)
.catch(fail);
break;
case 'groups:milestones:new':
case 'groups:milestones:create':
import('./pages/groups/milestones/new')
.then(callDefault)
.catch(fail);
break;
case 'groups:milestones:edit':
case 'groups:milestones:update':
import('./pages/groups/milestones/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:compare:show':
import('./pages/projects/compare/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:branches:new':
import('./pages/projects/branches/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:branches:create':
import('./pages/projects/branches/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:branches:index':
import('./pages/projects/branches/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:issues:new':
import('./pages/projects/issues/new')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:issues:edit':
import('./pages/projects/issues/edit')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:merge_requests:creations:new':
import('./pages/projects/merge_requests/creations/new')
.then(callDefault)
.catch(fail);
case 'projects:merge_requests:creations:diffs':
import('./pages/projects/merge_requests/creations/diffs')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:merge_requests:edit':
import('./pages/projects/merge_requests/edit')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:tags:new':
import('./pages/projects/tags/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:snippets:show':
import('./pages/projects/snippets/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:snippets:new':
case 'projects:snippets:create':
import('./pages/projects/snippets/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:snippets:edit':
case 'projects:snippets:update':
import('./pages/projects/snippets/edit')
.then(callDefault)
.catch(fail);
break;
case 'snippets:new':
import('./pages/snippets/new')
.then(callDefault)
.catch(fail);
break;
case 'snippets:edit':
import('./pages/snippets/edit')
.then(callDefault)
.catch(fail);
break;
case 'snippets:create':
import('./pages/snippets/new')
.then(callDefault)
.catch(fail);
break;
case 'snippets:update':
import('./pages/snippets/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:releases:edit':
import('./pages/projects/releases/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:merge_requests:show':
new Diff();
new ZenMode();
initIssuableSidebar();
initNotes();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
shortcut_handler = new ShortcutsIssuable(true);
break;
case 'dashboard:activity':
import('./pages/dashboard/activity')
.then(callDefault)
.catch(fail);
break;
case 'projects:commit:show':
import('./pages/projects/commit/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:commit:pipelines':
import('./pages/projects/commit/pipelines')
.then(callDefault)
.catch(fail);
break;
case 'projects:activity':
import('./pages/projects/activity')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:commits:show':
import('./pages/projects/commits/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:show':
shortcut_handler = true;
break;
case 'projects:edit':
import('./pages/projects/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:imports:show':
import('./pages/projects/imports/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:pipelines:new':
case 'projects:pipelines:create':
import('./pages/projects/pipelines/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
import('./pages/projects/pipelines/builds')
.then(callDefault)
.catch(fail);
break;
case 'groups:activity':
import('./pages/groups/activity')
.then(callDefault)
.catch(fail);
break;
case 'groups:show':
shortcut_handler = true;
break;
case 'groups:group_members:index':
import('./pages/groups/group_members/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:project_members:index':
import('./pages/projects/project_members')
.then(callDefault)
.catch(fail);
break;
case 'groups:create':
case 'groups:new':
import('./pages/groups/new')
.then(callDefault)
.catch(fail);
break;
case 'groups:edit':
import('./pages/groups/edit')
.then(callDefault)
.catch(fail);
break;
case 'admin:groups:create':
case 'admin:groups:new':
import('./pages/admin/groups/new')
.then(callDefault)
.catch(fail);
break;
case 'admin:groups:edit':
import('./pages/admin/groups/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:tree:show':
import('./pages/projects/tree/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:find_file:show':
import('./pages/projects/find_file/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:blob:show':
import('./pages/projects/blob/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:blame:show':
import('./pages/projects/blame/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'groups:labels:new':
import('./pages/groups/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'groups:labels:edit':
import('./pages/groups/labels/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:new':
import('./pages/projects/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:edit':
import('./pages/projects/labels/edit')
.then(callDefault)
.catch(fail);
break;
case 'groups:labels:index':
import('./pages/groups/labels/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:index':
import('./pages/projects/labels/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:network:show':
// Ensure we don't create a particular shortcut handler here. This is
// already created, where the network graph is created.
shortcut_handler = true;
break;
case 'projects:forks:new':
import('./pages/projects/forks/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:artifacts:browse':
import('./pages/projects/artifacts/browse')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:artifacts:file':
import('./pages/projects/artifacts/file')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'help:index':
import('./pages/help')
.then(callDefault)
.catch(fail);
break;
case 'search:show':
import('./pages/search/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:settings:repository:show':
import('./pages/projects/settings/repository/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:settings:ci_cd:show':
import('./pages/projects/settings/ci_cd/show')
.then(callDefault)
.catch(fail);
break;
case 'groups:settings:ci_cd:show':
import('./pages/groups/settings/ci_cd/show')
.then(callDefault)
.catch(fail);
break;
case 'ci:lints:create':
case 'ci:lints:show':
import('./pages/ci/lints')
.then(callDefault)
.catch(fail);
break;
case 'users:show':
import('./pages/users/show')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show')
.then(callDefault)
.catch(fail);
break;
case 'snippets:show':
import('./pages/snippets/show')
.then(callDefault)
.catch(fail);
break;
case 'import:fogbugz:new_user_map':
import('./pages/import/fogbugz/new_user_map')
.then(callDefault)
.catch(fail);
break;
case 'profiles:personal_access_tokens:index':
import('./pages/profiles/personal_access_tokens')
.then(callDefault)
.catch(fail);
break;
case 'admin:impersonation_tokens:index':
import('./pages/admin/impersonation_tokens')
.then(callDefault)
.catch(fail);
break;
case 'projects:clusters:show':
case 'projects:clusters:update':
case 'projects:clusters:destroy':
import('./pages/projects/clusters/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:clusters:index':
import('./pages/projects/clusters/index')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:groups:index':
import('./pages/dashboard/groups/index')
.then(callDefault)
.catch(fail);
break;
}
switch (path[0]) {
case 'sessions':
import('./pages/sessions')
.then(callDefault)
.catch(fail);
break;
case 'omniauth_callbacks':
import('./pages/omniauth_callbacks')
.then(callDefault)
.catch(fail);
break;
case 'admin':
import('./pages/admin')
.then(callDefault)
.catch(fail);
switch (path[1]) {
case 'broadcast_messages':
import('./pages/admin/broadcast_messages')
.then(callDefault)
.catch(fail);
break;
case 'cohorts':
import('./pages/admin/cohorts')
.then(callDefault)
.catch(fail);
break;
case 'groups':
switch (path[2]) {
case 'show':
import('./pages/admin/groups/show')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'projects':
import('./pages/admin/projects')
.then(callDefault)
.catch(fail);
break;
case 'labels':
switch (path[2]) {
case 'new':
import('./pages/admin/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'edit':
import('./pages/admin/labels/edit')
.then(callDefault)
.catch(fail);
break;
}
case 'abuse_reports':
import('./pages/admin/abuse_reports')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'profiles':
import('./pages/profiles/index')
.then(callDefault)
.catch(fail);
break;
case 'projects':
import('./pages/projects')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
switch (path[1]) {
case 'compare':
import('./pages/projects/compare')
.then(callDefault)
.catch(fail);
break;
case 'create':
case 'new':
import('./pages/projects/new')
.then(callDefault)
.catch(fail);
break;
case 'wikis':
import('./pages/projects/wikis')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
}
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
new Shortcuts();
}
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(fail);
}
};
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
};
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
};
return Dispatcher;
})();
})();
export default function initDispatcher() {
return new Dispatcher();
function initSearch() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
}
function initFieldErrors() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
}
function initPageShortcuts(page) {
const pagesWithCustomShortcuts = [
'projects:activity',
'projects:artifacts:browse',
'projects:artifacts:file',
'projects:blame:show',
'projects:blob:show',
'projects:commit:show',
'projects:commits:show',
'projects:find_file:show',
'projects:issues:edit',
'projects:issues:index',
'projects:issues:new',
'projects:issues:show',
'projects:merge_requests:creations:diffs',
'projects:merge_requests:creations:new',
'projects:merge_requests:edit',
'projects:merge_requests:index',
'projects:merge_requests:show',
'projects:network:show',
'projects:show',
'projects:tree:show',
'groups:show',
];
if (pagesWithCustomShortcuts.indexOf(page) === -1) {
new Shortcuts();
}
}
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
}
function initPerformanceBar() {
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(() => Flash('Error loading performance bar module'));
}
}
export default () => {
initSearch();
initFieldErrors();
const page = $('body').attr('data-page');
if (page) {
initPageShortcuts(page);
initGFMInput();
initPerformanceBar();
}
};

View file

@ -4,10 +4,7 @@ function addMousetrapClick(el, key) {
el.addEventListener('click', () => Mousetrap.trigger(key));
}
function domContentLoaded() {
export default () => {
addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?');
addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
};

View file

@ -17,9 +17,9 @@ class DueDateSelect {
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
this.fieldName = $dropdown.data('field-name');
this.abilityName = $dropdown.data('ability-name');
this.issueUpdateURL = $dropdown.data('issue-update');
this.fieldName = $dropdown.data('fieldName');
this.abilityName = $dropdown.data('abilityName');
this.issueUpdateURL = $dropdown.data('issueUpdate');
this.rawSelectedDate = null;
this.displayedDate = null;

View file

@ -1,8 +1,9 @@
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '../../lib/utils/text_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@ -21,14 +22,18 @@
export default {
components: {
userAvatarLink,
'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent,
UserAvatarLink,
CommitComponent,
ActionsComponent,
ExternalUrlComponent,
StopComponent,
RollbackComponent,
TerminalButtonComponent,
MonitoringButtonComponent,
},
directives: {
tooltip,
},
props: {
@ -443,7 +448,11 @@
v-if="!model.isFolder"
class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
<span class="flex-truncate-child">{{ model.name }}</span>
<span
class="flex-truncate-child"
v-tooltip
:title="model.name"
>{{ model.name }}</span>
</a>
<span
v-else

View file

@ -2,8 +2,8 @@
/**
* Render environments table.
*/
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {

View file

@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: '#environments-folder-list-view',
components: {
environmentsFolderApp,
@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});

View file

@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: '#environments-list-view',
components: {
environmentsComponent,
@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});

View file

@ -25,7 +25,7 @@ export default {
if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === '';
}
this.isParallelView = Cookies.get('diff_view') === 'parallel';

View file

@ -1,101 +0,0 @@
import eventHub from '../event_hub';
export default {
name: 'RecentSearchesDropdownContent',
props: {
items: {
type: Array,
required: true,
},
isLocalStorageAvailable: {
type: Boolean,
required: false,
default: true,
},
allowedKeys: {
type: Array,
required: true,
},
},
computed: {
processedItems() {
return this.items.map((item) => {
const { tokens, searchToken }
= gl.FilteredSearchTokenizer.processTokens(item, this.allowedKeys);
const resultantTokens = tokens.map(token => ({
prefix: `${token.key}:`,
suffix: `${token.symbol}${token.value}`,
}));
return {
text: item,
tokens: resultantTokens,
searchToken,
};
});
},
hasItems() {
return this.items.length > 0;
},
},
methods: {
onItemActivated(text) {
eventHub.$emit('recentSearchesItemSelected', text);
},
onRequestClearRecentSearches(e) {
// Stop the dropdown from closing
e.stopPropagation();
eventHub.$emit('requestClearRecentSearches');
},
},
template: `
<div>
<div
v-if="!isLocalStorageAvailable"
class="dropdown-info-note">
This feature requires local storage to be enabled
</div>
<ul v-else-if="hasItems">
<li
v-for="(item, index) in processedItems"
:key="index">
<button
type="button"
class="filtered-search-history-dropdown-item"
@click="onItemActivated(item.text)">
<span>
<span
v-for="(token, tokenIndex) in item.tokens"
class="filtered-search-history-dropdown-token">
<span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span>
</span>
</span>
<span class="filtered-search-history-dropdown-search-token">
{{ item.searchToken }}
</span>
</button>
</li>
<li class="divider"></li>
<li>
<button
type="button"
class="filtered-search-history-clear-button"
@click="onRequestClearRecentSearches($event)">
Clear recent searches
</button>
</li>
</ul>
<div
v-else
class="dropdown-info-note">
You don't have any recent searches
</div>
</div>
`,
};

View file

@ -0,0 +1,104 @@
<script>
import eventHub from '../event_hub';
import FilteredSearchTokenizer from '../filtered_search_tokenizer';
export default {
name: 'RecentSearchesDropdownContent',
props: {
items: {
type: Array,
required: true,
},
isLocalStorageAvailable: {
type: Boolean,
required: false,
default: true,
},
allowedKeys: {
type: Array,
required: true,
},
},
computed: {
processedItems() {
return this.items.map((item) => {
const { tokens, searchToken }
= FilteredSearchTokenizer.processTokens(item, this.allowedKeys);
const resultantTokens = tokens.map(token => ({
prefix: `${token.key}:`,
suffix: `${token.symbol}${token.value}`,
}));
return {
text: item,
tokens: resultantTokens,
searchToken,
};
});
},
hasItems() {
return this.items.length > 0;
},
},
methods: {
onItemActivated(text) {
eventHub.$emit('recentSearchesItemSelected', text);
},
onRequestClearRecentSearches(e) {
// Stop the dropdown from closing
e.stopPropagation();
eventHub.$emit('requestClearRecentSearches');
},
},
};
</script>
<template>
<div>
<div
v-if="!isLocalStorageAvailable"
class="dropdown-info-note">
This feature requires local storage to be enabled
</div>
<ul v-else-if="hasItems">
<li
v-for="(item, index) in processedItems"
:key="`processed-items-${index}`"
>
<button
type="button"
class="filtered-search-history-dropdown-item"
@click="onItemActivated(item.text)">
<span>
<span
class="filtered-search-history-dropdown-token"
v-for="(token, index) in item.tokens"
:key="`dropdown-token-${index}`"
>
<span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
</span>
</span>
<span class="filtered-search-history-dropdown-search-token">
{{ item.searchToken }}
</span>
</button>
</li>
<li class="divider"></li>
<li>
<button
type="button"
class="filtered-search-history-clear-button"
@click="onRequestClearRecentSearches($event)">
Clear recent searches
</button>
</li>
</ul>
<div
v-else
class="dropdown-info-note">
You don't have any recent searches
</div>
</div>
</template>

View file

@ -1,9 +1,10 @@
import Flash from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
class DropdownEmoji extends gl.FilteredSearchDropdown {
export default class DropdownEmoji extends FilteredSearchDropdown {
constructor(options = {}) {
super(options);
this.config = {
@ -49,7 +50,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown {
itemClicked(e) {
super.itemClicked(e, (selected) => {
const name = selected.querySelector('.js-data-value').innerText.trim();
return gl.DropdownUtils.getEscapedText(name);
return DropdownUtils.getEscapedText(name);
});
}
@ -76,6 +77,3 @@ class DropdownEmoji extends gl.FilteredSearchDropdown {
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownEmoji = DropdownEmoji;

View file

@ -1,14 +1,17 @@
import Filter from '~/droplab/plugins/filter';
import './filtered_search_dropdown';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
class DropdownHint extends gl.FilteredSearchDropdown {
export default class DropdownHint extends FilteredSearchDropdown {
constructor(options = {}) {
const { input, tokenKeys } = options;
super(options);
this.config = {
Filter: {
template: 'hint',
filterFunction: gl.DropdownUtils.filterHint.bind(null, {
filterFunction: DropdownUtils.filterHint.bind(null, {
input,
allowedKeys: tokenKeys.getKeys(),
}),
@ -45,10 +48,10 @@ class DropdownHint extends gl.FilteredSearchDropdown {
});
if (searchTerms.length > 0) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
}
this.dismissDropdown();
this.dispatchInputEvent();
@ -73,6 +76,3 @@ class DropdownHint extends gl.FilteredSearchDropdown {
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownHint = DropdownHint;

View file

@ -1,9 +1,10 @@
import Flash from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
class DropdownNonUser extends gl.FilteredSearchDropdown {
export default class DropdownNonUser extends FilteredSearchDropdown {
constructor(options = {}) {
const { input, endpoint, symbol, preprocessing } = options;
super(options);
@ -21,7 +22,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
},
},
Filter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
filterFunction: DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
template: 'title',
},
};
@ -30,7 +31,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
itemClicked(e) {
super.itemClicked(e, (selected) => {
const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
return `${this.symbol}${DropdownUtils.getEscapedText(title)}`;
});
}
@ -45,6 +46,3 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownNonUser = DropdownNonUser;

View file

@ -1,9 +1,11 @@
import Flash from '../flash';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import FilteredSearchDropdown from './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
import DropdownUtils from './dropdown_utils';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
class DropdownUser extends gl.FilteredSearchDropdown {
export default class DropdownUser extends FilteredSearchDropdown {
constructor(options = {}) {
const { tokenKeys } = options;
super(options);
@ -55,8 +57,8 @@ class DropdownUser extends gl.FilteredSearchDropdown {
}
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get());
const query = DropdownUtils.getSearchInput(this.input);
const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get());
let value = lastToken || '';
@ -77,6 +79,3 @@ class DropdownUser extends gl.FilteredSearchDropdown {
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownUser = DropdownUser;

View file

@ -1,7 +1,10 @@
import _ from 'underscore';
import FilteredSearchContainer from './container';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
class DropdownUtils {
export default class DropdownUtils {
static getEscapedText(text) {
let escapedText = text;
const hasSpace = text.indexOf(' ') !== -1;
@ -24,7 +27,7 @@ class DropdownUtils {
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
const searchInput = gl.DropdownUtils.getSearchInput(input);
const searchInput = DropdownUtils.getSearchInput(input);
const title = updatedItem.title.toLowerCase();
let value = searchInput.toLowerCase();
@ -114,9 +117,9 @@ class DropdownUtils {
static filterHint(config, item) {
const { input, allowedKeys } = config;
const updatedItem = item;
const searchInput = gl.DropdownUtils.getSearchQuery(input);
const searchInput = DropdownUtils.getSearchQuery(input);
const { lastToken, tokens } =
gl.FilteredSearchTokenizer.processTokens(searchInput, allowedKeys);
FilteredSearchTokenizer.processTokens(searchInput, allowedKeys);
const lastKey = lastToken.key || lastToken || '';
const allowMultiple = item.type === 'array';
const itemInExistingTokens = tokens.some(t => t.key === item.hint);
@ -140,7 +143,7 @@ class DropdownUtils {
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
// Return boolean based on whether it was set
@ -190,7 +193,7 @@ class DropdownUtils {
}
} else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const inputValue = input && input.value;
@ -211,7 +214,7 @@ class DropdownUtils {
static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
const { right } = DropdownUtils.getInputSelectionPosition(filteredSearchInput);
return inputValue.slice(0, right);
}
@ -252,6 +255,3 @@ class DropdownUtils {
};
}
}
window.gl = window.gl || {};
gl.DropdownUtils = DropdownUtils;

View file

@ -1,10 +0,0 @@
import './dropdown_emoji';
import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';

View file

@ -1,6 +1,9 @@
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
export default class FilteredSearchDropdown {
constructor({ droplab, dropdown, input, filter }) {
this.droplab = droplab;
this.hookId = input && input.id;
@ -30,11 +33,11 @@ class FilteredSearchDropdown {
const { selected } = e.detail;
if (selected.tagName === 'LI' && selected.innerHTML) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
const dataValueSet = DropdownUtils.setDataValueIfSelected(this.filter, selected);
if (!dataValueSet) {
const value = getValueFunction(selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.resetFilters();
@ -108,6 +111,9 @@ class FilteredSearchDropdown {
if (hook) {
const data = hook.list.data || [];
if (!data) return;
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
@ -117,6 +123,3 @@ class FilteredSearchDropdown {
}
}
}
window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown;

View file

@ -1,15 +1,33 @@
import _ from 'underscore';
import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
import DropdownUtils from './dropdown_utils';
import DropdownHint from './dropdown_hint';
import DropdownEmoji from './dropdown_emoji';
import DropdownNonUser from './dropdown_non_user';
import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) {
export default class FilteredSearchDropdownManager {
constructor({
baseEndpoint = '',
tokenizer,
page,
isGroup,
isGroupAncestor,
isGroupDecendent,
filteredSearchTokenKeys,
}) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
this.setupMapping();
@ -33,43 +51,43 @@ class FilteredSearchDropdownManager {
const allowedMappings = {
hint: {
reference: null,
gl: 'DropdownHint',
gl: DropdownHint,
element: this.container.querySelector('#js-dropdown-hint'),
},
};
const availableMappings = {
author: {
reference: null,
gl: 'DropdownUser',
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-author'),
},
assignee: {
reference: null,
gl: 'DropdownUser',
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-assignee'),
},
milestone: {
reference: null,
gl: 'DropdownNonUser',
gl: DropdownNonUser,
extraArguments: {
endpoint: `${this.baseEndpoint}/milestones.json`,
endpoint: this.getMilestoneEndpoint(),
symbol: '%',
},
element: this.container.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
gl: DropdownNonUser,
extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json`,
endpoint: this.getLabelsEndpoint(),
symbol: '~',
preprocessing: gl.DropdownUtils.duplicateLabelPreprocessing,
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
element: this.container.querySelector('#js-dropdown-label'),
},
'my-reaction': {
reference: null,
gl: 'DropdownEmoji',
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
};
@ -83,14 +101,26 @@ class FilteredSearchDropdownManager {
this.mapping = allowedMappings;
}
getMilestoneEndpoint() {
const endpoint = `${this.baseEndpoint}/milestones.json`;
return endpoint;
}
getLabelsEndpoint() {
const endpoint = `${this.baseEndpoint}/labels.json`;
return endpoint;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
input.value = '';
if (clicked) {
gl.FilteredSearchVisualTokens.moveInputToTheRight();
FilteredSearchVisualTokens.moveInputToTheRight();
}
}
@ -131,9 +161,9 @@ class FilteredSearchDropdownManager {
const extraArguments = mappingKey.extraArguments || {};
const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new gl[glClass](<arguments>)`
// Passing glArguments to `new glClass(<arguments>)`
mappingKey.reference =
new (Function.prototype.bind.apply(gl[glClass], [null, glArguments]))();
new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
}
if (firstLoad) {
@ -171,7 +201,7 @@ class FilteredSearchDropdownManager {
}
setDropdown() {
const query = gl.DropdownUtils.getSearchQuery(true);
const query = DropdownUtils.getSearchQuery(true);
const { lastToken, searchToken } =
this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys());
@ -216,6 +246,3 @@ class FilteredSearchDropdownManager {
this.droplab.destroy();
}
}
window.gl = window.gl || {};
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;

View file

@ -1,21 +1,34 @@
import _ from 'underscore';
import {
getParameterByName,
getUrlParamsArray,
} from '~/lib/utils/common_utils';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import DropdownUtils from './dropdown_utils';
class FilteredSearchManager {
export default class FilteredSearchManager {
constructor({
page,
isGroup = false,
isGroupAncestor = false,
isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
this.isGroup = false;
this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
@ -66,14 +79,15 @@ class FilteredSearchManager {
});
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(
this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
this.tokenizer,
this.page,
this.isGroup,
this.filteredSearchTokenKeys,
);
this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager({
baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
tokenizer: this.tokenizer,
page: this.page,
isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor,
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
@ -85,7 +99,6 @@ class FilteredSearchManager {
this.bindEvents();
this.loadSearchParamsFromURL();
this.dropdownManager.setDropdown();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
@ -197,8 +210,8 @@ class FilteredSearchManager {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
@ -206,8 +219,8 @@ class FilteredSearchManager {
if (backspaceCount === 2) {
backspaceCount = 0;
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
this.filteredSearchInput.value = FilteredSearchVisualTokens.getLastTokenPartial();
FilteredSearchVisualTokens.removeLastTokenPartial();
}
}
@ -275,7 +288,7 @@ class FilteredSearchManager {
e.stopImmediatePropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
FilteredSearchVisualTokens.selectToken(button, true);
this.removeSelectedToken();
}
}
@ -287,7 +300,7 @@ class FilteredSearchManager {
const isElementTokensContainer = e.target.classList.contains('tokens-container');
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
gl.FilteredSearchVisualTokens.moveInputToTheRight();
FilteredSearchVisualTokens.moveInputToTheRight();
this.dropdownManager.resetDropdowns();
}
}
@ -300,13 +313,13 @@ class FilteredSearchManager {
if (token && canEdit) {
e.preventDefault();
e.stopPropagation();
gl.FilteredSearchVisualTokens.editToken(token);
FilteredSearchVisualTokens.editToken(token);
this.tokenChange();
}
}
toggleClearSearchButton() {
const query = gl.DropdownUtils.getSearchQuery();
const query = DropdownUtils.getSearchQuery();
const hidden = 'hidden';
const hasHidden = this.clearSearchButton.classList.contains(hidden);
@ -318,7 +331,7 @@ class FilteredSearchManager {
}
handleInputPlaceholder() {
const query = gl.DropdownUtils.getSearchQuery();
const query = DropdownUtils.getSearchQuery();
const placeholder = 'Search or filter results...';
const currentPlaceholder = this.filteredSearchInput.placeholder;
@ -338,7 +351,7 @@ class FilteredSearchManager {
}
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken();
FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.dropdownManager.updateCurrentDropdownOffset();
@ -358,7 +371,7 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
const { tokenName, tokenValue } = DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
}
@ -386,12 +399,12 @@ class FilteredSearchManager {
const { tokens, searchToken }
= this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys());
const { isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) {
tokens.forEach((t) => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
});
const fragments = searchToken.split(':');
@ -404,10 +417,10 @@ class FilteredSearchManager {
const searchTerms = inputValues.join(' ');
input.value = input.value.replace(searchTerms, '');
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
input.value = input.value.replace(`${tokenKey}:`, '');
}
} else {
@ -415,7 +428,7 @@ class FilteredSearchManager {
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
// Trim the last space as seen in the if statement above
input.value = input.value.replace(searchToken, '').trim();
@ -431,7 +444,7 @@ class FilteredSearchManager {
saveCurrentSearchQuery() {
// Don't save before we have fetched the already saved searches
this.fetchingRecentSearchesPromise.then(() => {
const searchQuery = gl.DropdownUtils.getSearchQuery();
const searchQuery = DropdownUtils.getSearchQuery();
if (searchQuery.length > 0) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
this.recentSearchesService.save(resultantSearches);
@ -447,7 +460,7 @@ class FilteredSearchManager {
}
loadSearchParamsFromURL() {
const urlParams = gl.utils.getUrlParamsArray();
const urlParams = getUrlParamsArray();
const params = this.getAllParams(urlParams);
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
@ -463,7 +476,7 @@ class FilteredSearchManager {
if (condition) {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
FilteredSearchVisualTokens.addFilterVisualToken(
condition.tokenKey,
condition.value,
canEdit,
@ -492,7 +505,7 @@ class FilteredSearchManager {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
canEdit,
@ -503,7 +516,7 @@ class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
@ -511,7 +524,7 @@ class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
@ -543,13 +556,13 @@ class FilteredSearchManager {
search(state = null) {
const paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery();
const searchQuery = DropdownUtils.getSearchQuery();
this.saveCurrentSearchQuery();
const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
const currentState = state || gl.utils.getParameterByName('state') || 'opened';
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
@ -628,6 +641,3 @@ class FilteredSearchManager {
return true;
}
}
window.gl = window.gl || {};
gl.FilteredSearchManager = FilteredSearchManager;

View file

@ -1,6 +1,6 @@
import './filtered_search_token_keys';
class FilteredSearchTokenizer {
export default class FilteredSearchTokenizer {
static processTokens(input, allowedKeys) {
// Regex extracts `(token):(symbol)(value)`
// Values that start with a double quote must end in a double quote (same for single)
@ -50,6 +50,3 @@ class FilteredSearchTokenizer {
};
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer;

View file

@ -1,10 +1,12 @@
import _ from 'underscore';
import AjaxCache from '../lib/utils/ajax_cache';
import AjaxCache from '~/lib/utils/ajax_cache';
import { objectToQueryString } from '~/lib/utils/common_utils';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
import DropdownUtils from './dropdown_utils';
class FilteredSearchVisualTokens {
export default class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
const lastVisualToken = inputLi && inputLi.previousElementSibling;
@ -15,6 +17,21 @@ class FilteredSearchVisualTokens {
};
}
/**
* Returns a computed API endpoint
* and query string composed of values from endpointQueryParams
* @param {String} endpoint
* @param {String} endpointQueryParams
*/
static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
if (!endpointQueryParams) {
return endpoint;
}
const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
return `${endpoint}?${queryString}`;
}
static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
@ -74,7 +91,7 @@ class FilteredSearchVisualTokens {
let processed = labels;
if (!labels.preprocessed) {
processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels);
processed = DropdownUtils.duplicateLabelPreprocessing(labels);
AjaxCache.override(labelsEndpoint, processed);
processed.preprocessed = true;
}
@ -85,12 +102,15 @@ class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
const labelsEndpoint = `${baseEndpoint}/labels.json`;
const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
`${baseEndpoint}/labels.json`,
filteredSearchInput.dataset.endpointQueryParams,
);
return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
.then((labels) => {
const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue);
const matchingLabel = (labels || []).find(label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue);
if (!matchingLabel) {
return;
@ -259,11 +279,11 @@ class FilteredSearchVisualTokens {
static tokenizeInput() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (input.value) {
if (isLastVisualTokenValid) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
FilteredSearchVisualTokens.addSearchVisualToken(input.value);
} else {
FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
}
@ -324,12 +344,12 @@ class FilteredSearchVisualTokens {
if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!isLastVisualTokenValid) {
const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
const lastPartial = FilteredSearchVisualTokens.getLastTokenPartial();
FilteredSearchVisualTokens.removeLastTokenPartial();
FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
}
tokenContainer.removeChild(inputLi);
@ -337,6 +357,3 @@ class FilteredSearchVisualTokens {
}
}
}
window.gl = window.gl || {};
gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;

View file

@ -1,5 +1,5 @@
import Vue from 'vue';
import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content';
import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue';
import eventHub from './event_hub';
class RecentSearchesRoot {
@ -33,7 +33,7 @@ class RecentSearchesRoot {
this.vm = new Vue({
el: this.wrapperElement,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
RecentSearchesDropdownContent,
},
data() { return state; },
template: `

View file

@ -485,7 +485,7 @@ GitLabDropdown = (function() {
$target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
!$target.data('is-link')) {
!$target.data('isLink')) {
e.stopPropagation();
return false;
} else {
@ -607,7 +607,20 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
var field, fieldName, html, selected, text, url, value;
var field, fieldName, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
}
}
// Hide element
if (this.options.hideRow && this.options.hideRow(value)) {
rowHidden = true;
}
if (group == null) {
group = false;
}
@ -616,6 +629,7 @@ GitLabDropdown = (function() {
index = false;
}
html = document.createElement('li');
if (data === 'divider' || data === 'separator') {
html.className = data;
return html;
@ -631,11 +645,9 @@ GitLabDropdown = (function() {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) {
selected = true;

View file

@ -1,5 +1,4 @@
/* global autosize */
import autosize from 'autosize';
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
@ -13,7 +12,7 @@ export default class GLForm {
this.destroy();
// Setup the form
this.setupForm();
this.form.data('gl-form', this);
this.form.data('glForm', this);
}
destroy() {
@ -22,7 +21,7 @@ export default class GLForm {
if (this.autoComplete) {
this.autoComplete.destroy();
}
this.form.data('gl-form', null);
this.form.data('glForm', null);
}
setupForm() {

View file

@ -1,3 +1,8 @@
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
@ -5,13 +10,13 @@ export default class GpgBadges {
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({
url: form.data('signatures-path'),
data: form.serialize(),
}).done((response) => {
response.signatures.forEach((signature) => {
const params = parseQueryStringIntoObject(form.serialize());
return axios.get(form.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
});
})
.catch(() => flash(__('An error occurred while loading commits')));
}
}

View file

@ -1,4 +0,0 @@
import Chart from 'vendor/Chart';
// export to global scope
window.Chart = Chart;

View file

@ -30,11 +30,11 @@
default: 'bottom',
},
/**
* value could either be number or string
* as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount`
* are always number
*/
* value could either be number or string
* as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount`
* are always number
*/
value: {
type: [Number, String],
required: false,

View file

@ -1,13 +1,14 @@
import axios from './lib/utils/axios_utils';
import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils';
import { normalizeHeaders } from './lib/utils/common_utils';
export default function groupsSelect() {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('all-available');
const skipGroups = $select.data('skip-groups') || [];
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
$select.select2({
placeholder: 'Search for a group',
multiple: $select.hasClass('multiselect'),
@ -17,24 +18,23 @@ export default function groupsSelect() {
dataType: 'json',
quietMillis: 250,
transport(params) {
return $.ajax(params)
.then((data, status, xhr) => {
const results = data || [];
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then((res) => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
params.success({
results,
pagination: {
more,
},
};
})
.then(params.success)
.fail(params.error);
});
}).catch(params.error);
},
data(search, page) {
return {

View file

@ -1,6 +1,8 @@
// We will render the icons list here
if ($('#user-content-gitlab-icons').length > 0) {
const $iconsHeader = $('#user-content-gitlab-icons');
const $iconsList = $('<div id="iconsList">ICONS</div>');
$($iconsList).insertAfter($iconsHeader.parent());
}
export default () => {
if ($('#user-content-gitlab-icons').length > 0) {
const $iconsHeader = $('#user-content-gitlab-icons');
const $iconsList = $('<div id="iconsList">ICONS</div>');
$($iconsList).insertAfter($iconsHeader.parent());
}
};

Some files were not shown because too many files have changed in this diff Show more