Commit e4a31011 authored by Andreas Kämmerle's avatar Andreas Kämmerle

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into...

Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into update-template-name-via-sentence-case

# Conflicts:
#	.gitlab/issue_templates/Feature proposal.md
parents d505b488 c489d53b

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

......@@ -69,9 +69,3 @@ rules:
FunctionExpression:
parameters: 1
body: 1
## Destructuring: https://eslint.org/docs/rules/prefer-destructuring
prefer-destructuring: off
## no-restricted-globals: https://eslint.org/docs/rules/no-restricted-globals
no-restricted-globals: off
## no-multi-assign: https://eslint.org/docs/rules/no-multi-assign
no-multi-assign: off
......@@ -14,3 +14,12 @@ lib/gitlab/gitaly_client/ref_service.rb
lib/gitlab/gitaly_client/commit_service.rb
lib/gitlab/git/commit.rb
lib/gitlab/git/tag.rb
ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/api/epics.rb
ee/lib/api/geo_nodes.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
ee/spec/**/*
......@@ -29,7 +29,7 @@ eslint-report.html
/app/assets/javascripts/locale/**/app.js
/backups/*
/config/aws.yml
/config/database.yml
/config/database*.yml
/config/gitlab.yml
/config/gitlab_ci.yml
/config/initializers/rack_attack.rb
......@@ -76,3 +76,4 @@ eslint-report.html
/.rspec
/plugins/*
/.gitlab_pages_secret
package-lock.json
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
......@@ -220,18 +220,6 @@ stages:
paths:
- log/development.log
# Review docs base
.review-docs: &review-docs
<<: *dedicated-runner
<<: *except-qa
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
when: manual
only:
- branches
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
......@@ -264,29 +252,56 @@ package-and-qa:
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-omnibus
SCRIPT_NAME: trigger-build
retry: 0
script:
- ./$SCRIPT_NAME
- ./$SCRIPT_NAME omnibus
when: manual
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:
<<: *review-docs
stage: build
# Review docs base
.review-docs: &review-docs
<<: *dedicated-runner
<<: *single-script-job
variables:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
environment:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy-manual:
<<: *review-docs
stage: build
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
when: manual
only:
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
<<: *except-docs-and-qa
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
review-docs-deploy:
<<: *review-docs
stage: post-test
script:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
only:
- /(^docs[\/-].*|.*-docs$)/
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
<<: *except-qa
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
......@@ -295,9 +310,10 @@ review-docs-cleanup:
environment:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
when: manual
script:
- gem install gitlab --no-ri --no-rdoc
- ./SCRIPT_NAME cleanup
- ./$SCRIPT_NAME cleanup
##
# Trigger a docker image build in CNG (Cloud Native GitLab) repository
......@@ -394,7 +410,11 @@ compile-assets:
- date
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- date
- free -m
- bundle exec rake gitlab:assets:compile
variables:
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
artifacts:
expire_in: 7d
paths:
......@@ -411,6 +431,7 @@ setup-test-env:
script:
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- scripts/gitaly-test-build # Do not use 'bundle exec' here
- BUNDLE_GEMFILE=Gemfile.rails5 bundle install $BUNDLE_INSTALL_FLAGS
artifacts:
expire_in: 7d
paths:
......@@ -586,6 +607,12 @@ downtime_check:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
rails5_gemfile_lock_check:
<<: *dedicated-no-docs-no-db-pull-cache-job
<<: *except-docs-and-qa
script:
- scripts/rails5-gemfile-lock-check
ee_compat_check:
<<: *rake-exec
except:
......@@ -658,10 +685,13 @@ gitlab:assets:compile:
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
script:
- date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date
- free -m
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
......@@ -801,8 +831,6 @@ lint:javascript:report:
- 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:
......
### Description
### Problem to solve
(Include problem, use cases, benefits, and/or goals)
### Further details
(Include use cases, benefits, and/or goals)
### Proposal
(Proposal)
### What does success look like, and how can we measure that?
(If no way to measure success, link to an issue that will implement a way to measure this)
### Links / references
......
......@@ -39,6 +39,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary
#### Links
| Description | Link |
......
Add a description of your merge request here. Merge requests without an adequate
description will not be reviewed until one is added.
## Database Checklist
## Database checklist
When adding migrations:
......@@ -31,7 +31,7 @@ When removing columns, tables, indexes or other structures:
- [ ] Removed these in a post-deployment migration
- [ ] Made sure the application no longer uses (or ignores) these structures
## General Checklist
## General checklist
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
......
......@@ -487,7 +487,7 @@ Style/EmptyLiteral:
- 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/gitaly_client.rb'
- 'scripts/trigger-build-omnibus'
- 'scripts/trigger-build'
- 'spec/features/merge_requests/versions_spec.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/lib/gitlab/request_context_spec.rb'
......
This diff is collapsed.
This diff is collapsed.
......@@ -35,7 +35,7 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'doorkeeper-openid_connect', '~> 1.5'
gem 'omniauth', '~> 1.8'
gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
......@@ -93,6 +93,10 @@ gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
......@@ -104,6 +108,7 @@ gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 1.2'
gem 'mini_magick'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
......@@ -129,7 +134,7 @@ gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.7.1'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2'
gem 'gitlab-markup', '~> 1.6.4'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2'
......@@ -225,7 +230,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0'
# Sanitize user input
gem 'sanitize', '~> 2.0'
gem 'sanitize', '~> 4.6.5'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
......@@ -294,7 +299,6 @@ gem 'peek-sidekiq', '~> 1.0.3'
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
......@@ -328,7 +332,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.5.0'
gem 'factory_bot_rails', '~> 4.8.2'
gem 'rspec-rails', '~> 3.6.0'
gem 'rspec-rails', '~> 3.7.0'
gem 'rspec-retry', '~> 0.4.5'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
......@@ -342,7 +346,7 @@ group :development, :test do
gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'selenium-webdriver', '~> 3.5'
gem 'selenium-webdriver', '~> 3.12'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
......@@ -374,7 +378,7 @@ end
group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0'
gem 'email_spec', '~> 2.2.0'
gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2'
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
......@@ -384,7 +388,7 @@ group :test do
gem 'test-prof', '~> 0.2.5'
end
gem 'octokit', '~> 4.8'
gem 'octokit', '~> 4.9'
gem 'mail_room', '~> 0.9.1'
......@@ -404,18 +408,17 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support
gem 'net-ssh', '~> 4.2.0'
gem 'net-ssh', '~> 5.0'
gem 'sshkey', '~> 1.9.0'
# Required for ED25519 SSH host key support
group :ed25519 do
gem 'rbnacl-libsodium'
gem 'rbnacl', '~> 4.0'
gem 'ed25519', '~> 1.2'
gem 'bcrypt_pbkdf', '~> 1.0'
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.103.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -49,7 +49,6 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
asana (0.6.0)
faraday (~> 0.9)
......@@ -77,7 +76,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
bcrypt (3.1.11)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
......@@ -115,7 +114,7 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.6)
childprocess (0.7.0)
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
......@@ -172,15 +171,17 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2)
railties (>= 4.2)
doorkeeper-openid_connect (1.3.0)
doorkeeper-openid_connect (1.5.0)
doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
ed25519 (1.2.4)
email_reply_trimmer (0.1.6)
email_spec (1.6.0)
email_spec (2.2.0)
htmlentities (~> 4.3.3)
launchy (~> 2.1)
mail (~> 2.2)
mail (~> 2.7)
encryptor (3.0.0)
equalizer (0.0.11)
erubis (2.7.0)
......@@ -281,7 +282,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.100.0)
gitaly-proto (0.103.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -294,15 +295,15 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.2)
gitlab-gollum-lib (4.2.7.5)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 3.1)
sanitize (~> 2.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
......@@ -310,7 +311,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.3)
gitlab-markup (1.6.4)
gitlab-styles (2.3.2)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
......@@ -358,12 +359,16 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-path-helpers (1.0.1)
activesupport (~> 4)
grape-path-helpers (1.0.5)
activesupport (>= 4, < 5.1)
grape (~> 1.0)
rake (~> 12)
grape_logging (1.7.0)
grape
graphiql-rails (1.4.10)
railties
sprockets-rails
graphql (1.8.1)
grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
......@@ -422,12 +427,10 @@ GEM
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
json (1.8.6)
json-jwt (1.9.2)
json-jwt (1.9.4)
activesupport
aes_key_wrap
bindata
securecompare
url_safe_base64
json-schema (2.8.0)
addressable (>= 2.4)
jwt (1.5.6)
......@@ -446,7 +449,6 @@ GEM
kgio (2.10.0)
knapsack (1.16.0)
rake
timecop (>= 0.1.0)
kubeclient (3.1.0)
http (~> 2.2.2)
recursive-open-struct (~> 1.0, >= 1.0.4)
......@@ -493,6 +495,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.7.0)
......@@ -505,10 +508,12 @@ GEM
mustermann (~> 1.0.0)
mysql2 (0.4.10)
net-ldap (0.16.0)
net-ssh (4.2.0)
net-ssh (5.0.1)
netrc (0.11.0)
nokogiri (1.8.2)
nokogiri (1.8.3)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.0)
......@@ -517,7 +522,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
octokit (4.8.0)
octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
......@@ -691,10 +696,6 @@ GEM
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbnacl (4.0.2)
ffi
rbnacl-libsodium (1.0.11)
rbnacl (>= 3.0.1)
rdoc (6.0.4)
re2 (1.1.1)
recaptcha (3.0.0)
......@@ -740,36 +741,36 @@ GEM
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-mocks (3.6.0)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-support (~> 3.7.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (3.6.0)
rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-support (~> 3.6.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
rspec-retry (0.4.5)
rspec-core
rspec-set (0.1.3)
rspec-support (3.6.0)
rspec-support (3.7.1)
rspec_profiling (0.0.5)
activerecord
pg
......@@ -801,10 +802,12 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.1)
rugged (0.27.2)
safe_yaml (1.0.4)
sanitize (2.1.0)
sanitize (4.6.5)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
......@@ -822,15 +825,14 @@ GEM
scss_lint (0.56.0)
rake (>= 0.9, < 13)
sass (~> 3.5.3)
securecompare (1.0.0)
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
selenium-webdriver (3.5.0)
selenium-webdriver (3.12.0)
childprocess (~> 0.5)
rubyzip (~> 1.0)
rubyzip (~> 1.2)
sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
......@@ -866,7 +868,7 @@ GEM
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprockets (3.7.1)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
......@@ -934,7 +936,6 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.6)
procto (~> 0.0.2)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
......@@ -972,7 +973,6 @@ DEPENDENCIES
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
......@@ -1009,10 +1009,11 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
doorkeeper-openid_connect (~> 1.3)
doorkeeper-openid_connect (~> 1.5)
dropzonejs-rails (~> 0.7.1)
ed25519 (~> 1.2)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
email_spec (~> 2.2.0)
factory_bot_rails (~> 4.8.2)
faraday (~> 0.12)
fast_blank
......@@ -1036,12 +1037,12 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.100.0)
gitaly-proto (~> 0.103.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-markup (~> 1.6.4)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
......@@ -1052,6 +1053,8 @@ DEPENDENCIES
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
......@@ -1077,14 +1080,15 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
mini_magick
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
net-ldap
net-ssh (~> 4.2.0)
net-ssh (~> 5.0)
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
octokit (~> 4.8)
octokit (~> 4.9)
omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.3)
......@@ -1123,8 +1127,6 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbnacl (~> 4.0)
rbnacl-libsodium
rdoc (~> 6.0)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
......@@ -1137,7 +1139,7 @@ DEPENDENCIES
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
rspec-rails (~> 3.7.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
......@@ -1149,12 +1151,12 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 2.0)
sanitize (~> 4.6.5)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5)
selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
......
This diff is collapsed.
......@@ -15,6 +15,8 @@
- [Between the 1st and the 7th](#between-the-1st-and-the-7th)
- [On the 7th](#on-the-7th)
- [After the 7th](#after-the-7th)
- [Regressions](#regressions)
- [How to manage a regression](#how-to-manage-a-regression)
- [Release retrospective and kickoff](#release-retrospective-and-kickoff)
- [Retrospective](#retrospective)
- [Kickoff](#kickoff)
......@@ -168,6 +170,8 @@ the stable branch are:
* Fixes for [regressions](#regressions)
* Fixes for security issues
* Fixes or improvements to automated QA scenarios
* Documentation updates for changes in the same release
* New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the
......@@ -184,11 +188,7 @@ next patch release.
If a merge request is to be picked into more than one release it will need one
`Pick into X.Y` label per release where the merge request should be back-ported
to.
For example, if the current patch release is `10.1.1` and a regression fix needs
to be backported down to the `9.5` release, you will need to assign it the
`10.1` milestone and the following labels:
to. For example:
- `Pick into 10.1`
- `Pick into 10.0`
......@@ -199,26 +199,9 @@ to be backported down to the `9.5` release, you will need to assign it the
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.
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.
**Do not** set the relevant `Pick into X.Y` label (see above) before request an
exception; this should be done after the exception is approved.
You can find who is who on the [team page](https://about.gitlab.com/team/).
Whether an exception is made is determined by weighing the benefit and urgency of the change
(how important it is to the company that this is released _right now_ instead of in a month)
against the potential negative impact
(things breaking without enough time to comfortably find and fix them before the release on the 22nd).
When in doubt, we err on the side of _not_ cherry-picking.
For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
All MRs which have had exceptions granted must be merged by the 15th.
Check [this guide](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md) about how to open an exception request before opening one.
### Regressions
## Regressions
A regression for a particular monthly release is a bug that exists in that
release, but wasn't present in the release before. This includes bugs in
......@@ -236,10 +219,30 @@ month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc)
and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc),
just like any other issue, to help GitLab team members focus on issues that are
relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on).
### How to manage a regression
Regressions are very important, and they should be considered high priority
issues that should be solved as soon as possible, especially if they affect
users. Despite that, ~regression label itself does not imply when the issue
will be scheduled.
When a regression is found:
1. Create an issue describing the problem in the most detailed way possible
1. If possible, provide links to real examples and how to reproduce the problem
1. Label the issue properly, using the [team label](../CONTRIBUTING.md#team-labels),
the [subject label](../CONTRIBUTING.md#subject-labels)
and any other label that may apply in the specific case
1. Add the ~bug and ~regression labels
1. Notify the respective Engineering Manager to evaluate the Severity of the regression and add a [Severity label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-severity-labels). The counterpart Product Manager is included to weigh-in on prioritization as needed to set the [Priority label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-priority-labels).
1. If the regression is either an ~S1, ~S2 or ~S3 severity, label the regression with the current milestone as it should be fixed in the current milestone.
1. If the regression was introduced in an RC of the current release, label with ~Deliverable
1. If the regression was introduced in the previous release, label with ~"Next Patch Release"
1. If the regression is an ~S4 severity, the regression may be scheduled for later milestones at the discretion of Engineering Manager and Product Manager.
When a new issue is found, the fix should start as soon as possible. You can
ping the Engineering Manager or the Product Manager for the relative area to
make them aware of the issue earlier. They will analyze the priority and change
it if needed.
## Release retrospective and kickoff
......
......@@ -35,7 +35,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/).
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
## Website
......@@ -126,5 +126,5 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/likes) seem to like it.
11.0.0-pre
11.1.0-pre
/* eslint-disable no-param-reassign, class-methods-use-this */
/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import Cookies from 'js-cookie';
......
......@@ -26,7 +26,7 @@ export default class AjaxLoadingSpinner {
}
static toggleLoadingIcon(iconElement) {
const classList = iconElement.classList;
const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('fa-spinner');
classList.toggle('fa-spin');
......
......@@ -150,14 +150,15 @@ const Api = {
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
groupProjects(groupId, query, options, callback) {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
const defaults = {
search: query,
per_page: 20,
};
return axios
.get(url, {
params: {
search: query,
per_page: 20,
},
params: Object.assign({}, defaults, options),
})
.then(({ data }) => callback(data));
},
......@@ -243,6 +244,15 @@ const Api = {
});
},
createBranch(id, { ref, branch }) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
return axios.post(url, {
ref,
branch,
});
},
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
......
/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
/* eslint-disable no-param-reassign, prefer-template, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
......@@ -31,7 +31,9 @@ export default class Autosave {
// 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);
if (field) {
field.dispatchEvent(event);
}
}
save() {
......
This diff is collapsed.
......@@ -72,11 +72,11 @@ export default {
rel="noopener noreferrer"
>
<img
class="project-badge"
:src="imageUrlWithRetries"
class="project-badge"
aria-hidden="true"
@load="onLoad"
@error="onError"
aria-hidden="true"
/>
</a>
......@@ -91,9 +91,9 @@ export default {
>
<div class="btn btn-default btn-sm disabled">
<icon
:size="16"
class="prepend-left-8 append-right-8"
name="doc_image"
:size="16"
aria-hidden="true"
/>
</div>
......@@ -105,16 +105,16 @@ export default {
</div>
<button
v-tooltip
v-show="hasError"
:title="s__('Badges|Reload badge image')"
class="btn btn-transparent btn-sm text-primary"
type="button"
v-tooltip
:title="s__('Badges|Reload badge image')"
@click="reloadImage"
>
<icon
name="retry"
:size="16"
name="retry"
/>
</button>
</div>
......
......@@ -153,10 +153,10 @@ export default {
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<input
id="badge-link-url"
type="text"
class="form-control"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
type="text"
class="form-control"
@input="debouncedPreview"
/>
<span
......@@ -169,10 +169,10 @@ export default {
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<input
id="badge-image-url"
type="text"
class="form-control"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
type="text"
class="form-control"
@input="debouncedPreview"
/>
<span
......@@ -184,8 +184,8 @@ export default {
<div class="form-group">
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
<badge
id="badge-preview"
v-show="renderedBadge && !isRendering"
id="badge-preview"
:image-url="renderedImageUrl"
:link-url="renderedLinkUrl"
/>
......@@ -202,16 +202,16 @@ export default {
<div class="row-content-block">
<loading-button
type="submit"
container-class="btn btn-success"
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
type="submit"
container-class="btn btn-success"
/>
<button
v-if="isEditing"
class="btn btn-cancel"
type="button"
v-if="isEditing"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
......
......@@ -41,9 +41,9 @@ export default {
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<badge
class="table-section section-30"
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
class="table-section section-30"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
......@@ -54,29 +54,29 @@ export default {
v-if="canEditBadge"
class="table-action-buttons">
<button
:disabled="badge.isDeleting"
class="btn btn-default append-right-8"
type="button"
:disabled="badge.isDeleting"
@click="editBadge(badge)"
>
<icon
name="pencil"
:size="16"
:aria-label="__('Edit')"
name="pencil"
/>
</button>
<button
:disabled="badge.isDeleting"
class="btn btn-danger"
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
:disabled="badge.isDeleting"
@click="updateBadgeInModal(badge)"
>
<icon
name="remove"
:size="16"
:aria-label="__('Delete')"
name="remove"
/>
</button>
<loading-icon
......
......@@ -44,8 +44,8 @@ export default {
<gl-modal
id="delete-badge-modal"
:header-title-text="s__('Badges|Delete badge?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Badges|Delete badge')"
footer-primary-button-variant="danger"
@submit="onSubmitModal">
<div class="well">
<badge
......
......@@ -52,7 +52,7 @@ export default function initCopyToClipboard() {
* data types to the intended values.
*/
$(document).on('copy', 'body > textarea[readonly]', (e) => {
const clipboardData = e.originalEvent.clipboardData;
const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = e.target.value;
......
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, max-len, no-restricted-syntax, guard-for-in, no-continue */
import $ from 'jquery';
import _ from 'underscore';
......@@ -119,7 +119,7 @@ const gfmRules = {
return el.outerHTML;
},
'dl'(el, text) {
let lines = text.trim().split('\n');
let lines = text.replace(/\n\n/g, '\n').trim().split('\n');
// Add two spaces to the front of subsequent list items lines,
// or leave the line entirely blank.
lines = lines.map((l) => {
......@@ -129,9 +129,13 @@ const gfmRules = {
return ` ${line}`;
});
return `<dl>\n${lines.join('\n')}\n</dl>`;
return `<dl>\n${lines.join('\n')}\n</dl>\n`;
},
'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
'dt, dd, summary, details'(el, text) {
const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>\n`;
},
'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>`;
},
......@@ -215,22 +219,22 @@ const gfmRules = {
return text.replace(/^- /mg, '1. ');
},
'h1'(el, text) {
return `# ${text.trim()}`;
return `# ${text.trim()}\n`;
},
'h2'(el, text) {
return `## ${text.trim()}`;
return `## ${text.trim()}\n`;
},
'h3'(el, text) {
return `### ${text.trim()}`;
return `### ${text.trim()}\n`;
},
'h4'(el, text) {
return `#### ${text.trim()}`;
return `#### ${text.trim()}\n`;
},
'h5'(el, text) {
return `##### ${text.trim()}`;
return `##### ${text.trim()}\n`;
},
'h6'(el, text) {
return `###### ${text.trim()}`;
return `###### ${text.trim()}\n`;
},
'strong'(el, text) {
return `**${text}**`;
......@@ -241,11 +245,13 @@ const gfmRules = {
'del'(el, text) {
return `~~${text}~~`;
},
'sup'(el, text) {
return `^${text}`;
},
'hr'(el) {
return '-----';
// extra leading \n is to ensure that there is a blank line between
// a list followed by an hr, otherwise this breaks old redcarpet rendering
return '\n-----\n';
},
'p'(el, text) {
return `${text.trim()}\n`;
},
'table'(el) {
const theadEl = el.querySelector('thead');
......@@ -263,7 +269,9 @@ const gfmRules = {
let before = '';
let after = '';
switch (cell.style.textAlign) {
const alignment = cell.align || cell.style.textAlign;
switch (alignment) {
case 'center':
before = ':';
after = ':';
......@@ -313,7 +321,7 @@ export class CopyAsGFM {
}
static copyAsGFM(e, transformer) {
const clipboardData = e.originalEvent.clipboardData;
const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const documentFragment = getSelectedFragment();
......@@ -330,7 +338,7 @@ export class CopyAsGFM {
}
static pasteGFM(e) {
const clipboardData = e.originalEvent.clipboardData;
const { clipboardData } = e.originalEvent;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
......
......@@ -84,7 +84,7 @@ class BalsamiqViewer {
renderTemplate(preview) {
const resource = this.getResource(preview.resourceID);
const name = BalsamiqViewer.parseTitle(resource);
const image = preview.image;
const { image } = preview;
const template = PREVIEW_TEMPLATE({
name,
......
......@@ -12,7 +12,7 @@ export default function loadBalsamiqFile() {
if (!(viewer instanceof Element)) return;
const endpoint = viewer.dataset.endpoint;
const { endpoint } = viewer.dataset;
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
......
/* eslint-disable no-new */
import Vue from 'vue';
import pdfLab from '../../pdf/index.vue';
......
......@@ -5,7 +5,7 @@ export default () => {
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
const target = e.target;
const { target } = e;
e.preventDefault();
......
......@@ -70,7 +70,7 @@ export default class BlobViewer {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
if (this.switcher && location.hash.indexOf('#L') === 0) {
if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
/* eslint-disable no-new */
import $ from 'jquery';
import NewCommitForm from '../new_commit_form';
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* eslint-disable comma-dangle */
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
......@@ -14,17 +13,28 @@ window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: {
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState,
},
props: {
list: Object,
disabled: Boolean,
issueLinkBase: String,
rootPath: String,
list: {
type: Object,
default: () => ({}),
},
disabled: {
type: Boolean,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
boardId: {
type: String,
required: true,
......@@ -46,56 +56,8 @@ gl.issueBoards.Board = Vue.extend({
});
},
deep: true,
},
detailIssue: {
handler () {
if (!Object.keys(this.detailIssue.issue).length) return;
const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) {
const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0];
const left = boardsList.scrollLeft - offsetLeft;
let right = (offsetLeft + this.$el.offsetWidth);
if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
// -290 here because width of boardsList is animating so therefore
// getting the width here is incorrect
// 290 is the width of the sidebar
right -= (boardsList.offsetWidth - 290);
} else {
right -= boardsList.offsetWidth;
}
if (right - boardsList.scrollLeft > 0) {
$(boardsList).animate({
scrollLeft: right
}, this.sortableOptions.animation);
} else if (left > 0) {
$(boardsList).animate({
scrollLeft: offsetLeft
}, this.sortableOptions.animation);
}
}
},
deep: true
}
},
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
}
},
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
......@@ -125,4 +87,19 @@ gl.issueBoards.Board = Vue.extend({
this.list.isExpanded = !isCollapsed;
}
},
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
}
},
},
template: '#js-board-template',
});
......@@ -72,8 +72,8 @@ export default {
:key="index"
>
<span
class="label-color"
:style="{ backgroundColor: label.color }">
:style="{ backgroundColor: label.color }"
class="label-color">
</span>
{{ label.title }}
</li>
......
<script>
/* eslint-disable vue/require-default-prop */
import './issue_card_inner';
import eventHub from '../eventhub';
/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardsIssueCard',
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
export default {
name: 'BoardsIssueCard',
components: {
IssueCardInner,
},
issue: {
type: Object,
default: () => ({}),
props: {
list: {
type: Object,
default: () => ({}),
},
issue: {
type: Object,
default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
issueLinkBase: {
type: String,
default: '',
data() {
return {
showDetail: false,
detailIssue: Store.detail,
};
},
disabled: {
type: Boolean,
default: false,
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
data() {
return {
showDetail: false,
detailIssue: Store.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue');
} else {
eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue');
} else {
eventHub.$emit('newDetailIssue', this.issue);
Store.detail.list = this.list;
}
}
}
},
},
},
};
};
</script>
<template>
<li
class="board-card"
:class="{
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
......@@ -85,6 +84,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
class="board-card"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
......
/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
/* eslint-disable comma-dangle, no-alert */
import $ from 'jquery';
import Vue from 'vue';
......@@ -8,13 +8,16 @@ window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardDelete = Vue.extend({
props: {
list: Object
list: {
type: Object,
default: () => ({}),
},
},
methods: {
deleteBoard () {
$(this.$el).tooltip('hide');
if (confirm('Are you sure you want to delete this list?')) {
if (window.confirm('Are you sure you want to delete this list?')) {
this.list.destroy();
}
}
......
......@@ -87,10 +87,46 @@ export default {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: true,
group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
group: {
name: 'issues',
/**
* Dynamically determine between which containers
* items can be moved or copied as
* Assignee lists (EE feature) require this behavior
*/
pull: (to, from, dragEl, e) => {
// As per Sortable's docs, `to` should provide
// reference to exact sortable container on which
// we're trying to drag element, but either it is
// a library's bug or our markup structure is too complex
// that `to` never points to correct container
// See https://github.com/RubaXa/Sortable/issues/1037
//
// So we use `e.target` which is always accurate about
// which element we're currently dragging our card upon
// So from there, we can get reference to actual container
// and thus the container type to enable Copy or Move
if (e.target) {
const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType;
if (toBoardType) {
const fromBoardType = this.list.type;
if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
(fromBoardType === 'label' && toBoardType === 'assignee')) {
return 'clone';
}
}
}
return true;
},
revertClone: true,
},
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
......@@ -169,21 +205,22 @@ export default {
<template>
<div class="board-list-component">
<div
v-if="loading"
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
aria-label="Loading issues">
<loading-icon />
</div>
<board-new-issue
v-if="list.type !== 'closed' && showIssueForm"
:group-id="groupId"
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
:list="list"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
:data-board-type="list.type"
:class="{ 'is-smaller': showIssueForm }"
class="board-list js-board-list">
<board-card
v-for="(issue, index) in issues"
ref="issue"
......@@ -196,8 +233,8 @@ export default {
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
class="board-list-count text-center"
data-issue-id="-1">
<loading-icon
v-show="list.loadingMore"
......
......@@ -49,11 +49,12 @@ export default {
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
assignees: [],
assignees,
project_id: this.selectedProject.id,
});
......@@ -95,26 +96,26 @@ export default {
<div class="board-card">
<form @submit="submit($event)">
<div
class="flash-container"
v-if="error"
class="flash-container"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label
class="label-light"
:for="list.id + '-title'"
class="label-light"
>
Title
</label>
<input
ref="input"
v-model="title"
:id="list.id + '-title'"
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'"
/>
<project-select
v-if="groupId"
......@@ -122,10 +123,10 @@ export default {
/>
<div class="clearfix prepend-top-10">
<button
ref="submit-button"
:disabled="disabled"
class="btn btn-success float-left"
type="submit"
:disabled="disabled"
ref="submit-button"
>
Submit issue
</button>
......@@ -141,4 +142,3 @@ export default {
</div>
</div>
</template>
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* eslint-disable comma-dangle, no-new */
import $ from 'jquery';
import Vue from 'vue';
......@@ -6,13 +6,13 @@ 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.vue';
import assignees from '../../sidebar/components/assignees/assignees.vue';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue';
import Assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import RemoveBtn from './sidebar/remove_issue.vue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import Subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;
......@@ -21,8 +21,17 @@ window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({
components: {
AssigneeTitle,
Assignees,
RemoveBtn,
Subscriptions,
},
props: {
currentUser: Object
currentUser: {
type: Object,
default: () => ({}),
},
},
data() {
return {
......@@ -64,6 +73,26 @@ gl.issueBoards.BoardSidebar = Vue.extend({
deep: true
},
},
created () {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
},
beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
},
methods: {
closeSidebar () {
this.detail.issue = {};
......@@ -97,30 +126,4 @@ gl.issueBoards.BoardSidebar = Vue.extend({
});
},
},
created () {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
},
beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
},
components: {
assigneeTitle,
assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
subscriptions,
},
});
import $ from 'jquery';
import Vue from 'vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.IssueCardInner = Vue.extend({
props: {
issue: {
type: Object,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
},
components: {
UserAvatarLink,
},
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return `+${this.numberOverLimit}`;
},
shouldRenderCounter() {
if (this.issue.assignees.length <= this.maxRender) {
return false;
}
return this.issue.assignees.length > this.numberOverLimit;
},
issueId() {
if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
filterPath.push(param);
} else {
filterPath.splice(labelIndex, 1);
}
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
},
template: `
<div>
<div class="board-card-header">
<h4 class="board-card-title">
<i
class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"
aria-hidden="true"
/>
<a
class="js-no-trigger"
:href="issue.path"
:title="issue.title">{{ issue.title }}</a>
<span
class="board-card-number"
v-if="issueId"
>
{{ issue.referencePath }}
</span>
</h4>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
v-if="shouldRenderAssignee(index)"
class="js-no-trigger"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
:tooltip-text="assigneeUrlTitle(assignee)"
tooltip-placement="bottom"
/>
<span
class="avatar-counter has-tooltip"
:title="assigneeCounterTooltip"
v-if="shouldRenderCounter"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
<div
class="board-card-footer"
v-if="showLabelFooter"
>
<button
class="badge color-label has-tooltip"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
@click="filterByLabel(label, $event)"
:style="labelStyle(label)"
:title="label.description"
data-container="body">
{{ label.title }}
</button>
</div>
</div>
`,
});
<script>
import $ from 'jquery';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
import tooltip from '../../vue_shared/directives/tooltip';
const Store = gl.issueBoards.BoardsStore;
export default {
components: {
UserAvatarLink,
},
directives: {
tooltip,
},
props: {
issue: {
type: Object,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
},
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return `+${this.numberOverLimit}`;
},
shouldRenderCounter() {
if (this.issue.assignees.length <= this.maxRender) {
return false;
}
return this.issue.assignees.length > this.numberOverLimit;
},
issueId() {
if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
if (labelIndex === -1) {
filterPath.push(param);
} else {
filterPath.splice(labelIndex, 1);
}
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl();
eventHub.$emit('updateTokens');
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
},
};
</script>
<template>
<div>
<div class="board-card-header">
<h4 class="board-card-title">
<i
v-if="issue.confidential"
class="fa fa-eye-slash confidential-icon"
aria-hidden="true"
></i>
<a
:href="issue.path"
:title="issue.title"
class="js-no-trigger">{{ issue.title }}</a>
<span
v-if="issueId"
class="board-card-number"
>
{{ issue.referencePath }}
</span>
</h4>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
:key="assignee.id"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
:tooltip-text="assigneeUrlTitle(assignee)"
class="js-no-trigger"
tooltip-placement="bottom"
/>
<span
v-tooltip
v-if="shouldRenderCounter"
:title="assigneeCounterTooltip"
class="avatar-counter"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
<div
v-if="showLabelFooter"
class="board-card-footer"
>
<button
v-tooltip
v-for="label in issue.labels"
v-if="showLabel(label)"
:key="label.id"
:style="labelStyle(label)"
:title="label.description"
class="badge color-label"
type="button"
data-container="body"
@click="filterByLabel(label, $event)"
>
{{ label.title }}
</button>
</div>
</div>
</template>
import Vue from 'vue';
<script>
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalEmptyState = Vue.extend({
export default {
mixins: [modalMixin],
data() {
return ModalStore.store;
},
props: {
newIssuePath: {
type: String,
......@@ -17,6 +14,9 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
contents() {
const obj = {
......@@ -38,32 +38,36 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
return obj;
},
},
template: `
<section class="empty-state">
<div class="row">
<div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a
:href="newIssuePath"
class="btn btn-success btn-inverted"
v-if="activeTab === 'all'">
New issue
</a>
<button
type="button"
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
Open issues
</button>
</div>
};
</script>
<template>
<section class="empty-state">
<div class="row">
<div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a
v-if="activeTab === 'all'"
:href="newIssuePath"
class="btn btn-success btn-inverted"
>
New issue
</a>
<button
v-if="activeTab === 'selected'"
class="btn btn-default"
type="button"
@click="changeTab('all')"
>
Open issues
</button>
</div>
</div>
</section>
`,
});
</div>
</section>
</template>
import Vue from 'vue';
<script>
import Flash from '../../../flash';
import { __ } from '../../../locale';
import './lists_dropdown';
import ListsDropdown from './lists_dropdown.vue';
import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalFooter = Vue.extend({
export default {
components: {
ListsDropdown,
},
mixins: [modalMixin],
data() {
return {
......@@ -52,31 +55,32 @@ gl.issueBoards.ModalFooter = Vue.extend({
this.toggleModal(false);
},
},
components: {
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
},
template: `
<footer
class="form-actions add-issues-footer">
<div class="float-left">
<button
class="btn btn-success"
type="button"
:disabled="submitDisabled"
@click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown></lists-dropdown>
</div>
};
</script>
<template>
<footer
class="form-actions add-issues-footer"
>
<div class="float-left">
<button
class="btn btn-default float-right"
:disabled="submitDisabled"
class="btn btn-success"
type="button"
@click="toggleModal(false)">
Cancel
@click="addIssues"
>
{{ submitText }}
</button>
</footer>
`,
});
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown/>
</div>
<button
class="btn btn-default float-right"
type="button"
@click="toggleModal(false)"
>
Cancel
</button>
</footer>
</template>
import Vue from 'vue';
import modalFilters from './filters';
import './tabs';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({
mixins: [modalMixin],
props: {
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll();
},
},
components: {
'modal-tabs': gl.issueBoards.ModalTabs,
modalFilters,
},
template: `
<div>
<header class="add-issues-header form-actions">
<h2>
Add issues
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleModal(false)">
<span aria-hidden="true">×</span>
</button>
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
<div
class="add-issues-search append-bottom-10"
v-if="showSearch">
<modal-filters :store="filter" />
<button
type="button"
class="btn btn-success btn-inverted prepend-left-10"
ref="selectAllBtn"
@click="toggleAll">
{{ selectAllText }}
</button>
</div>
</div>
`,
});
<script>
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
components: {
ModalTabs,
ModalFilters,
},
mixins: [modalMixin],
props: {
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll();
},
},
};
</script>
<template>
<div>
<header class="add-issues-header form-actions">
<h2>
Add issues
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleModal(false)"
>
<span aria-hidden="true">×</span>
</button>
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0"/>
<div
v-if="showSearch"
class="add-issues-search append-bottom-10">
<modal-filters :store="filter" />
<button
ref="selectAllBtn"
type="button"
class="btn btn-success btn-inverted prepend-left-10"
@click="toggleAll"
>
{{ selectAllText }}
</button>
</div>
</div>
</template>
/* global ListIssue */
import Vue from 'vue';
import queryData from '~/boards/utils/query_data';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
import './empty_state';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.IssuesModal = Vue.extend({
props: {
newIssuePath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
watch: {
page() {
this.loadIssues();
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues()
.then(loadingDone)
.catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
}
},
deep: true,
},
},
methods: {
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService.getBacklog(queryData(this.filter.path, {
page: this.page,
per: this.perPage,
}))
.then(res => res.data)
.then((data) => {
if (clearIssues) {
this.issues = [];
}
data.issues.forEach((issueObj) => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue;
this.issues.push(issue);
});
this.loadingNewPage = false;
if (!this.issuesCount) {
this.issuesCount = data.size;
}
}).catch(() => {
// TODO: handle request error
});
},
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
created() {
this.page = 1;
},
components: {
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter,
'empty-state': gl.issueBoards.ModalEmptyState,
loadingIcon,
},
template: `
<div
class="add-issues-modal"
v-if="showAddIssuesModal">
<div class="add-issues-container">
<modal-header
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath">
</modal-header>
<modal-list
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:empty-state-svg="emptyStateSvg"
v-if="!loading && showList && !filterLoading"></modal-list>
<empty-state
v-if="showEmptyState"
:new-issue-path="newIssuePath"
:empty-state-svg="emptyStateSvg"></empty-state>
<section
class="add-issues-list text-center"
v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<loading-icon />
</div>
</section>
<modal-footer></modal-footer>
</div>
</div>
`,
});
<script>
/* global ListIssue */
import queryData from '~/boards/utils/query_data';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
export default {
components: {
EmptyState,
ModalHeader,
ModalList,
ModalFooter,
loadingIcon,
},
props: {
newIssuePath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
watch: {
page() {
this.loadIssues();
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues()
.then(loadingDone)
.catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
}
},
deep: true,
},
},
created() {
this.page = 1;
},
methods: {
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService
.getBacklog(
queryData(this.filter.path, {
page: this.page,
per: this.perPage,
}),
)
.then(res => res.data)
.then(data => {
if (clearIssues) {
this.issues = [];
}
data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue;
this.issues.push(issue);
});
this.loadingNewPage = false;
if (!this.issuesCount) {
this.issuesCount = data.size;
}
})
.catch(() => {
// TODO: handle request error
});
},
},
};
</script>
<template>
<div
v-if="showAddIssuesModal"
class="add-issues-modal">
<div class="add-issues-container">
<modal-header
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath"
/>
<modal-list
v-if="!loading && showList && !filterLoading"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:empty-state-svg="emptyStateSvg"
/>
<empty-state
v-if="showEmptyState"
:new-issue-path="newIssuePath"
:empty-state-svg="emptyStateSvg"
/>
<section
v-if="loading || filterLoading"
class="add-issues-list text-center"
>
<div class="add-issues-list-loading">
<loading-icon />
</div>
</section>
<modal-footer/>
</div>
</div>
</template>
import Vue from 'vue';
import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalList = Vue.extend({
props: {
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
watch: {
activeTab() {
if (this.activeTab === 'all') {
ModalStore.purgeUnselectedIssues();
}
},
},
computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues;
},
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
},
methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if (
this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true;
this.page += 1;
}
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
},
mounted() {
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
template: `
<section
class="add-issues-list add-issues-list-columns"
ref="list">
<div
class="empty-state add-issues-empty-state-filter text-center"
v-if="issuesCount > 0 && issues.length === 0">
<div
class="svg-content">
<img :src="emptyStateSvg"/>
</div>
<div class="text-content">
<h4>
There are no issues to show.
</h4>
</div>
</div>
<div
v-for="group in groupedIssues"
class="add-issues-list-column">
<div
v-for="issue in group"
v-if="showIssue(issue)"
class="board-card-parent">
<div
class="board-card"
:class="{ 'is-active': issue.selected }"
@click="toggleIssue($event, issue)">
<issue-card-inner
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath">
</issue-card-inner>
<span
:aria-label="'Issue #' + issue.id + ' selected'"
aria-checked="true"
v-if="issue.selected"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
</div>
</div>
</div>
</section>
`,
});
<script>
import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
export default {
components: {
IssueCardInner,
},
props: {
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues;
},
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
},
watch: {
activeTab() {
if (this.activeTab === 'all') {
ModalStore.purgeUnselectedIssues();
}
},
},
mounted() {
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if (
this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true;
this.page += 1;
}
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
},
};
</script>
<template>
<section
ref="list"
class="add-issues-list add-issues-list-columns">
<div
v-if="issuesCount > 0 && issues.length === 0"
class="empty-state add-issues-empty-state-filter text-center">
<div
class="svg-content">
<img :src="emptyStateSvg" />
</div>
<div class="text-content">
<h4>
There are no issues to show.
</h4>
</div>
</div>
<div
v-for="(group, index) in groupedIssues"
:key="index"
class="add-issues-list-column">
<div
v-for="issue in group"
v-if="showIssue(issue)"
:key="issue.id"
class="board-card-parent">
<div
:class="{ 'is-active': issue.selected }"
class="board-card"
@click="toggleIssue($event, issue)">
<issue-card-inner
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"/>
<span
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
aria-checked="true"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
</div>
</div>
</div>
</section>
</template>
import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() {
return {
modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
this.modal.selectedList = null;
},
template: `
<div class="dropdown inline">
<button
class="dropdown-menu-toggle"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<span
class="dropdown-label-box"
:style="{ backgroundColor: selected.label.color }">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="list in state.lists"
v-if="list.type == 'label'">
<a
href="#"
role="button"
:class="{ 'is-active': list.id == selected.id }"
@click.prevent="modal.selectedList = list">
<span
class="dropdown-label-box"
:style="{ backgroundColor: list.label.color }">
</span>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div>
`,
});
import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalTabs = Vue.extend({
mixins: [modalMixin],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
template: `
<div class="top-area prepend-top-10 append-bottom-10">
<ul class="nav-links issues-state-filters">
<li :class="{ 'active': activeTab == 'all' }">
<a
href="#"
role="button"
@click.prevent="changeTab('all')">
Open issues
<span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
</li>
<li :class="{ 'active': activeTab == 'selected' }">
<a
href="#"
role="button"
@click.prevent="changeTab('selected')">
Selected issues
<span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
</li>
</ul>
</div>
`,
});
This diff is collapsed.
/* eslint-disable class-methods-use-this */
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment