Commit b55ab7ab authored by Phil Hughes's avatar Phil Hughes

Merge remote-tracking branch 'origin/26914-deploy_history_data_source' into...

Merge remote-tracking branch 'origin/26914-deploy_history_data_source' into metrics-deployment-history
parents 8d3b2581 e8f2daae

Too many changes to show.

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

...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
"plugins": [ "plugins": [
["istanbul", { ["istanbul", {
"exclude": [ "exclude": [
"app/assets/javascripts/droplab/**/*",
"spec/javascripts/**/*" "spec/javascripts/**/*"
] ]
}], }],
......
...@@ -13,9 +13,11 @@ ...@@ -13,9 +13,11 @@
}, },
"plugins": [ "plugins": [
"filenames", "filenames",
"import" "import",
"html"
], ],
"settings": { "settings": {
"html/html-extensions": [".html", ".html.raw", ".vue"],
"import/resolver": { "import/resolver": {
"webpack": { "webpack": {
"config": "./config/webpack.config.js" "config": "./config/webpack.config.js"
......
...@@ -30,6 +30,7 @@ eslint-report.html ...@@ -30,6 +30,7 @@ eslint-report.html
/config/unicorn.rb /config/unicorn.rb
/config/secrets.yml /config/secrets.yml
/config/sidekiq.yml /config/sidekiq.yml
/config/registry.key
/coverage/* /coverage/*
/coverage-javascript/ /coverage-javascript/
/db/*.sqlite3 /db/*.sqlite3
......
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-git-2.7-phantomjs-2.1-node-7.1" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1"
cache: cache:
key: "ruby-233" key: "ruby-233"
...@@ -8,14 +8,15 @@ cache: ...@@ -8,14 +8,15 @@ cache:
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
RAILS_ENV: "test" RAILS_ENV: "test"
NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
SETUP_DB: "true" SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
...@@ -66,6 +67,7 @@ stages: ...@@ -66,6 +67,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
...@@ -86,6 +88,7 @@ stages: ...@@ -86,6 +88,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
...@@ -129,9 +132,7 @@ setup-test-env: ...@@ -129,9 +132,7 @@ setup-test-env:
stage: prepare stage: prepare
script: script:
- node --version - node --version
- yarn --version
- yarn install --pure-lockfile - yarn install --pure-lockfile
- yarn check # ensure that yarn.lock matches package.json
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts: artifacts:
...@@ -277,7 +278,6 @@ rake karma: ...@@ -277,7 +278,6 @@ rake karma:
cache: cache:
paths: paths:
- vendor/ruby - vendor/ruby
- node_modules
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner <<: *dedicated-runner
...@@ -292,14 +292,31 @@ rake karma: ...@@ -292,14 +292,31 @@ rake karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
lint-doc: docs:check:apilint:
image: "phusion/baseimage"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
image: "phusion/baseimage:latest" cache: {}
dependencies: []
before_script: [] before_script: []
script: script:
- scripts/lint-doc.sh - scripts/lint-doc.sh
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
bundler:check: bundler:check:
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -331,10 +348,8 @@ migration paths: ...@@ -331,10 +348,8 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git fetch origin v8.5.9 - git fetch origin v8.14.10
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
- cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA - git checkout $CI_COMMIT_SHA
...@@ -361,9 +376,6 @@ coverage: ...@@ -361,9 +376,6 @@ coverage:
lint:javascript: lint:javascript:
<<: *dedicated-runner <<: *dedicated-runner
cache:
paths:
- node_modules/
stage: test stage: test
before_script: [] before_script: []
script: script:
...@@ -371,9 +383,6 @@ lint:javascript: ...@@ -371,9 +383,6 @@ lint:javascript:
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
cache:
paths:
- node_modules/
stage: post-test stage: post-test
before_script: [] before_script: []
script: script:
......
...@@ -25,14 +25,20 @@ logs, and code as it's very hard to read otherwise.) ...@@ -25,14 +25,20 @@ logs, and code as it's very hard to read otherwise.)
#### Results of GitLab environment info #### Results of GitLab environment info
<details>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`) `sudo gitlab-rake gitlab:env:info`)
(For installations from source run and paste the output of: (For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`) `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
</details>
#### Results of GitLab application Check #### Results of GitLab application Check
<details>
(For installations with omnibus-gitlab package run and paste the output of: (For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`) `sudo gitlab-rake gitlab:check SANITIZE=true`)
...@@ -41,6 +47,8 @@ logs, and code as it's very hard to read otherwise.) ...@@ -41,6 +47,8 @@ logs, and code as it's very hard to read otherwise.)
(we will only investigate if the tests are passing) (we will only investigate if the tests are passing)
</details>
### Possible fixes ### Possible fixes
(If you can, link to the line of code that might be responsible for the problem) (If you can, link to the line of code that might be responsible for the problem)
...@@ -533,6 +533,10 @@ Style/WhileUntilModifier: ...@@ -533,6 +533,10 @@ Style/WhileUntilModifier:
Style/WordArray: Style/WordArray:
Enabled: true Enabled: true
# Use `proc` instead of `Proc.new`.
Style/Proc:
Enabled: true
# Metrics ##################################################################### # Metrics #####################################################################
# A calculated magnitude based on number of assignments, # A calculated magnitude based on number of assignments,
...@@ -950,6 +954,10 @@ RSpec/DescribeClass: ...@@ -950,6 +954,10 @@ RSpec/DescribeClass:
RSpec/DescribeMethod: RSpec/DescribeMethod:
Enabled: false Enabled: false
# Avoid describing symbols.
RSpec/DescribeSymbol:
Enabled: true
# Checks that the second argument to top level describe is the tested method # Checks that the second argument to top level describe is the tested method
# name. # name.
RSpec/DescribedClass: RSpec/DescribedClass:
......
This diff is collapsed.
...@@ -2,6 +2,55 @@ ...@@ -2,6 +2,55 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.0.5 (2017-04-10)
- Add shortcuts and counters to MRs and issues in navbar.
- Disable invalid service templates.
- Handle SSH keys that have multiple spaces between each marker.
## 9.0.4 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 9.0.3 (2017-04-05)
- Fix name colision when importing GitHub pull requests from forked repositories. !9719
- Fix GitHub Importer for PRs of deleted forked repositories. !9992
- Fix environment folder route when special chars present in environment name. !10250
- Improve Markdown rendering when a lot of merge requests are referenced. !10252
- Allow users to import GitHub projects to subgroups.
- Backport API changes needed to fix sticking in EE.
- Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery. (mhasbini)
- Make CI build to use optimistic locking only on status change.
- Fix race condition where a namespace would be deleted before a project was deleted.
- Fix linking to new issue with selected template via url parameter.
- Remove unnecessary ORDER BY clause when updating todos. (mhasbini)
- API: Make the /notes endpoint work with noteable iid instead of id.
- Fixes method not replacing URL parameters correctly and breaking pipelines pagination.
- Move issue, mr, todos next to profile dropdown in top nav.
## 9.0.2 (2017-03-29)
- Correctly update paths when changing a child group.
- Fixed private group name disclosure via new/update forms.
## 9.0.1 (2017-03-28)
- Resolve "404 when requesting build trace". !9759 (dosuken123)
- Simplify search queries for projects and merge requests. !10053 (mhasbini)
- Fix after_script processing for Runners APIv4. !10185
- Fix escaped html appearing in milestone page. !10224
- Fix bug that caused jobs that already had been retried to be retried again when retrying a pipeline. !10249
- Allow filtering by all started milestones.
- Allow sorting by due date and priority.
- Fixed branches pagination not displaying.
- Fixed filtered search not working in IE.
- Optimize labels finder query when searching for a project with a group. (mhasbini)
## 9.0.0 (2017-03-22) ## 9.0.0 (2017-03-22)
- Fix inconsistent naming for services that delete things. !5803 (dixpac) - Fix inconsistent naming for services that delete things. !5803 (dixpac)
...@@ -285,6 +334,14 @@ entry. ...@@ -285,6 +334,14 @@ entry.
- Change development tanuki favicon colors to match logo color order. - Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids. - API issues - support filtering by iids.
## 8.17.5 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.17.4 (2017-03-19) ## 8.17.4 (2017-03-19)
- Only show public emails in atom feeds. - Only show public emails in atom feeds.
...@@ -498,6 +555,14 @@ entry. ...@@ -498,6 +555,14 @@ entry.
- Remove deprecated GitlabCiService. - Remove deprecated GitlabCiService.
- Requeue pending deletion projects. - Requeue pending deletion projects.
## 8.16.9 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.16.8 (2017-03-19) ## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds. - Only show public emails in atom feeds.
......
...@@ -314,9 +314,12 @@ request is as follows: ...@@ -314,9 +314,12 @@ request is as follows:
organized commits by [squashing them][git-squash] organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch 1. Submit a merge request (MR) to the `master` branch
1. Leave the approvals settings as they are: 1. Your merge request needs at least 1 approval but feel free to require more.
1. Your merge request needs at least 1 approval For instance if you're touching backend and frontend code, it's a good idea
1. You don't have to select any approvers to require 2 approvals: 1 from a backend maintainer and 1 from a frontend
maintainer
1. You don't have to select any approvers, but you can if you really want
specific people to approve your merge request
1. The MR title should describe the change you want to make 1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it. used to achieve it.
...@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted: ...@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted:
1. If your merge request includes only frontend changes [^1], it must be 1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][team]**. **approved by a [frontend maintainer][team]**.
1. If your merge request includes frontend and backend changes [^1], it must 1. If your merge request includes frontend and backend changes [^1], it must
be approved by a frontend **and** a backend maintainer. be **approved by a [frontend and a backend maintainer][team]**.
1. To lower the amount of merge requests maintainers need to review, you can 1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][team] for a first review. ask or assign any [reviewers][team] for a first review.
1. If you need some guidance (e.g. it's your first merge request), feel free 1. If you need some guidance (e.g. it's your first merge request), feel free
...@@ -556,6 +559,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -556,6 +559,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[^1]: Specs other than JavaScript specs are considered backend code. Haml [^1]: Please note that specs other than JavaScript specs are considered backend
changes are considered backend code if they include Ruby code other than just code.
pure HTML.
...@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0' ...@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0' gem 'rugged', '~> 0.25.1.1'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
...@@ -63,7 +63,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' ...@@ -63,7 +63,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap'
# Git Wiki # Git Wiki
# Required manually in config/initializers/gollum.rb to control load order # Required manually in config/initializers/gollum.rb to control load order
gem 'gollum-lib', '~> 4.2', require: false gem 'gollum-lib', '~> 4.2', require: false
gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection # Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
...@@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0' ...@@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
# Pagination # Pagination
gem 'kaminari', '~> 0.17.0' gem 'kaminari', '~> 0.17.0'
...@@ -144,6 +147,9 @@ gem 'sidekiq-cron', '~> 0.4.4' ...@@ -144,6 +147,9 @@ gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
# Cron Parser
gem 'rufus-scheduler', '~> 3.1.10'
# HTTP requests # HTTP requests
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.13.3'
...@@ -223,7 +229,7 @@ gem 'oj', '~> 2.17.4' ...@@ -223,7 +229,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2' gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6' gem 'chronic_duration', '~> 0.10.6'
gem 'webpack-rails', '~> 0.9.9' gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0' gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
...@@ -260,7 +266,6 @@ group :development do ...@@ -260,7 +266,6 @@ group :development do
gem 'brakeman', '~> 3.6.0', require: false gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.5.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler # Better errors handler
...@@ -272,6 +277,7 @@ group :development do ...@@ -272,6 +277,7 @@ group :development do
end end
group :development, :test do group :development, :test do
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4' gem 'pry-rails', '~> 0.3.4'
...@@ -301,7 +307,7 @@ group :development, :test do ...@@ -301,7 +307,7 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.47.1', require: false gem 'rubocop', '~> 0.47.1', require: false
gem 'rubocop-rspec', '~> 1.12.0', require: false gem 'rubocop-rspec', '~> 1.15.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false gem 'haml_lint', '~> 0.21.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
...@@ -352,4 +358,6 @@ gem 'vmstat', '~> 2.3.0' ...@@ -352,4 +358,6 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.3.0' gem 'gitaly', '~> 0.5.0'
gem 'toml-rb', '~> 0.3.15', require: false
...@@ -117,6 +117,7 @@ GEM ...@@ -117,6 +117,7 @@ GEM
chronic_duration (0.10.6) chronic_duration (0.10.6)
numerizer (~> 0.1.1) numerizer (~> 0.1.1)
chunky_png (1.3.5) chunky_png (1.3.5)
citrus (3.0.2)
cliver (0.3.2) cliver (0.3.2)
coderay (1.1.1) coderay (1.1.1)
coercible (1.0.0) coercible (1.0.0)
...@@ -253,7 +254,7 @@ GEM ...@@ -253,7 +254,7 @@ GEM
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.3.0) gitaly (0.5.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -288,9 +289,9 @@ GEM ...@@ -288,9 +289,9 @@ GEM
rouge (~> 2.0) rouge (~> 2.0)
sanitize (~> 2.1.0) sanitize (~> 2.1.0)
stringex (~> 2.5.1) stringex (~> 2.5.1)
gollum-rugged_adapter (0.4.2) gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15) mime-types (>= 1.15)
rugged (~> 0.24.0, >= 0.21.3) rugged (~> 0.25)
gon (6.1.0) gon (6.1.0)
actionpack (>= 3.0) actionpack (>= 3.0)
json json
...@@ -345,6 +346,8 @@ GEM ...@@ -345,6 +346,8 @@ GEM
tilt tilt
hashdiff (0.3.2) hashdiff (0.3.2)
hashie (3.5.5) hashie (3.5.5)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
health_check (2.6.0) health_check (2.6.0)
rails (>= 4.0) rails (>= 4.0)
hipchat (1.5.2) hipchat (1.5.2)
...@@ -668,7 +671,7 @@ GEM ...@@ -668,7 +671,7 @@ GEM
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.12.0) rubocop-rspec (1.15.0)
rubocop (>= 0.42.0) rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
...@@ -682,7 +685,7 @@ GEM ...@@ -682,7 +685,7 @@ GEM
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.1.10) rufus-scheduler (3.1.10)
rugged (0.24.0) rugged (0.25.1.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
...@@ -784,6 +787,8 @@ GEM ...@@ -784,6 +787,8 @@ GEM
tilt (2.0.6) tilt (2.0.6)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
tool (0.2.3) tool (0.2.3)
truncato (0.7.8) truncato (0.7.8)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
...@@ -823,8 +828,8 @@ GEM ...@@ -823,8 +828,8 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpack-rails (0.9.9) webpack-rails (0.9.10)
rails (>= 3.2.0) railties (>= 3.2.0)
websocket-driver (0.6.3) websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2) websocket-extensions (0.1.2)
...@@ -899,19 +904,20 @@ DEPENDENCIES ...@@ -899,19 +904,20 @@ DEPENDENCIES
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
gitaly (~> 0.3.0) gitaly (~> 0.5.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.8.6) google-api-client (~> 0.8.6)
grape (~> 0.19.0) grape (~> 0.19.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
haml_lint (~> 0.21.0) haml_lint (~> 0.21.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0) html-pipeline (~> 1.11.0)
...@@ -984,10 +990,11 @@ DEPENDENCIES ...@@ -984,10 +990,11 @@ DEPENDENCIES
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec_profiling (~> 0.0.5) rspec_profiling (~> 0.0.5)
rubocop (~> 0.47.1) rubocop (~> 0.47.1)
rubocop-rspec (~> 1.12.0) rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rugged (~> 0.24.0) rufus-scheduler (~> 3.1.10)
rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
...@@ -1014,6 +1021,7 @@ DEPENDENCIES ...@@ -1014,6 +1021,7 @@ DEPENDENCIES
test_after_commit (~> 1.1) test_after_commit (~> 1.1)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
toml-rb (~> 0.3.15)
truncato (~> 0.7.8) truncato (~> 0.7.8)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
...@@ -1026,8 +1034,8 @@ DEPENDENCIES ...@@ -1026,8 +1034,8 @@ DEPENDENCIES
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
webmock (~> 1.24.0) webmock (~> 1.24.0)
webpack-rails (~> 0.9.9) webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.14.5 1.14.6
...@@ -33,7 +33,7 @@ core team members will mention this person. ...@@ -33,7 +33,7 @@ core team members will mention this person.
### Merge request coaching ### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get Several people from the [GitLab team][team] are helping community members to get
their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done). their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
...@@ -57,19 +57,72 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -57,19 +57,72 @@ star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html [Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature Freeze ## 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 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 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.
Merge requests may still be merged into master during this period, 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. 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. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th
These types of merge requests for the upcoming release need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack
and a dedicated team with front-end, back-end, and UX.
* **Small features**: any other feature request.
**Large features** must be with a maintainer **by the 1st**. This means that:
* There is a merge request (even if it's WIP).
* The person (or people, if it needs a frontend and backend maintainer) who will
ultimately be responsible for merging this have been pinged on the MR.
It's OK if merge request isn't completely done, but this allows the maintainer
enough time to make the decision about whether this can make it in before the
freeze. If the maintainer doesn't think it will make it, they should inform the
developers working on it and the Product Manager responsible for the feature.
The maintainer can also choose to assign a reviewer to perform an initial
review, but this way the maintainer is unlikely to be surprised by receiving an
MR later in the cycle.
**Small features** must be with a reviewer (not necessarily maintainer) **by the
3rd**.
Most merge requests from the community do not have a specific release
target. However, if one does and falls into either of the above categories, it's
the reviewer's responsibility to manage the above communication and assignment
on behalf of the community member.
### On the 7th
Merge requests should still be complete, following the
[definition of done][done]. The single exception is documentation, and this can
only be left until after the freeze if:
* There is a follow-up issue to add documentation.
* It is assigned to the person writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
All Community Edition merge requests from GitLab team members merged on the
freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts].
### After the 7th
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch. and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd). These fixes will be shipped in the next RC for that release if it is before the 22nd.
If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release.
If you think a merge request should go into the upcoming release even though it does not meet these requirements, 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. Exceptions require sign-off from 3 people besides the developer:
1. a Release Manager 1. a Release Manager
...@@ -158,3 +211,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -158,3 +211,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html
8.18.0-pre 9.1.0-pre
/* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
...@@ -6,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji'; ...@@ -6,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid'; import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
const requestAnimationFrame = window.requestAnimationFrame || const requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
...@@ -51,7 +54,7 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -51,7 +54,7 @@ function renderCategory(name, emojiList, opts = {}) {
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
${name} ${name}
</h5> </h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass}"> <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
...@@ -103,8 +106,9 @@ function AwardsHandler() { ...@@ -103,8 +106,9 @@ function AwardsHandler() {
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon'); const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$target.closest('.js-awards-block').addClass('current'); $target.closest('.js-awards-block').addClass('current');
return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
}); });
} }
...@@ -124,16 +128,18 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -124,16 +128,18 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
} }
const $menu = $('.emoji-menu'); const $menu = $('.emoji-menu');
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) { if ($menu.length) {
if ($menu.is('.is-visible')) { if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active'); $addBtn.removeClass('is-active');
$menu.removeClass('is-visible'); $menu.removeClass('is-visible');
$('#emoji_search').blur(); $('.js-emoji-menu-search').blur();
} else { } else {
$addBtn.addClass('is-active'); $addBtn.addClass('is-active');
this.positionMenu($menu, $addBtn); this.positionMenu($menu, $addBtn);
$menu.addClass('is-visible'); $menu.addClass('is-visible');
$('#emoji_search').focus(); $('.js-emoji-menu-search').focus();
} }
} else { } else {
$addBtn.addClass('is-loading is-active'); $addBtn.addClass('is-loading is-active');
...@@ -143,10 +149,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -143,10 +149,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
this.positionMenu($createdMenu, $addBtn); this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => { return setTimeout(() => {
$createdMenu.addClass('is-visible'); $createdMenu.addClass('is-visible');
$('#emoji_search').focus(); $('.js-emoji-menu-search').focus();
}, 200); }, 200);
}); });
} }
$thumbsBtn.toggleClass('disabled', $userAuthored);
}; };
// Create the emoji menu with the first category of emojis. // Create the emoji menu with the first category of emojis.
...@@ -174,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { ...@@ -174,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
const emojiMenuMarkup = ` const emojiMenuMarkup = `
<div class="emoji-menu"> <div class="emoji-menu">
<input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content"> <div class="emoji-menu-content">
${frequentlyUsedCatgegory} ${frequentlyUsedCatgegory}
...@@ -259,11 +267,13 @@ AwardsHandler.prototype.addAward = function addAward( ...@@ -259,11 +267,13 @@ AwardsHandler.prototype.addAward = function addAward(
callback, callback,
) { ) {
const normalizedEmoji = this.normalizeEmojiName(emoji); const normalizedEmoji = this.normalizeEmojiName(emoji);
this.postEmoji(awardUrl, normalizedEmoji, () => { const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined; return typeof callback === 'function' ? callback() : undefined;
}); });
return $('.emoji-menu').removeClass('is-visible'); $('.emoji-menu').removeClass('is-visible');
$('.js-add-award.is-active').removeClass('is-active');
}; };
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
...@@ -323,6 +333,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) { ...@@ -323,6 +333,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) {
return $emojiButton.hasClass('active'); return $emojiButton.hasClass('active');
}; };
AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) {
return $button.hasClass('js-user-authored');
};
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
const counter = $('.js-counter', $emojiButton); const counter = $('.js-counter', $emojiButton);
const counterNumber = parseInt(counter.text(), 10); const counterNumber = parseInt(counter.text(), 10);
...@@ -427,20 +441,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { ...@@ -427,20 +441,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
}); });
}; };
AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) { AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) {
return $.post(awardUrl, { if (this.isUserAuthored($emojiButton)) {
name: emoji, this.userAuthored($emojiButton);
}, (data) => { } else {
if (data.ok) { $.post(awardUrl, {
callback(); name: emoji,
} }, (data) => {
}); if (data.ok) {
callback();
}
}).fail(() => new Flash('Something went wrong on our end.'));
}
}; };
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
}; };
AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) {
const oldTitle = this.getAwardTooltip($emojiButton);
const newTitle = 'You cannot vote on your own issue, MR and note';
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
// Restore tooltip back to award list
return setTimeout(() => {
$emojiButton.tooltip('hide');
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
}, 2800);
};
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
const options = { const options = {
scrollTop: $('.awards').offset().top - 110, scrollTop: $('.awards').offset().top - 110,
...@@ -473,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj ...@@ -473,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj
}; };
AwardsHandler.prototype.setupSearch = function setupSearch() { AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { const $search = $('.js-emoji-menu-search');
this.registerEventListener('on', $search, 'input', (e) => {
const term = $(e.target).val().trim(); const term = $(e.target).val().trim();
// Clean previous search results this.searchEmojis(term);
$('ul.emoji-menu-search, h5.emoji-search').remove(); });
if (term.length > 0) {
// Generate a search result block const $menu = $('.emoji-menu');
const h5 = $('<h5 class="emoji-search" />').text('Search results'); this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
const foundEmojis = this.searchEmojis(term).show(); if (e.target === e.currentTarget) {
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); // Clear the search
$('.emoji-menu-content ul, .emoji-menu-content h5').hide(); this.searchEmojis('');
$('.emoji-menu-content').append(h5).append(ul);
} else {
$('.emoji-menu-content').children().show();
} }
}); });
}; };
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
const $search = $('.js-emoji-menu-search');
$search.val(term);
// Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
if (term.length > 0) {
// Generate a search result block
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.findMatchingEmojiElements(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
$('.emoji-menu-content').append(h5).append(ul);
} else {
$('.emoji-menu-content').children().show();
}
};
AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) {
const safeTerm = term.toLowerCase(); const safeTerm = term.toLowerCase();
const namesMatchingAlias = []; const namesMatchingAlias = [];
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */ import autosize from 'vendor/autosize';
/* global autosize */
var autosize = require('vendor/autosize'); $(() => {
const $fields = $('.js-autosize');
(function() { $fields.on('autosize:resized', function resized() {
$(function() { const $field = $(this);
var $fields; $field.data('height', $field.outerHeight());
$fields = $('.js-autosize');
$fields.on('autosize:resized', function() {
var $field;
$field = $(this);
return $field.data('height', $field.outerHeight());
});
$fields.on('resize.autosize', function() {
var $field;
$field = $(this);
if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight());
autosize.destroy($field);
return $field.css('max-height', window.outerHeight);
}
});
autosize($fields);
autosize.update($fields);
return $fields.css('resize', 'vertical');
}); });
}).call(window);
$fields.on('resize.autosize', function resize() {
const $field = $(this);
if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight());
autosize.destroy($field);
$field.css('max-height', window.outerHeight);
}
});
autosize($fields);
autosize.update($fields);
$fields.css('resize', 'vertical');
});
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
var container;
container = $(this).closest(".js-details-container");
return container.toggleClass("open");
});
// Show details content. Hides link after click.
//
// %div
// %a.js-details-expand
// %div.js-details-content
//
return $("body").on("click", ".js-details-expand", function(e) {
$(this).next('.js-details-content').removeClass("hide");
$(this).hide();
var truncatedItem = $(this).siblings('.js-details-short'); $(() => {
if (truncatedItem.length) { $('body').on('click', '.js-details-target', function target() {
truncatedItem.addClass("hide"); $(this).closest('.js-details-container').toggleClass('open');
}
return e.preventDefault();
});
}); });
}).call(window);
// Show details content. Hides link after click.
//
// %div
// %a.js-details-expand
// %div.js-details-content
//
$('body').on('click', '.js-details-expand', function expand(e) {
e.preventDefault();
$(this).next('.js-details-content').removeClass('hide');
$(this).hide();
const truncatedItem = $(this).siblings('.js-details-short');
if (truncatedItem.length) {
truncatedItem.addClass('hide');
}
});
});
import spreadString from './spread_string';
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ // On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16) const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16) const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
...@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) { ...@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16) const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16) const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) { function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0); const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5; return cp >= tone1 && cp <= tone5;
}); });
...@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) { ...@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing // doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) { function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode); isSkinToneComboEmoji(emojiUnicode);
} }
...@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) ...@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) { function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false; let hasPersonEmoji = false;
let hasZwj = false; let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => { Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0); const cp = character.codePointAt(0);
if (cp === zwj) { if (cp === zwj) {
hasZwj = true; hasZwj = true;
......
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
function knownCharCodeAt(givenString, index) {
const str = `${givenString}`;
const end = str.length;
const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
let idx = index;
while ((surrogatePairs.exec(str)) != null) {
const li = surrogatePairs.lastIndex;
if (li - 2 < idx) {
idx += 1;
} else {
break;
}
}
if (idx >= end || idx < 0) {
return NaN;
}
const code = str.charCodeAt(idx);
let high;
let low;
if (code >= 0xD800 && code <= 0xDBFF) {
high = code;
low = str.charCodeAt(idx + 1);
// Go one further, since one of the "characters" is part of a surrogate pair
return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
return code;
}
// See http://stackoverflow.com/a/38901550/796832
// ES5/PhantomJS compatible version of spreading a string
//
// [...'foo'] -> ['f', 'o', 'o']
// [...'🖐🏿'] -> ['🖐', '🏿']
function spreadString(str) {
const arr = [];
let i = 0;
while (!isNaN(knownCharCodeAt(str, i))) {
const codePoint = knownCharCodeAt(str, i);
arr.push(String.fromCodePoint(codePoint));
i += 1;
}
return arr;
}
export default spreadString;
import './autosize';
import './bind_in_out';
import './details_behavior';
import { installGlEmojiElement } from './gl_emoji';
import './quick_submit';
import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, max-len */ import '../commons/bootstrap';
// Quick Submit behavior // Quick Submit behavior
// //
// When a child field of a form with a `js-quick-submit` class receives a // When a child field of a form with a `js-quick-submit` class receives a
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted. // is submitted.
//
import '../commons/bootstrap';
// //
// ### Example Markup // ### Example Markup
// //
...@@ -17,61 +14,59 @@ import '../commons/bootstrap'; ...@@ -17,61 +14,59 @@ import '../commons/bootstrap';
// <input type="submit" value="Submit" /> // <input type="submit" value="Submit" />
// </form> // </form>
// //
(function() {
var isMac, keyCodeIs;
isMac = function() { function isMac() {
return navigator.userAgent.match(/Macintosh/); return navigator.userAgent.match(/Macintosh/);
}; }
keyCodeIs = function(e, keyCode) { function keyCodeIs(e, keyCode) {
if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) { if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) {
return false; return false;
} }
return e.keyCode === keyCode; return e.keyCode === keyCode;
}; }
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) { $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
var $form, $submit_button; // Enter
// Enter if (!keyCodeIs(e, 13)) {
if (!keyCodeIs(e, 13)) { return;
return; }
}
if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) { const onlyMeta = e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey;
return; const onlyCtrl = e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey;
} if (!onlyMeta && !onlyCtrl) {
e.preventDefault(); return;
$form = $(e.target).closest('form'); }
$submit_button = $form.find('input[type=submit], button[type=submit]');
if ($submit_button.attr('disabled')) { e.preventDefault();
return; const $form = $(e.target).closest('form');
} const $submitButton = $form.find('input[type=submit], button[type=submit]');
$submit_button.disable();
return $form.submit(); if (!$submitButton.attr('disabled')) {
}); $submitButton.disable();
$form.submit();
}
});
// If the user tabs to a submit button on a `js-quick-submit` form, display a
// tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function displayTooltip(e) {
// Tab
if (!keyCodeIs(e, 9)) {
return;
}
const $this = $(this);
const title = isMac() ?
'You can also press &#8984;-Enter' :
'You can also press Ctrl-Enter';
// If the user tabs to a submit button on a `js-quick-submit` form, display a $this.tooltip({
// tooltip to let them know they could've used the hotkey container: 'body',
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) { html: 'true',
var $this, title; placement: 'auto top',
// Tab title,
if (!keyCodeIs(e, 9)) { trigger: 'manual',
return;
}
if (isMac()) {
title = "You can also press &#8984;-Enter";
} else {
title = "You can also press Ctrl-Enter";
}
$this = $(this);
return $this.tooltip({
container: 'body',
html: 'true',
placement: 'auto top',
title: title,
trigger: 'manual'
}).tooltip('show').one('blur', function() {
return $this.tooltip('hide');
});
}); });
}).call(window); $this.tooltip('show').one('blur', () => $this.tooltip('hide'));
});
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, max-len */ import '../commons/bootstrap';
// Requires Input behavior // Requires Input behavior
// //
// When called on a form with input fields with the `required` attribute, the // When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values. // form's submit button will be disabled until all required fields have values.
//
import '../commons/bootstrap';
// //
// ### Example Markup // ### Example Markup
// //
...@@ -14,49 +12,43 @@ import '../commons/bootstrap'; ...@@ -14,49 +12,43 @@ import '../commons/bootstrap';
// <input type="submit" value="Submit"> // <input type="submit" value="Submit">
// </form> // </form>
// //
(function() {
$.fn.requiresInput = function() {
var $button, $form, fieldSelector, requireInput, required;
$form = $(this);
$button = $('button[type=submit], input[type=submit]', $form);
required = '[required=required]';
fieldSelector = "input" + required + ", select" + required + ", textarea" + required;
requireInput = function() {
var values;
values = _.map($(fieldSelector, $form), function(field) {
// Collect the input values of *all* required fields
return field.value;
});
// Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) {
return $button.disable();
} else {
return $button.enable();
}
};
// Set initial button state
requireInput();
return $form.on('change input', fieldSelector, requireInput);
};
$(function() { $.fn.requiresInput = function requiresInput() {
var $form, hideOrShowHelpBlock; const $form = $(this);
$form = $('form.js-requires-input'); const $button = $('button[type=submit], input[type=submit]', $form);
$form.requiresInput(); const fieldSelector = 'input[required=required], select[required=required], textarea[required=required]';
// Hide or Show the help block when creating a new project
// based on the option selected function requireInput() {
hideOrShowHelpBlock = function(form) { // Collect the input values of *all* required fields
var selected; const values = _.map($(fieldSelector, $form), field => field.value);
selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('options-parent') === 'groups') { // Disable the button if any required fields are empty
return form.find('.help-block').hide(); if (values.length && _.any(values, _.isEmpty)) {
} else if (selected.length) { $button.disable();
return form.find('.help-block').show(); } else {
} $button.enable();
}; }
hideOrShowHelpBlock($form); }
return $('.select2.js-select-namespace').change(function() {
return hideOrShowHelpBlock($form); // Set initial button state
}); requireInput();
}); $form.on('change input', fieldSelector, requireInput);
}).call(window); };
// Hide or Show the help block when creating a new project
// based on the option selected
function hideOrShowHelpBlock(form) {
const selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('options-parent') === 'groups') {
form.find('.help-block').hide();
} else if (selected.length) {
form.find('.help-block').show();
}
}
$(() => {
const $form = $('form.js-requires-input');
$form.requiresInput();
hideOrShowHelpBlock($form);
$('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form));
});
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) { // Toggle button. Show/hide content inside parent container.
$(function() { // Button does not change visibility. If button has icon - it changes chevron style.
var toggleContainer = function(container, /* optional */toggleState) { //
var $container = $(container); // %div.js-toggle-container
// %button.js-toggle-button
$container // %div.js-toggle-content
.find('.js-toggle-button .fa') //
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); $(() => {
function toggleContainer(container, toggleState) {
$container const $container = $(container);
.find('.js-toggle-content')
.toggle(toggleState); $container
}; .find('.js-toggle-button .fa')
.toggleClass('fa-chevron-up', toggleState)
// Toggle button. Show/hide content inside parent container. .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
// Button does not change visibility. If button has icon - it changes chevron style.
// $container
// %div.js-toggle-container .find('.js-toggle-content')
// %a.js-toggle-button .toggle(toggleState);
// %div.js-toggle-content }
//
$('body').on('click', '.js-toggle-button', function(e) { $('body').on('click', '.js-toggle-button', function toggleButton(e) {
toggleContainer($(this).closest('.js-toggle-container')); e.target.classList.toggle('open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') { const targetTag = e.currentTarget.tagName.toLowerCase();
e.preventDefault(); if (targetTag === 'a' || targetTag === 'button') {
} e.preventDefault();
});
// If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container!
var hash = w.gl.utils.getLocationHash();
var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container');
if (container) {
toggleContainer(container, true);
anchor.scrollIntoView();
} }
}); });
})(window);
// If we're accessing a permalink, ensure it is not inside a
// closed js-toggle-container!
const hash = window.gl.utils.getLocationHash();
const anchor = hash && document.getElementById(hash);
const container = anchor && $(anchor).closest('.js-toggle-container');
if (container) {
toggleContainer(container, true);
anchor.scrollIntoView();
}
});
import * as THREE from 'three/build/three.module';
import STLLoaderClass from 'three-stl-loader';
import OrbitControlsClass from 'three-orbit-controls';
import MeshObject from './mesh_object';
const STLLoader = STLLoaderClass(THREE);
const OrbitControls = OrbitControlsClass(THREE);
export default class Renderer {
constructor(container) {
this.renderWrapper = this.render.bind(this);
this.objects = [];
this.container = container;
this.width = this.container.offsetWidth;
this.height = 500;
this.loader = new STLLoader();
this.fov = 45;
this.camera = new THREE.PerspectiveCamera(
this.fov,
this.width / this.height,
1,
1000,
);
this.scene = new THREE.Scene();
this.scene.add(this.camera);
// Setup the viewer
this.setupRenderer();
this.setupGrid();
this.setupLight();
// Setup OrbitControls
this.controls = new OrbitControls(
this.camera,
this.renderer.domElement,
);
this.controls.minDistance = 5;
this.controls.maxDistance = 30;
this.controls.enableKeys = false;
this.loadFile();
}
setupRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true,
});
this.renderer.setClearColor(0xFFFFFF);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(
this.width,
this.height,
);
}
setupLight() {
// Point light illuminates the object
const pointLight = new THREE.PointLight(
0xFFFFFF,
2,
0,
);
pointLight.castShadow = true;
this.camera.add(pointLight);
// Ambient light illuminates the scene
const ambientLight = new THREE.AmbientLight(
0xFFFFFF,
1,
);
this.scene.add(ambientLight);
}
setupGrid() {
this.grid = new THREE.GridHelper(
20,
20,
0x000000,
0x000000,
);
this.scene.add(this.grid);
}
loadFile() {
this.loader.load(this.container.dataset.endpoint, (geo) => {
const obj = new MeshObject(geo);
this.objects.push(obj);
this.scene.add(obj);
this.start();
this.setDefaultCameraPosition();
});
}
start() {
// Empty the container first
this.container.innerHTML = '';
// Add to DOM
this.container.appendChild(this.renderer.domElement);
// Make controls visible
this.container.parentNode.classList.remove('is-stl-loading');
this.render();
}
render() {
this.renderer.render(
this.scene,
this.camera,
);
requestAnimationFrame(this.renderWrapper);
}
changeObjectMaterials(type) {
this.objects.forEach((obj) => {
obj.changeMaterial(type);
});
}
setDefaultCameraPosition() {
const obj = this.objects[0];
const radius = (obj.geometry.boundingSphere.radius / 1.5);
const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2));
this.camera.position.set(
0,
dist + 1,
dist,
);
this.camera.lookAt(this.grid);
this.controls.update();
}
}
import {
Matrix4,
MeshLambertMaterial,
Mesh,
} from 'three/build/three.module';
const defaultColor = 0xE24329;
const materials = {
default: new MeshLambertMaterial({
color: defaultColor,
}),
wireframe: new MeshLambertMaterial({
color: defaultColor,
wireframe: true,
}),
};
export default class MeshObject extends Mesh {
constructor(geo) {
super(
geo,
materials.default,
);
this.geometry.computeBoundingSphere();
this.rotation.set(-Math.PI / 2, 0, 0);
if (this.geometry.boundingSphere.radius > 4) {
const scale = 4 / this.geometry.boundingSphere.radius;
this.geometry.applyMatrix(
new Matrix4().makeScale(
scale,
scale,
scale,
),
);
this.geometry.computeBoundingSphere();
this.position.x = -this.geometry.boundingSphere.center.x;
this.position.z = this.geometry.boundingSphere.center.y;
}
}
changeMaterial(type) {
this.material = materials[type];
}
}
...@@ -35,7 +35,7 @@ export default class BlobFileDropzone { ...@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { this.on('sending', function (file, xhr, formData) {
formData.append('target_branch', form.find('input[name="target_branch"]').val()); formData.append('branch_name', form.find('input[name="branch_name"]').val());
formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val());
formData.append('commit_message', form.find('.js-commit-message').val()); formData.append('commit_message', form.find('.js-commit-message').val());
}); });
......
function BlobForkSuggestion(openButton, cancelButton, suggestionSection) {
if (openButton) {
openButton.addEventListener('click', () => {
suggestionSection.classList.remove('hidden');
});
}
if (cancelButton) {
cancelButton.addEventListener('click', () => {
suggestionSection.classList.add('hidden');
});
}
}
export default BlobForkSuggestion;
/* eslint-disable class-methods-use-this */
/* global Flash */
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
export default class FileTemplateMediator {
constructor({ editor, currentAction }) {
this.editor = editor;
this.currentAction = currentAction;
this.initTemplateSelectors();
this.initTemplateTypeSelector();
this.initDomElements();
this.initDropdowns();
this.initPageEvents();
}
initTemplateSelectors() {
// Order dictates template type dropdown item order
this.templateSelectors = [
GitignoreSelector,
BlobCiYamlSelector,
DockerfileSelector,
LicenseSelector,
].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this }));
}
initTemplateTypeSelector() {
this.typeSelector = new FileTemplateTypeSelector({
mediator: this,
dropdownData: this.templateSelectors
.map((templateSelector) => {
const cfg = templateSelector.config;
return {
name: cfg.name,
key: cfg.key,
};
}),
});
}
initDomElements() {
const $templatesMenu = $('.template-selectors-menu');
const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu');
const $fileEditor = $('.file-editor');
this.$templatesMenu = $templatesMenu;
this.$undoMenu = $undoMenu;
this.$undoBtn = $undoMenu.find('button');
this.$templateSelectors = $templatesMenu.find('.template-selector-dropdowns-wrap');
this.$filenameInput = $fileEditor.find('.js-file-path-name-input');
this.$fileContent = $fileEditor.find('#file-content');
this.$commitForm = $fileEditor.find('form');
this.$navLinks = $fileEditor.find('.nav-links');
}
initDropdowns() {
if (this.currentAction === 'create') {
this.typeSelector.show();
} else {
this.hideTemplateSelectorMenu();
}
this.displayMatchedTemplateSelector();
}
initPageEvents() {
this.listenForFilenameInput();
this.prepFileContentForSubmit();
this.listenForPreviewMode();
}
listenForFilenameInput() {
this.$filenameInput.on('keyup blur', () => {
this.displayMatchedTemplateSelector();
});
}
prepFileContentForSubmit() {
this.$commitForm.submit(() => {
this.$fileContent.val(this.editor.getValue());
});
}
listenForPreviewMode() {
this.$navLinks.on('click', 'a', (e) => {
const urlPieces = e.target.href.split('#');
const hash = urlPieces[1];
if (hash === 'preview') {
this.hideTemplateSelectorMenu();
} else if (hash === 'editor') {
this.showTemplateSelectorMenu();
}
});
}
selectTemplateType(item, el, e) {
if (e) {
e.preventDefault();
}
this.templateSelectors.forEach((selector) => {
if (selector.config.key === item.key) {
selector.show();
} else {
selector.hide();
}
});
this.typeSelector.setToggleText(item.name);
this.cacheToggleText();
}
selectTemplateFile(selector, query, data) {
selector.renderLoading();
// in case undo menu is already already there
this.destroyUndoMenu();
this.fetchFileTemplate(selector.config.endpoint, query, data)
.then((file) => {
this.showUndoMenu();
this.setEditorContent(file);
this.setFilename(selector.config.name);
selector.renderLoaded();
})
.catch(err => new Flash(`An error occurred while fetching the template: ${err}`));
}
displayMatchedTemplateSelector() {
const currentInput = this.getFilename();
this.templateSelectors.forEach((selector) => {
const match = selector.config.pattern.test(currentInput);
if (match) {
this.typeSelector.show();
this.selectTemplateType(selector.config);
this.showTemplateSelectorMenu();
}
});
}
fetchFileTemplate(apiCall, query, data) {
return new Promise((resolve) => {
const resolveFile = file => resolve(file);
if (!data) {
apiCall(query, resolveFile);
} else {
apiCall(query, data, resolveFile);
}
});
}
setEditorContent(file) {
if (!file && file !== '') return;
const newValue = file.content || file;
this.editor.setValue(newValue, 1);
this.editor.focus();
this.editor.navigateFileStart();
}
findTemplateSelectorByKey(key) {
return this.templateSelectors.find(selector => selector.config.key === key);
}
showUndoMenu() {
this.$undoMenu.removeClass('hidden');
this.$undoBtn.on('click', () => {
this.restoreFromCache();
this.destroyUndoMenu();
});
}
destroyUndoMenu() {
this.cacheFileContents();
this.cacheToggleText();
this.$undoMenu.addClass('hidden');
this.$undoBtn.off('click');
}
hideTemplateSelectorMenu() {
this.$templatesMenu.hide();
}
showTemplateSelectorMenu() {
this.$templatesMenu.show();
}
cacheToggleText() {
this.cachedToggleText = this.getTemplateSelectorToggleText();
}
cacheFileContents() {
this.cachedContent = this.editor.getValue();
this.cachedFilename = this.getFilename();
}
restoreFromCache() {
this.setEditorContent(this.cachedContent);
this.setFilename(this.cachedFilename);
this.setTemplateSelectorToggleText();
}
getTemplateSelectorToggleText() {
return this.$templateSelectors
.find('.js-template-selector-wrap:visible .dropdown-toggle-text')
.text();
}
setTemplateSelectorToggleText() {
return this.$templateSelectors
.find('.js-template-selector-wrap:visible .dropdown-toggle-text')
.text(this.cachedToggleText);
}
getTypeSelectorToggleText() {
return this.typeSelector.getToggleText();
}
getFilename() {
return this.$filenameInput.val();
}
setFilename(name) {
this.$filenameInput.val(name);
}
getSelected() {
return this.templateSelectors.find(selector => selector.selected);
}
}
/* global Api */
export default class FileTemplateSelector {
constructor(mediator) {
this.mediator = mediator;
this.$dropdown = null;
this.$wrapper = null;
}
init() {
const cfg = this.config;
this.$dropdown = $(cfg.dropdown);
this.$wrapper = $(cfg.wrapper);
this.$loadingIcon = this.$wrapper.find('.fa-chevron-down');
this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
this.initDropdown();
}
show() {
if (this.$dropdown === null) {
this.init();
}
this.$wrapper.removeClass('hidden');
}
hide() {
if (this.$dropdown !== null) {
this.$wrapper.addClass('hidden');
}
}
getToggleText() {
return this.$dropdownToggleText.text();
}
setToggleText(text) {
this.$dropdownToggleText.text(text);
}
renderLoading() {
this.$loadingIcon
.addClass('fa-spinner fa-spin')
.removeClass('fa-chevron-down');
}
renderLoaded() {
this.$loadingIcon
.addClass('fa-chevron-down')
.removeClass('fa-spinner fa-spin');
}
reportSelection(query, el, e, data) {
e.preventDefault();
return this.mediator.selectTemplateFile(this, query, data);
}
}
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab';
Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
json: {},
};
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="iPython notebook loading">
</i>
</div>
<notebook-lab
v-if="!loading && !error"
:notebook="json"
code-css-class="code white" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst parsing the file.
</span>
</p>
</div>
`,
methods: {
loadFile() {
this.$http.get(el.dataset.endpoint)
.then((res) => {
this.json = res.json();
this.loading = false;
})
.catch((e) => {
if (e.status) {
this.loadError = true;
}
this.error = true;
});
},
},
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
});
};
import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook);
/* eslint-disable no-new */
import Vue from 'vue';
import PDFLab from 'vendor/pdflab';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
export default () => {
const el = document.getElementById('js-pdf-viewer');
return new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
pdf: el.dataset.endpoint,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
</div>
<pdf-lab
v-if="!loadError"
:pdf="pdf"
@pdflabload="onLoad"
@pdflaberror="onError" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst decoding the file.
</span>
</p>
</div>
`,
});
};
import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF);
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
export default class SketchLoader {
constructor(container) {
this.container = container;
this.loadingIcon = this.container.querySelector('.js-loading-icon');
this.load();
}
load() {
return this.getZipFile()
.then(data => JSZip.loadAsync(data))
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
.then((content) => {
const url = window.URL || window.webkitURL;
const blob = new Blob([new Uint8Array(content)], {
type: 'image/png',
});
const previewUrl = url.createObjectURL(blob);
this.render(previewUrl);
})
.catch(this.error.bind(this));
}
getZipFile() {
return new JSZip.external.Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
render(previewUrl) {
const previewLink = document.createElement('a');
const previewImage = document.createElement('img');
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
previewImage.className = 'img-responsive';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
this.removeLoadingIcon();
}
error() {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
errorMsg.textContent = `
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
`;
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
}
removeLoadingIcon() {
if (this.loadingIcon) {
this.loadingIcon.remove();
}
}
}
/* eslint-disable no-new */
import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
});
import Renderer from './3d_viewer';
document.addEventListener('DOMContentLoaded', () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
el.addEventListener('click', (e) => {
const target = e.target;
e.preventDefault();
document.querySelector('.js-material-changer.active').classList.remove('active');
target.classList.add('active');
target.blur();
viewer.changeObjectMaterials(target.dataset.type);
});
});
});
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobCiYamlSelector extends TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
}
}
/* global Api */
import BlobCiYamlSelector from './blob_ci_yaml_selector';
export default class BlobCiYamlSelectors {
constructor({ editor, $dropdowns }) {
this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
this.initSelectors(editor);
}
initSelectors(editor) {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobCiYamlSelector({
editor,
pattern: /(.gitlab-ci.yml)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobDockerfileSelector extends TemplateSelector {
requestFile(query) {
return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
}
}
import BlobDockerfileSelector from './blob_dockerfile_selector';
export default class BlobDockerfileSelectors {
constructor({ editor, $dropdowns }) {
this.editor = editor;
this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
this.initSelectors();
}
initSelectors() {
const editor = this.editor;
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobDockerfileSelector({
editor,
pattern: /(Dockerfile)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobGitignoreSelector extends TemplateSelector {
requestFile(query) {
return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
}
}
import BlobGitignoreSelector from './blob_gitignore_selector';
export default class BlobGitignoreSelectors {
constructor({ editor, $dropdowns }) {
this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
this.editor = editor;
this.initSelectors();
}
initSelectors() {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobGitignoreSelector({
pattern: /(.gitignore)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
dropdown: $dropdown,
editor: this.editor,
});
});
}
}
/* global Api */
import TemplateSelector from './template_selector';
export default class BlobLicenseSelector extends TemplateSelector {
requestFile(query) {
const data = {
project: this.dropdown.data('project'),
fullname: this.dropdown.data('fullname'),
};
return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
}
}
/* eslint-disable no-unused-vars, no-param-reassign */
import BlobLicenseSelector from './blob_license_selector';
export default class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
this.$dropdowns = $dropdowns || $('.js-license-selector');
this.initSelectors(editor);
}
initSelectors(editor) {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
return new BlobLicenseSelector({
editor,
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-license-selector-wrap'),
dropdown: $dropdown,
});
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobCiYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitlab-ci-yaml',
name: '.gitlab-ci.yml',
pattern: /(.gitlab-ci.yml)/,
endpoint: Api.gitlabCiYml,
dropdown: '.js-gitlab-ci-yml-selector',
wrapper: '.js-gitlab-ci-yml-selector-wrap',
};
}
initDropdown() {
// maybe move to super class as well
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'dockerfile',
name: 'Dockerfile',
pattern: /(Dockerfile)/,
endpoint: Api.dockerfileYml,
dropdown: '.js-dockerfile-selector',
wrapper: '.js-dockerfile-selector-wrap',
};
}
initDropdown() {
// maybe move to super class as well
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobGitignoreSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitignore',
name: '.gitignore',
pattern: /(.gitignore)/,
endpoint: Api.gitignoreText,
dropdown: '.js-gitignore-selector',
wrapper: '.js-gitignore-selector-wrap',
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
text: item => item.name,
});
}
}
/* global Api */
import FileTemplateSelector from '../file_template_selector';
export default class BlobLicenseSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'license',
name: 'LICENSE',
pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
endpoint: Api.licenseText,
dropdown: '.js-license-selector',
wrapper: '.js-license-selector-wrap',
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
toggleLabel: item => item.name,
search: {
fields: ['name'],
},
clicked: (query, el, e) => {
const data = {
project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'),
};
this.reportSelection(query.id, el, e, data);
},
text: item => item.name,
});
}
}
import FileTemplateSelector from '../file_template_selector';
export default class FileTemplateTypeSelector extends FileTemplateSelector {
constructor({ mediator, dropdownData }) {
super(mediator);
this.mediator = mediator;
this.config = {
dropdown: '.js-template-type-selector',
wrapper: '.js-template-type-selector-wrap',
dropdownData,
};
}
initDropdown() {
this.$dropdown.glDropdown({
data: this.config.dropdownData,
filterable: false,
selectable: true,
toggleLabel: item => item.name,
clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e),
text: item => item.name,
});
}
}
...@@ -13,8 +13,9 @@ $(() => { ...@@ -13,8 +13,9 @@ $(() => {
const urlRoot = editBlobForm.data('relative-url-root'); const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix'); const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language'); const blobLanguage = editBlobForm.data('blob-language');
const currentAction = $('.js-file-title').data('current-action');
new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage); new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
new NewCommitForm(editBlobForm); new NewCommitForm(editBlobForm);
} }
......
/* global ace */ /* global ace */
import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
export default class EditBlob { export default class EditBlob {
constructor(assetsPath, aceMode) { constructor(assetsPath, aceMode, currentAction) {
this.configureAceEditor(aceMode, assetsPath); this.configureAceEditor(aceMode, assetsPath);
this.prepFileContentForSubmit();
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
this.initFileSelectors(); this.initFileSelectors(currentAction);
} }
configureAceEditor(aceMode, assetsPath) { configureAceEditor(aceMode, assetsPath) {
...@@ -19,6 +15,10 @@ export default class EditBlob { ...@@ -19,6 +15,10 @@ export default class EditBlob {
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
this.editor.focus(); this.editor.focus();
if (aceMode) { if (aceMode) {
...@@ -26,29 +26,13 @@ export default class EditBlob { ...@@ -26,29 +26,13 @@ export default class EditBlob {
} }
} }
prepFileContentForSubmit() { initFileSelectors(currentAction) {
$('form').submit(() => { this.fileTemplateMediator = new TemplateSelectorMediator({
$('#file-content').val(this.editor.getValue()); currentAction,
editor: this.editor,
}); });
} }
initFileSelectors() {
this.blobTemplateSelectors = [
new BlobLicenseSelectors({
editor: this.editor,
}),
new BlobGitignoreSelectors({
editor: this.editor,
}),
new BlobCiYamlSelectors({
editor: this.editor,
}),
new BlobDockerfileSelectors({
editor: this.editor,
}),
];
}
initModePanesAndLinks() { initModePanesAndLinks() {
this.$editModePanes = $('.js-edit-mode-pane'); this.$editModePanes = $('.js-edit-mode-pane');
this.$editModeLinks = $('.js-edit-mode a'); this.$editModeLinks = $('.js-edit-mode a');
......
...@@ -38,6 +38,10 @@ $(() => { ...@@ -38,6 +38,10 @@ $(() => {
Store.create(); Store.create();
// hack to allow sidebar scripts like milestone_select manipulate the BoardsStore
gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args);
gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args);
gl.IssueBoardsApp = new Vue({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
...@@ -81,6 +85,7 @@ $(() => { ...@@ -81,6 +85,7 @@ $(() => {
if (list.type === 'closed') { if (list.type === 'closed') {
list.position = Infinity; list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} }
}); });
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */ /* global Sortable */
import Vue from 'vue'; import Vue from 'vue';
import boardList from './board_list';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
require('./board_delete'); require('./board_delete');
require('./board_list'); require('./board_list');
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({ gl.issueBoards.Board = Vue.extend({
template: '#js-board-template', template: '#js-board-template',
components: { components: {
'board-list': gl.issueBoards.BoardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
boardBlankState, boardBlankState,
}, },
props: { props: {
list: Object, list: Object,
disabled: Boolean, disabled: Boolean,
issueLinkBase: String, issueLinkBase: String,
rootPath: String, rootPath: String,
}, },
data () { data () {
return { return {
detailIssue: Store.detail, detailIssue: Store.detail,
filter: Store.filter, filter: Store.filter,
}; };
}, },
watch: { watch: {
filter: { filter: {
handler() { handler() {
this.list.page = 1; this.list.page = 1;
this.list.getIssues(true); this.list.getIssues(true);
},
deep: true,
}, },
detailIssue: { deep: true,
handler () { },
if (!Object.keys(this.detailIssue.issue).length) return; detailIssue: {
handler () {
if (!Object.keys(this.detailIssue.issue).length) return;
const issue = this.list.findIssue(this.detailIssue.issue.id); const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) { if (issue) {
const offsetLeft = this.$el.offsetLeft; const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0]; const boardsList = document.querySelectorAll('.boards-list')[0];
const left = boardsList.scrollLeft - offsetLeft; const left = boardsList.scrollLeft - offsetLeft;
let right = (offsetLeft + this.$el.offsetWidth); let right = (offsetLeft + this.$el.offsetWidth);
if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
// -290 here because width of boardsList is animating so therefore // -290 here because width of boardsList is animating so therefore
// getting the width here is incorrect // getting the width here is incorrect
// 290 is the width of the sidebar // 290 is the width of the sidebar
right -= (boardsList.offsetWidth - 290); right -= (boardsList.offsetWidth - 290);
} else { } else {
right -= boardsList.offsetWidth; right -= boardsList.offsetWidth;
} }
if (right - boardsList.scrollLeft > 0) { if (right - boardsList.scrollLeft > 0) {
$(boardsList).animate({ $(boardsList).animate({
scrollLeft: right scrollLeft: right
}, this.sortableOptions.animation); }, this.sortableOptions.animation);
} else if (left > 0) { } else if (left > 0) {
$(boardsList).animate({ $(boardsList).animate({
scrollLeft: offsetLeft scrollLeft: offsetLeft
}, this.sortableOptions.animation); }, this.sortableOptions.animation);
}
} }
}, }
deep: true },
} deep: true
}, }
methods: { },
showNewIssueForm() { methods: {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; showNewIssueForm() {
} this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}, }
mounted () { },
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ mounted () {
disabled: this.disabled, this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
group: 'boards', disabled: this.disabled,
draggable: '.is-draggable', group: 'boards',
handle: '.js-board-handle', draggable: '.is-draggable',
onEnd: (e) => { handle: '.js-board-handle',
gl.issueBoards.onEnd(); onEnd: (e) => {
gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(); const order = this.sortable.toArray();
const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => { this.$nextTick(() => {
Store.moveList(list, order); Store.moveList(list, order);
}); });
}
} }
}); }
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
}, },
}); });
})();
...@@ -50,9 +50,7 @@ export default { ...@@ -50,9 +50,7 @@ export default {
this.showDetail = false; this.showDetail = false;
}, },
showIssue(e) { showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase(); if (e.target.classList.contains('js-no-trigger')) return;
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) { if (this.showDetail) {
this.showDetail = false; this.showDetail = false;
......
...@@ -2,22 +2,20 @@ ...@@ -2,22 +2,20 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { window.gl = window.gl || {};
window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardDelete = Vue.extend({ gl.issueBoards.BoardDelete = Vue.extend({
props: { props: {
list: Object list: Object
}, },
methods: { methods: {
deleteBoard () { deleteBoard () {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
if (confirm('Are you sure you want to delete this list?')) { if (confirm('Are you sure you want to delete this list?')) {
this.list.destroy(); this.list.destroy();
}
} }
} }
}); }
})(); });
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */ /* global Sortable */
import Vue from 'vue';
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue';
import boardCard from './board_card'; import boardCard from './board_card';
import eventHub from '../eventhub';
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({ export default {
template: '#js-board-list-template', name: 'BoardList',
components: { props: {
boardCard, disabled: {
boardNewIssue, type: Boolean,
required: true,
}, },
props: { list: {
disabled: Boolean, type: Object,
list: Object, required: true,
issues: Array,
loading: Boolean,
issueLinkBase: String,
rootPath: String,
}, },
data () { issues: {
return { type: Array,
scrollOffset: 250, required: true,
filters: Store.state.filters,
showCount: false,
showIssueForm: false
};
}, },
watch: { loading: {
filters: { type: Boolean,
handler () { required: true,
this.list.loadingMore = false; },
this.$refs.list.scrollTop = 0; issueLinkBase: {
}, type: String,
deep: true required: true,
}, },
issues () { rootPath: {
this.$nextTick(() => { type: String,
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { required: true,
this.list.page += 1; },
this.list.getIssues(false); },
} data() {
return {
scrollOffset: 250,
filters: Store.state.filters,
showCount: false,
showIssueForm: false,
};
},
components: {
boardCard,
boardNewIssue,
},
methods: {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage() {
const getIssues = this.list.nextPage();
if (this.scrollHeight() > Math.ceil(this.listHeight())) { if (getIssues) {
this.showCount = true; this.list.loadingMore = true;
} else { getIssues.then(() => {
this.showCount = false; this.list.loadingMore = false;
}
}); });
} }
}, },
methods: { toggleForm() {
listHeight () { this.showIssueForm = !this.showIssueForm;
return this.$refs.list.getBoundingClientRect().height; },
}, onScroll() {
scrollHeight () { if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
return this.$refs.list.scrollHeight; this.loadNextPage();
}, }
scrollTop () { },
return this.$refs.list.scrollTop + this.listHeight(); },
watch: {
filters: {
handler() {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
}, },
loadNextPage () { deep: true,
const getIssues = this.list.nextPage(); },
issues() {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (getIssues) { if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.list.loadingMore = true; this.showCount = true;
getIssues.then(() => { } else {
this.list.loadingMore = false; this.showCount = false;
});
} }
}, });
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
},
created() {
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
}, },
mounted () { },
const options = gl.issueBoards.getBoardSortableDefaultOptions({ created() {
scroll: document.querySelectorAll('.boards-list')[0], eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
group: 'issues', },
disabled: this.disabled, mounted() {
filter: '.board-list-count, .is-disabled', const options = gl.issueBoards.getBoardSortableDefaultOptions({
dataIdAttr: 'data-issue-id', scroll: document.querySelectorAll('.boards-list')[0],
onStart: (e) => { group: 'issues',
const card = this.$refs.issue[e.oldIndex]; disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
card.showDetail = false; card.showDetail = false;
Store.moving.list = card.list; Store.moving.list = card.list;
Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
gl.issueBoards.onStart(); gl.issueBoards.onStart();
}, },
onAdd: (e) => { onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); gl.issueBoards.BoardsStore
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
this.$nextTick(() => { this.$nextTick(() => {
e.item.remove(); e.item.remove();
}); });
}, },
onUpdate: (e) => { onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); gl.issueBoards.BoardsStore
}, .moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
onMove(e) { },
return !e.related.classList.contains('board-list-count'); onMove(e) {
} return !e.related.classList.contains('board-list-count');
}); },
});
this.sortable = Sortable.create(this.$refs.list, options); this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more // Scroll event on list to load more
this.$refs.list.onscroll = () => { this.$refs.list.addEventListener('scroll', this.onScroll);
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { },
this.loadNextPage(); beforeDestroy() {
} eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
}; this.$refs.list.removeEventListener('scroll', this.onScroll);
}, },
beforeDestroy() { template: `
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); <div class="board-list-component">
}, <div
}); class="board-list-loading text-center"
})(); aria-label="Loading issues"
v-if="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-id="-1">
<i
class="fa fa-spinner fa-spin"
aria-label="Loading more issues"
aria-hidden="true"
v-show="list.loadingMore">
</i>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
<span v-else>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
</div>
`,
};
/* global ListIssue */ /* global ListIssue */
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default {
...@@ -49,7 +51,7 @@ export default { ...@@ -49,7 +51,7 @@ export default {
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); eventHub.$emit(`hide-issue-form-${this.list.id}`);
}, },
}, },
mounted() { mounted() {
......
...@@ -8,66 +8,64 @@ import Vue from 'vue'; ...@@ -8,66 +8,64 @@ import Vue from 'vue';
require('./sidebar/remove_issue'); require('./sidebar/remove_issue');
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardSidebar = Vue.extend({ gl.issueBoards.BoardSidebar = Vue.extend({
props: { props: {
currentUser: Object currentUser: Object
}, },
data() { data() {
return { return {
detail: Store.detail, detail: Store.detail,
issue: {}, issue: {},
list: {}, list: {},
}; };
}, },
computed: { computed: {
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
} }
}, },
watch: { watch: {
detail: { detail: {
handler () { handler () {
if (this.issue.id !== this.detail.issue.id) { if (this.issue.id !== this.detail.issue.id) {
$('.js-issue-board-sidebar', this.$el).each((i, el) => { $('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu(); $(el).data('glDropdown').clearMenu();
});
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
},
issue () {
if (this.showSidebar) {
this.$nextTick(() => {
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
$('.right-sidebar').getNiceScroll().resize();
}); });
} }
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
}, },
methods: { issue () {
closeSidebar () { if (this.showSidebar) {
this.detail.issue = {}; this.$nextTick(() => {
$('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
$('.right-sidebar').getNiceScroll().resize();
});
} }
}, }
mounted () { },
new IssuableContext(this.currentUser); methods: {
new MilestoneSelect(); closeSidebar () {
new gl.DueDateSelectors(); this.detail.issue = {};
new LabelsSelect(); }
new Sidebar(); },
gl.Subscription.bindAll('.subscription'); mounted () {
}, new IssuableContext(this.currentUser);
components: { new MilestoneSelect();
removeBtn: gl.issueBoards.RemoveIssueBtn, new gl.DueDateSelectors();
}, new LabelsSelect();
}); new Sidebar();
})(); gl.Subscription.bindAll('.subscription');
},
components: {
removeBtn: gl.issueBoards.RemoveIssueBtn,
},
});
import Vue from 'vue'; import Vue from 'vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.IssueCardInner = Vue.extend({ gl.issueBoards.IssueCardInner = Vue.extend({
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
}, },
methods: { issueLinkBase: {
showLabel(label) { type: String,
if (!this.list) return true; required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`;
},
assigneeUrl() {
return `${this.rootPath}${this.issue.assignee.username}`;
},
assigneeUrlTitle() {
return `Assigned to ${this.issue.assignee.name}`;
},
avatarUrlTitle() {
return `Avatar for ${this.issue.assignee.name}`;
},
issueId() {
return `#${this.issue.id}`;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
showLabel(label) {
if (!this.list) return true;
return !this.list.label || label.id !== this.list.label.id; return !this.list.label || label.id !== this.list.label.id;
}, },
filterByLabel(label, e) { filterByLabel(label, e) {
if (!this.updateFilters) return; if (!this.updateFilters) return;
const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title); const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`; const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param); const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide'); $(e.currentTarget).tooltip('hide');
if (labelIndex === -1) { if (labelIndex === -1) {
filterPath.push(param); filterPath.push(param);
} else { } else {
filterPath.splice(labelIndex, 1); filterPath.splice(labelIndex, 1);
} }
gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
Store.updateFiltersUrl(); Store.updateFiltersUrl();
eventHub.$emit('updateTokens'); eventHub.$emit('updateTokens');
}, },
labelStyle(label) { labelStyle(label) {
return { return {
backgroundColor: label.color, backgroundColor: label.color,
color: label.textColor, color: label.textColor,
}; };
},
}, },
template: ` },
<div> template: `
<div>
<div class="card-header">
<h4 class="card-title"> <h4 class="card-title">
<i <i
class="fa fa-eye-slash confidential-icon" class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"></i> v-if="issue.confidential"
aria-hidden="true"
/>
<a <a
:href="issueLinkBase + '/' + issue.id" class="js-no-trigger"
:title="issue.title"> :href="cardUrl"
{{ issue.title }} :title="issue.title">{{ issue.title }}</a>
</a>
</h4>
<div class="card-footer">
<span <span
class="card-number" class="card-number"
v-if="issue.id"> v-if="issue.id"
#{{ issue.id }} >
{{ issueId }}
</span> </span>
<a </h4>
class="card-assignee has-tooltip" <a
:href="rootPath + issue.assignee.username" class="card-assignee has-tooltip js-no-trigger"
:title="'Assigned to ' + issue.assignee.name" :href="assigneeUrl"
v-if="issue.assignee" :title="assigneeUrlTitle"
data-container="body"> v-if="issue.assignee"
<img data-container="body"
class="avatar avatar-inline s20" >
:src="issue.assignee.avatar" <img
width="20" class="avatar avatar-inline s20 js-no-trigger"
height="20" :src="issue.assignee.avatar"
:alt="'Avatar for ' + issue.assignee.name" /> width="20"
</a> height="20"
<button :alt="avatarUrlTitle"
class="label color-label has-tooltip" />
v-for="label in issue.labels" </a>
type="button" </div>
v-if="showLabel(label)" <div class="card-footer" v-if="showLabelFooter">
@click="filterByLabel(label, $event)" <button
:style="labelStyle(label)" class="label color-label has-tooltip js-no-trigger"
:title="label.description" v-for="label in issue.labels"
data-container="body"> type="button"
{{ label.title }} v-if="showLabel(label)"
</button> @click="filterByLabel(label, $event)"
</div> :style="labelStyle(label)"
:title="label.description"
data-container="body">
{{ label.title }}
</button>
</div> </div>
`, </div>
}); `,
})(); });
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalEmptyState = Vue.extend({ gl.issueBoards.ModalEmptyState = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
props: {
image: {
type: String,
required: true,
}, },
props: { newIssuePath: {
image: { type: String,
type: String, required: true,
required: true,
},
newIssuePath: {
type: String,
required: true,
},
}, },
computed: { },
contents() { computed: {
const obj = { contents() {
title: 'You haven\'t added any issues to your project yet', const obj = {
content: ` title: 'You haven\'t added any issues to your project yet',
An issue can be a bug, a todo or a feature request that needs to be content: `
discussed in a project. Besides, issues are searchable and filterable. An issue can be a bug, a todo or a feature request that needs to be
`, discussed in a project. Besides, issues are searchable and filterable.
}; `,
};
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = 'You haven\'t selected any issues yet';
obj.content = ` obj.content = `
Go back to <strong>Open issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.
`; `;
} }
return obj; return obj;
},
}, },
template: ` },
<section class="empty-state"> template: `
<div class="row"> <section class="empty-state">
<div class="col-xs-12 col-sm-6 col-sm-push-6"> <div class="row">
<aside class="svg-content" v-html="image"></aside> <div class="col-xs-12 col-sm-6 col-sm-push-6">
</div> <aside class="svg-content" v-html="image"></aside>
<div class="col-xs-12 col-sm-6 col-sm-pull-6"> </div>
<div class="text-content"> <div class="col-xs-12 col-sm-6 col-sm-pull-6">
<h4>{{ contents.title }}</h4> <div class="text-content">
<p v-html="contents.content"></p> <h4>{{ contents.title }}</h4>
<a <p v-html="contents.content"></p>
:href="newIssuePath" <a
class="btn btn-success btn-inverted" :href="newIssuePath"
v-if="activeTab === 'all'"> class="btn btn-success btn-inverted"
New issue v-if="activeTab === 'all'">
</a> New issue
<button </a>
type="button" <button
class="btn btn-default" type="button"
@click="changeTab('all')" class="btn btn-default"
v-if="activeTab === 'selected'"> @click="changeTab('all')"
Open issues v-if="activeTab === 'selected'">
</button> Open issues
</div> </button>
</div> </div>
</div> </div>
</section> </div>
`, </section>
}); `,
})(); });
...@@ -5,80 +5,78 @@ import Vue from 'vue'; ...@@ -5,80 +5,78 @@ import Vue from 'vue';
require('./lists_dropdown'); require('./lists_dropdown');
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooter = Vue.extend({ gl.issueBoards.ModalFooter = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state, state: gl.issueBoards.BoardsStore.state,
}; };
},
computed: {
submitDisabled() {
return !ModalStore.selectedCount();
}, },
computed: { submitText() {
submitDisabled() { const count = ModalStore.selectedCount();
return !ModalStore.selectedCount();
},
submitText() {
const count = ModalStore.selectedCount();
return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
},
}, },
methods: { },
addIssues() { methods: {
const list = this.modal.selectedList || this.state.lists[0]; addIssues() {
const selectedIssues = ModalStore.getSelectedIssues(); const list = this.modal.selectedList || this.state.lists[0];
const issueIds = selectedIssues.map(issue => issue.globalId); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId);
// Post the data to the backend // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, { gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id], add_label_ids: [list.label.id],
}).catch(() => { }).catch(() => {
new Flash('Failed to update issues, please try again.', 'alert'); new Flash('Failed to update issues, please try again.', 'alert');
selectedIssues.forEach((issue) => {
list.removeIssue(issue);
list.issuesSize -= 1;
});
});
// Add the issues on the frontend
selectedIssues.forEach((issue) => { selectedIssues.forEach((issue) => {
list.addIssue(issue); list.removeIssue(issue);
list.issuesSize += 1; list.issuesSize -= 1;
}); });
});
this.toggleModal(false); // Add the issues on the frontend
}, selectedIssues.forEach((issue) => {
}, list.addIssue(issue);
components: { list.issuesSize += 1;
'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, });
this.toggleModal(false);
}, },
template: ` },
<footer components: {
class="form-actions add-issues-footer"> 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
<div class="pull-left"> },
<button template: `
class="btn btn-success" <footer
type="button" class="form-actions add-issues-footer">
:disabled="submitDisabled" <div class="pull-left">
@click="addIssues">
{{ submitText }}
</button>
<span class="inline add-issues-footer-to-list">
to list
</span>
<lists-dropdown></lists-dropdown>
</div>
<button <button
class="btn btn-default pull-right" class="btn btn-success"
type="button" type="button"
@click="toggleModal(false)"> :disabled="submitDisabled"
Cancel @click="addIssues">
{{ submitText }}
</button> </button>
</footer> <span class="inline add-issues-footer-to-list">
`, to list
}); </span>
})(); <lists-dropdown></lists-dropdown>
</div>
<button
class="btn btn-default pull-right"
type="button"
@click="toggleModal(false)">
Cancel
</button>
</footer>
`,
});
...@@ -3,80 +3,78 @@ import modalFilters from './filters'; ...@@ -3,80 +3,78 @@ import modalFilters from './filters';
require('./tabs'); require('./tabs');
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalHeader = Vue.extend({ gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
props: { props: {
projectId: { projectId: {
type: Number, type: Number,
required: true, required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { milestonePath: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { labelPath: {
selectAllText() { type: String,
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { required: true,
return 'Select all';
}
return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { },
toggleAll() { data() {
this.$refs.selectAllBtn.blur(); return ModalStore.store;
},
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
ModalStore.toggleAll(); return 'Deselect all';
}, },
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
}, },
components: { },
'modal-tabs': gl.issueBoards.ModalTabs, methods: {
modalFilters, toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll();
}, },
template: ` },
<div> components: {
<header class="add-issues-header form-actions"> 'modal-tabs': gl.issueBoards.ModalTabs,
<h2> modalFilters,
Add issues },
<button template: `
type="button" <div>
class="close" <header class="add-issues-header form-actions">
data-dismiss="modal" <h2>
aria-label="Close" Add issues
@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 <button
type="button" type="button"
class="btn btn-success btn-inverted prepend-left-10" class="close"
ref="selectAllBtn" data-dismiss="modal"
@click="toggleAll"> aria-label="Close"
{{ selectAllText }} @click="toggleModal(false)">
<span aria-hidden="true">×</span>
</button> </button>
</div> </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>
`, </div>
}); `,
})(); });
...@@ -8,160 +8,158 @@ require('./list'); ...@@ -8,160 +8,158 @@ require('./list');
require('./footer'); require('./footer');
require('./empty_state'); require('./empty_state');
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.IssuesModal = Vue.extend({ gl.issueBoards.IssuesModal = Vue.extend({
props: { props: {
blankStateImage: { blankStateImage: {
type: String, type: String,
required: true, required: true,
},
newIssuePath: {
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() { newIssuePath: {
return ModalStore.store; type: String,
required: true,
}, },
watch: { issueLinkBase: {
page() { type: String,
this.loadIssues(); required: true,
}, },
showAddIssuesModal() { rootPath: {
if (this.showAddIssuesModal && !this.issues.length) { type: String,
this.loading = true; 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;
this.loadIssues()
.then(() => {
this.loading = false;
});
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
this.loadIssues() this.loadIssues(true)
.then(() => { .then(() => {
this.loading = false; this.filterLoading = false;
}); });
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
} }
}, },
filter: { deep: true,
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
this.loadIssues(true)
.then(() => {
this.filterLoading = false;
});
}
},
deep: true,
},
}, },
methods: { },
loadIssues(clearIssues = false) { methods: {
if (!this.showAddIssuesModal) return false; loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService.getBacklog(queryData(this.filter.path, {
page: this.page,
per: this.perPage,
})).then((res) => {
const data = res.json();
if (clearIssues) {
this.issues = [];
}
data.issues.forEach((issueObj) => { return gl.boardService.getBacklog(queryData(this.filter.path, {
const issue = new ListIssue(issueObj); page: this.page,
const foundSelectedIssue = ModalStore.findSelectedIssue(issue); per: this.perPage,
issue.selected = !!foundSelectedIssue; })).then((res) => {
const data = res.json();
this.issues.push(issue); if (clearIssues) {
}); this.issues = [];
}
this.loadingNewPage = false; data.issues.forEach((issueObj) => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue;
if (!this.issuesCount) { this.issues.push(issue);
this.issuesCount = data.size;
}
}); });
},
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0; this.loadingNewPage = false;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0; if (!this.issuesCount) {
}, this.issuesCount = data.size;
}
});
}, },
created() { },
this.page = 1; computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0;
}, },
components: { showEmptyState() {
'modal-header': gl.issueBoards.ModalHeader, if (!this.loading && this.issuesCount === 0) {
'modal-list': gl.issueBoards.ModalList, return true;
'modal-footer': gl.issueBoards.ModalFooter, }
'empty-state': gl.issueBoards.ModalEmptyState,
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
}, },
template: ` },
<div created() {
class="add-issues-modal" this.page = 1;
v-if="showAddIssuesModal"> },
<div class="add-issues-container"> components: {
<modal-header 'modal-header': gl.issueBoards.ModalHeader,
:project-id="projectId" 'modal-list': gl.issueBoards.ModalList,
:milestone-path="milestonePath" 'modal-footer': gl.issueBoards.ModalFooter,
:label-path="labelPath"> 'empty-state': gl.issueBoards.ModalEmptyState,
</modal-header> },
<modal-list template: `
:image="blankStateImage" <div
:issue-link-base="issueLinkBase" class="add-issues-modal"
:root-path="rootPath" v-if="showAddIssuesModal">
v-if="!loading && showList && !filterLoading"></modal-list> <div class="add-issues-container">
<empty-state <modal-header
v-if="showEmptyState" :project-id="projectId"
:image="blankStateImage" :milestone-path="milestonePath"
:new-issue-path="newIssuePath"></empty-state> :label-path="labelPath">
<section </modal-header>
class="add-issues-list text-center" <modal-list
v-if="loading || filterLoading"> :image="blankStateImage"
<div class="add-issues-list-loading"> :issue-link-base="issueLinkBase"
<i class="fa fa-spinner fa-spin"></i> :root-path="rootPath"
</div> v-if="!loading && showList && !filterLoading"></modal-list>
</section> <empty-state
<modal-footer></modal-footer> v-if="showEmptyState"
</div> :image="blankStateImage"
:new-issue-path="newIssuePath"></empty-state>
<section
class="add-issues-list text-center"
v-if="loading || filterLoading">
<div class="add-issues-list-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
</section>
<modal-footer></modal-footer>
</div> </div>
`, </div>
}); `,
})(); });
...@@ -3,159 +3,157 @@ ...@@ -3,159 +3,157 @@
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalList = Vue.extend({ gl.issueBoards.ModalList = Vue.extend({
props: { props: {
issueLinkBase: { issueLinkBase: {
type: String, type: String,
required: true, required: true,
},
rootPath: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
}, },
data() { rootPath: {
return ModalStore.store; type: String,
required: true,
}, },
watch: { image: {
activeTab() { type: String,
if (this.activeTab === 'all') { required: true,
ModalStore.purgeUnselectedIssues();
}
},
}, },
computed: { },
loopIssues() { data() {
if (this.activeTab === 'all') { return ModalStore.store;
return this.issues; },
} watch: {
activeTab() {
return this.selectedIssues; if (this.activeTab === 'all') {
}, ModalStore.purgeUnselectedIssues();
groupedIssues() { }
const groups = []; },
this.loopIssues.forEach((issue, i) => { },
const index = i % this.columns; computed: {
loopIssues() {
if (!groups[index]) { if (this.activeTab === 'all') {
groups.push([]); return this.issues;
} }
groups[index].push(issue);
});
return groups; return this.selectedIssues;
},
}, },
methods: { groupedIssues() {
scrollHandler() { const groups = [];
const currentPage = Math.floor(this.issues.length / this.perPage); this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage if (!groups[index]) {
&& currentPage === this.page) { groups.push([]);
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; groups[index].push(issue);
}, });
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') { return groups;
this.columns = 3;
} else if (breakpoint === 'sm') {
this.columns = 2;
} else {
this.columns = 1;
}
},
}, },
mounted() { },
this.scrollHandlerWrapper = this.scrollHandler.bind(this); methods: {
this.setColumnCountWrapper = this.setColumnCount.bind(this); scrollHandler() {
this.setColumnCount(); const currentPage = Math.floor(this.issues.length / this.perPage);
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
window.addEventListener('resize', this.setColumnCountWrapper); && 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;
}, },
beforeDestroy() { scrollHeight() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); return this.$refs.list.scrollHeight;
window.removeEventListener('resize', this.setColumnCountWrapper);
}, },
components: { scrollTop() {
'issue-card-inner': gl.issueBoards.IssueCardInner, return this.$refs.list.scrollTop + this.listHeight();
}, },
template: ` showIssue(issue) {
<section if (this.activeTab === 'all') return true;
class="add-issues-list add-issues-list-columns"
ref="list"> 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 <div
class="empty-state add-issues-empty-state-filter text-center" class="svg-content"
v-if="issuesCount > 0 && issues.length === 0"> v-html="image">
<div </div>
class="svg-content" <div class="text-content">
v-html="image"> <h4>
</div> There are no issues to show.
<div class="text-content"> </h4>
<h4>
There are no issues to show.
</h4>
</div>
</div> </div>
</div>
<div
v-for="group in groupedIssues"
class="add-issues-list-column">
<div <div
v-for="group in groupedIssues" v-for="issue in group"
class="add-issues-list-column"> v-if="showIssue(issue)"
class="card-parent">
<div <div
v-for="issue in group" class="card"
v-if="showIssue(issue)" :class="{ 'is-active': issue.selected }"
class="card-parent"> @click="toggleIssue($event, issue)">
<div <issue-card-inner
class="card" :issue="issue"
:class="{ 'is-active': issue.selected }" :issue-link-base="issueLinkBase"
@click="toggleIssue($event, issue)"> :root-path="rootPath">
<issue-card-inner </issue-card-inner>
:issue="issue" <span
:issue-link-base="issueLinkBase" :aria-label="'Issue #' + issue.id + ' selected'"
:root-path="rootPath"> aria-checked="true"
</issue-card-inner> v-if="issue.selected"
<span class="issue-card-selected text-center">
:aria-label="'Issue #' + issue.id + ' selected'" <i class="fa fa-check"></i>
aria-checked="true" </span>
v-if="issue.selected"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
</div>
</div> </div>
</div> </div>
</section> </div>
`, </section>
}); `,
})(); });
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
state: gl.issueBoards.BoardsStore.state, state: gl.issueBoards.BoardsStore.state,
}; };
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[0];
}, },
computed: { },
selected() { destroyed() {
return this.modal.selectedList || this.state.lists[0]; this.modal.selectedList = null;
}, },
}, template: `
destroyed() { <div class="dropdown inline">
this.modal.selectedList = null; <button
}, class="dropdown-menu-toggle"
template: ` type="button"
<div class="dropdown inline"> data-toggle="dropdown"
<button aria-expanded="false">
class="dropdown-menu-toggle" <span
type="button" class="dropdown-label-box"
data-toggle="dropdown" :style="{ backgroundColor: selected.label.color }">
aria-expanded="false"> </span>
<span {{ selected.title }}
class="dropdown-label-box" <i class="fa fa-chevron-down"></i>
:style="{ backgroundColor: selected.label.color }"> </button>
</span> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
{{ selected.title }} <ul>
<i class="fa fa-chevron-down"></i> <li
</button> v-for="list in state.lists"
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> v-if="list.type == 'label'">
<ul> <a
<li href="#"
v-for="list in state.lists" role="button"
v-if="list.type == 'label'"> :class="{ 'is-active': list.id == selected.id }"
<a @click.prevent="modal.selectedList = list">
href="#" <span
role="button" class="dropdown-label-box"
:class="{ 'is-active': list.id == selected.id }" :style="{ backgroundColor: list.label.color }">
@click.prevent="modal.selectedList = list"> </span>
<span {{ list.title }}
class="dropdown-label-box" </a>
:style="{ backgroundColor: list.label.color }"> </li>
</span> </ul>
{{ list.title }}
</a>
</li>
</ul>
</div>
</div> </div>
`, </div>
}); `,
})(); });
import Vue from 'vue'; import Vue from 'vue';
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalTabs = Vue.extend({ gl.issueBoards.ModalTabs = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [gl.issueBoards.ModalMixins],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
}, },
computed: { },
selectedCount() { destroyed() {
return ModalStore.selectedCount(); this.activeTab = 'all';
}, },
}, template: `
destroyed() { <div class="top-area prepend-top-10 append-bottom-10">
this.activeTab = 'all'; <ul class="nav-links issues-state-filters">
}, <li :class="{ 'active': activeTab == 'all' }">
template: ` <a
<div class="top-area prepend-top-10 append-bottom-10"> href="#"
<ul class="nav-links issues-state-filters"> role="button"
<li :class="{ 'active': activeTab == 'all' }"> @click.prevent="changeTab('all')">
<a Open issues
href="#" <span class="badge">
role="button" {{ issuesCount }}
@click.prevent="changeTab('all')"> </span>
Open issues </a>
<span class="badge"> </li>
{{ issuesCount }} <li :class="{ 'active': activeTab == 'selected' }">
</span> <a
</a> href="#"
</li> role="button"
<li :class="{ 'active': activeTab == 'selected' }"> @click.prevent="changeTab('selected')">
<a Selected issues
href="#" <span class="badge">
role="button" {{ selectedCount }}
@click.prevent="changeTab('selected')"> </span>
Selected issues </a>
<span class="badge"> </li>
{{ selectedCount }} </ul>
</span> </div>
</a> `,
</li> });
</ul>
</div>
`,
});
})();
(() => { const ModalStore = gl.issueBoards.ModalStore;
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalMixins = { gl.issueBoards.ModalMixins = {
methods: { methods: {
toggleModal(toggle) { toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle; ModalStore.store.showAddIssuesModal = toggle;
},
changeTab(tab) {
ModalStore.store.activeTab = tab;
},
}, },
}; changeTab(tab) {
})(); ModalStore.store.activeTab = tab;
},
},
};
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
/* global ListLabel */ /* global ListLabel */
import queryData from '../utils/query_data'; import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List { class List {
constructor (obj) { constructor (obj) {
this.id = obj.id; this.id = obj.id;
...@@ -58,7 +60,9 @@ class List { ...@@ -58,7 +60,9 @@ class List {
nextPage () { nextPage () {
if (this.issuesSize > this.issues.length) { if (this.issuesSize > this.issues.length) {
this.page += 1; if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
}
return this.getIssues(false); return this.getIssues(false);
} }
...@@ -145,10 +149,7 @@ class List { ...@@ -145,10 +149,7 @@ class List {
} }
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid);
.then(() => {
listFrom.getIssues(false);
});
} }
findIssue (id) { findIssue (id) {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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