Commit a210ddaa authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into add-star-for-action-scope

* upstream/master: (3161 commits)
  Derive project path from import URL
  add article to list
  link to CI/CD Deep Dive demo for #5800
  Add code review guidelines related to Build [CI skip].
  Make time span dropdown style on cycle analytics page consistent
  copyedit, add article to the list
  Make Markdown autocomplete dropdown style consistent
  Add 204. Remove duplicated method.
  Make sure we didn't commit conflicts
  Fix bug in blob test
  Always fetch branches before finding the merge base, otherwise we could find an outdated merge base
  Fixes dropdown margin in sidebar
  Fixes the search losing focus
  Docs add blog articles
  Inline script cleanup globals and easy
  Add option to use CommitLanguages RPC
  CI fixes for gitaly-ruby
  fix
  Allow logged in users to read user list under public restriction
  Small refactor in LegacyNamespace and moved back send_update_instructions
  ...
parents 28220818 a623446a

Too many changes to show.

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

...@@ -10,10 +10,10 @@ engines: ...@@ -10,10 +10,10 @@ engines:
languages: languages:
- ruby - ruby
- javascript - javascript
exclude_paths:
- "lib/api/v3/*"
eslint: eslint:
enabled: true enabled: true
fixme:
enabled: true
rubocop: rubocop:
enabled: true enabled: true
ratings: ratings:
...@@ -35,4 +35,13 @@ exclude_paths: ...@@ -35,4 +35,13 @@ exclude_paths:
- node_modules/ - node_modules/
- spec/ - spec/
- vendor/ - vendor/
- lib/api/v3/ - .yarn-cache/
- tmp/
- builds/
- coverage/
- public/
- shared/
- webpack-report/
- log/
- backups/
- coverage-javascript/
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"gon": false, "gon": false,
"localStorage": false "localStorage": false
}, },
"parser": "babel-eslint",
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
......
...@@ -3,3 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb ...@@ -3,3 +3,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb lib/gitlab/diff/position_tracer.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
lib/gitlab/redis/*.rb
...@@ -21,6 +21,7 @@ eslint-report.html ...@@ -21,6 +21,7 @@ eslint-report.html
/.yarn-cache /.yarn-cache
/.byebug_history /.byebug_history
/Vagrantfile /Vagrantfile
/app/assets/javascripts/locale/**/app.js
/backups/* /backups/*
/config/aws.yml /config/aws.yml
/config/database.yml /config/database.yml
...@@ -30,6 +31,9 @@ eslint-report.html ...@@ -30,6 +31,9 @@ eslint-report.html
/config/initializers/smtp_settings.rb /config/initializers/smtp_settings.rb
/config/initializers/relative_url.rb /config/initializers/relative_url.rb
/config/resque.yml /config/resque.yml
/config/redis.cache.yml
/config/redis.queues.yml
/config/redis.shared_state.yml
/config/unicorn.rb /config/unicorn.rb
/config/secrets.yml /config/secrets.yml
/config/sidekiq.yml /config/sidekiq.yml
...@@ -59,3 +63,4 @@ eslint-report.html ...@@ -59,3 +63,4 @@ eslint-report.html
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/.rspec
This diff is collapsed.
Please read this! Please read this!
Before opening a new issue, make sure to search for keywords in the issues Before opening a new issue, make sure to search for keywords in the issues
filtered by the "regression" or "bug" label: filtered by the "regression" or "bug" label.
For the Community Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug
For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate. and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate.
...@@ -20,6 +27,12 @@ Please remove this notice if you're confident your issue isn't a duplicate. ...@@ -20,6 +27,12 @@ Please remove this notice if you're confident your issue isn't a duplicate.
(How one can reproduce the issue - this is very important) (How one can reproduce the issue - this is very important)
### Example Project
(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
### What is the current *bug* behavior? ### What is the current *bug* behavior?
(What actually happens) (What actually happens)
......
...@@ -3,8 +3,14 @@ Please read this! ...@@ -3,8 +3,14 @@ Please read this!
Before opening a new issue, make sure to search for keywords in the issues Before opening a new issue, make sure to search for keywords in the issues
filtered by the "feature proposal" label: filtered by the "feature proposal" label:
For the Community Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal
and verify the issue you're about to submit isn't a duplicate. and verify the issue you're about to submit isn't a duplicate.
Please remove this notice if you're confident your issue isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate.
...@@ -21,12 +27,24 @@ Please remove this notice if you're confident your issue isn't a duplicate. ...@@ -21,12 +27,24 @@ Please remove this notice if you're confident your issue isn't a duplicate.
### Documentation blurb ### Documentation blurb
(Write the start of the documentation of this feature here, include: #### Overview
What is it?
Why should someone use this feature?
What is the underlying (business) problem?
How do you use this feature?
#### Use cases
Who is this for? Provide one or more use cases.
### Feature checklist
1. Why should someone use it; what's the underlying problem. Make sure these are completed before closing the issue,
2. What is the solution. with a link to the relevant commit.
3. How does someone use this
During implementation, this can then be copied and used as a starter for the documentation.) - [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance)
- [ ] Documentation
- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
/label ~"feature proposal" /label ~"feature proposal"
\ No newline at end of file
7.5
\ No newline at end of file
--color
--format Fuubar
This diff is collapsed.
This diff is collapsed.
...@@ -10,7 +10,7 @@ linters: ...@@ -10,7 +10,7 @@ linters:
# Reports when you use improper spacing around ! (the "bang") in !default, # Reports when you use improper spacing around ! (the "bang") in !default,
# !global, !important, and !optional flags. # !global, !important, and !optional flags.
BangFormat: BangFormat:
enabled: false enabled: true
# Whether or not to prefer `border: 0` over `border: none`. # Whether or not to prefer `border: 0` over `border: none`.
BorderZero: BorderZero:
...@@ -43,10 +43,11 @@ linters: ...@@ -43,10 +43,11 @@ linters:
# Rule sets should be ordered as follows: # Rule sets should be ordered as follows:
# - @extend declarations # - @extend declarations
# - @include declarations without inner @content # - @include declarations without inner @content
# - properties, @include declarations with inner @content # - properties
# - @include declarations with inner @content
# - nested rule sets. # - nested rule sets.
DeclarationOrder: DeclarationOrder:
enabled: false enabled: true
# `scss-lint:disable` control comments should be preceded by a comment # `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file. # explaining why these linters are being disabled for this file.
...@@ -93,7 +94,7 @@ linters: ...@@ -93,7 +94,7 @@ linters:
# The basenames of @imported SCSS partials should not begin with an # The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension. # underscore and should not include the filename extension.
ImportPath: ImportPath:
enabled: false enabled: true
# Avoid using !important in properties. It is usually indicative of a # Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code. # misunderstanding of CSS specificity and can lead to brittle code.
...@@ -133,7 +134,7 @@ linters: ...@@ -133,7 +134,7 @@ linters:
# Reports when you use an unknown or disabled CSS property # Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties). # (ignoring vendor-prefixed properties).
PropertySpelling: PropertySpelling:
enabled: false enabled: true
# Configure which units are allowed for property values. # Configure which units are allowed for property values.
PropertyUnits: PropertyUnits:
...@@ -176,6 +177,10 @@ linters: ...@@ -176,6 +177,10 @@ linters:
# Commas in lists should be followed by a space. # Commas in lists should be followed by a space.
SpaceAfterComma: SpaceAfterComma:
enabled: true
# Comment literals should be followed by a space.
SpaceAfterComment:
enabled: false enabled: false
# Properties should be formatted with a single space separating the colon # Properties should be formatted with a single space separating the colon
...@@ -240,7 +245,7 @@ linters: ...@@ -240,7 +245,7 @@ linters:
# Do not use parent selector references (&) when they would otherwise # Do not use parent selector references (&) when they would otherwise
# be unnecessary. # be unnecessary.
UnnecessaryParentReference: UnnecessaryParentReference:
enabled: false enabled: true
# URLs should be valid and not contain protocols or domain names. # URLs should be valid and not contain protocols or domain names.
UrlFormat: UrlFormat:
......
This diff is collapsed.
...@@ -31,7 +31,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._ ...@@ -31,7 +31,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
- [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight) - [Issue weight](#issue-weight)
- [Regression issues](#regression-issues) - [Regression issues](#regression-issues)
- [Technical debt](#technical-debt) - [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship) - [Stewardship](#stewardship)
- [Merge requests](#merge-requests) - [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines) - [Merge request guidelines](#merge-request-guidelines)
...@@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._ ...@@ -49,6 +49,8 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
Thank you for your interest in contributing to GitLab. This guide details how Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is efficient for everyone. to contribute to GitLab in a way that is efficient for everyone.
Looking for something to work on? Look for the label [Accepting Merge Requests](#i-want-to-contribute).
GitLab comes into two flavors, GitLab Community Edition (CE) our free and open GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
source edition, and GitLab Enterprise Edition (EE) which is our commercial source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for edition. Throughout this guide you will see references to CE and EE for
...@@ -112,8 +114,8 @@ scheduling into milestones. Labelling is a task for everyone. ...@@ -112,8 +114,8 @@ scheduling into milestones. Labelling is a task for everyone.
Most issues will have labels for at least one of the following: Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc. - Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~CI, ~Discussion, ~Edge, ~Frontend, ~Platform, etc. - Team: ~CI, ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch - Priority: ~Deliverable, ~Stretch
All labels, their meaning and priority are defined on the All labels, their meaning and priority are defined on the
...@@ -276,7 +278,7 @@ For feature proposals for EE, open an issue on the ...@@ -276,7 +278,7 @@ For feature proposals for EE, open an issue on the
In order to help track the feature proposals, we have created a In order to help track the feature proposals, we have created a
[`feature proposal`][fpl] label. For the time being, users that are not members [`feature proposal`][fpl] label. For the time being, users that are not members
of the project cannot add labels. You can instead ask one of the [core team] of the project cannot add labels. You can instead ask one of the [core team]
members to add the label `feature proposal` to the issue or add the following members to add the label ~"feature proposal" to the issue or add the following
code snippet right after your description in a new line: `~"feature proposal"`. code snippet right after your description in a new line: `~"feature proposal"`.
Please keep feature proposals as small and simple as possible, complex ones Please keep feature proposals as small and simple as possible, complex ones
...@@ -342,27 +344,29 @@ addressed. ...@@ -342,27 +344,29 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127 [8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue [update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
### Technical debt ### Technical and UX debt
In order to track things that can be improved in GitLab's codebase, we created In order to track things that can be improved in GitLab's codebase,
the ~"technical debt" label in [GitLab's issue tracker][ce-tracker]. we use the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
For user experience improvements, we use the ~"UX debt" label.
This label should be added to issues that describe things that can be improved, These labels should be added to issues that describe things that can be improved,
shortcuts that have been taken, code that needs refactoring, features that need shortcuts that have been taken, features that need additional attention, and all
additional attention, and all other things that have been left behind due to other things that have been left behind due to high velocity of development.
high velocity of development. For example, code that needs refactoring should use the ~"technical debt" label,
user experience refinements should use the ~"UX debt" label.
Everyone can create an issue, though you may need to ask for adding a specific Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels label, if you do not have permissions to do it by yourself. Additional labels
can be combined with the `technical debt` label, to make it easier to schedule can be combined with these labels, to make it easier to schedule
the improvements for a release. the improvements for a release.
Issues tagged with the `technical debt` label have the same priority like issues Issues tagged with these labels have the same priority like issues
that describe a new feature to be introduced in GitLab, and should be scheduled that describe a new feature to be introduced in GitLab, and should be scheduled
for a release by the appropriate person. for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is Make sure to mention the merge request that the ~"technical debt" issue or
associated with in the description of the issue. ~"UX debt" issue is associated with in the description of the issue.
### Stewardship ### Stewardship
......
...@@ -12,12 +12,13 @@ gem 'sprockets', '~> 3.7.0' ...@@ -12,12 +12,13 @@ gem 'sprockets', '~> 3.7.0'
gem 'default_value_for', '~> 3.0.0' gem 'default_value_for', '~> 3.0.0'
# Supported DBs # Supported DBs
gem 'mysql2', '~> 0.3.16', group: :mysql gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1' gem 'rugged', '~> 0.26.0'
gem 'grape-route-helpers', '~> 2.0.0'
gem 'faraday', '~> 0.11.0' gem 'faraday', '~> 0.12'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
...@@ -37,7 +38,7 @@ gem 'omniauth-saml', '~> 1.7.0' ...@@ -37,7 +38,7 @@ gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.0' gem 'omniauth-authentiq', '~> 0.3.1'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6' gem 'jwt', '~> 1.5.6'
...@@ -57,10 +58,14 @@ gem 'validates_hostname', '~> 1.0.6' ...@@ -57,10 +58,14 @@ gem 'validates_hostname', '~> 1.0.6'
# Browser detection # Browser detection
gem 'browser', '~> 2.2' gem 'browser', '~> 2.2'
# GPG
gem 'gpgme'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap' gem 'gitlab_omniauth-ldap', '~> 2.0.3', require: 'omniauth-ldap'
gem 'net-ldap'
# Git Wiki # Git Wiki
# Required manually in config/initializers/gollum.rb to control load order # Required manually in config/initializers/gollum.rb to control load order
...@@ -71,7 +76,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.4', require: false ...@@ -71,7 +76,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API # API
gem 'grape', '~> 0.19.0' gem 'grape', '~> 0.19.2'
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'
...@@ -85,18 +90,19 @@ gem 'kaminari', '~> 0.17.0' ...@@ -85,18 +90,19 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
# Files attachments # Files attachments
gem 'carrierwave', '~> 1.0' gem 'carrierwave', '~> 1.1'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 1.4'
gem 'fog-core', '~> 1.44' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5' gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.8.6' gem 'google-api-client', '~> 0.8.6'
...@@ -109,7 +115,7 @@ gem 'seed-fu', '~> 2.3.5' ...@@ -109,7 +115,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.5.1' gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
...@@ -121,6 +127,7 @@ gem 'asciidoctor', '~> 1.5.2' ...@@ -121,6 +127,7 @@ gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7' gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8' gem 'truncato', '~> 0.7.8'
gem 'bootstrap_form', '~> 2.7.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -156,11 +163,14 @@ gem 'rufus-scheduler', '~> 3.4' ...@@ -156,11 +163,14 @@ gem 'rufus-scheduler', '~> 3.4'
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.13.3'
# Colored output to console # Colored output to console
gem 'rainbow', '~> 2.1.0' gem 'rainbow', '~> 2.2'
# GitLab settings # GitLab settings
gem 'settingslogic', '~> 2.0.9' gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions
gem 're2', '~> 1.1.1'
# Misc # Misc
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
...@@ -235,7 +245,6 @@ gem 'webpack-rails', '~> 0.9.10' ...@@ -235,7 +245,6 @@ 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'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
...@@ -248,25 +257,40 @@ gem 'jquery-rails', '~> 4.1.0' ...@@ -248,25 +257,40 @@ gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3' gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9' gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1' gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0' gem 'base32', '~> 0.3.0'
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 2.4.0' gem 'sentry-raven', '~> 2.5.3'
gem 'premailer-rails', '~> 1.9.0' gem 'premailer-rails', '~> 1.9.7'
# I18n # I18n
gem 'ruby_parser', '~> 3.8.4', require: false gem 'ruby_parser', '~> 3.8', require: false
gem 'rails-i18n', '~> 4.0.9'
gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development gem 'gettext', '~> 3.2.2', require: false, group: :development
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
gem 'peek-sidekiq', '~> 1.0.3'
# Metrics # Metrics
group :metrics do group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta11'
gem 'raindrops', '~> 0.18'
end end
group :development do group :development do
...@@ -315,9 +339,9 @@ group :development, :test do ...@@ -315,9 +339,9 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.47.1', require: false gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.0', require: false gem 'rubocop-rspec', '~> 1.15.1', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.54.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
gem 'flay', '~> 2.8.0', require: false gem 'flay', '~> 2.8.0', require: false
...@@ -334,10 +358,10 @@ group :development, :test do ...@@ -334,10 +358,10 @@ group :development, :test do
end end
group :test do group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2' gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.24.0' gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
...@@ -354,10 +378,10 @@ gem 'html2text' ...@@ -354,10 +378,10 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.16.2'
# OAuth # OAuth
gem 'oauth2', '~> 1.3.0' gem 'oauth2', '~> 1.4'
# Soft deletion # Soft deletion
gem 'paranoia', '~> 2.2' gem 'paranoia', '~> 2.3.1'
# Health check # Health check
gem 'health_check', '~> 2.6.0' gem 'health_check', '~> 2.6.0'
...@@ -367,6 +391,13 @@ gem 'vmstat', '~> 2.3.0' ...@@ -367,6 +391,13 @@ 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.7.0' gem 'gitaly', '~> 0.21.0'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
# Feature toggles
gem 'flipper', '~> 0.10.2'
gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
This diff is collapsed.
...@@ -128,7 +128,7 @@ information, see ...@@ -128,7 +128,7 @@ information, see
### After the 7th ### 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](#regressions)
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 shipped in the next RC for that release if it is before the 22nd. These fixes will be shipped in the next RC for that release if it is before the 22nd.
...@@ -158,6 +158,24 @@ release should have the correct milestone assigned _and_ have the label ...@@ -158,6 +158,24 @@ release should have the correct milestone assigned _and_ have the label
Merge requests without a milestone and this label will Merge requests without a milestone and this label will
not be merged into any stable branches. not be merged into any stable branches.
### Regressions
A regression for a particular monthly release is a bug that exists in that
release, but wasn't present in the release before. This includes bugs in
features that were only added in that monthly release. Every regression **must**
have the milestone of the release it was introduced in - if a regression doesn't
have a milestone, it might be 'just' a bug!
For instance, if 10.5.0 adds a feature, and that feature doesn't work correctly,
then this is a regression in 10.5. If 10.5.1 then fixes that, but 10.5.3 somehow
reintroduces the bug, then this bug is still a regression in 10.5.
Because GitLab.com runs release candidates of new releases, a regression can be
reported in a release before its 'official' release date on the 22nd of the
month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
## Release retrospective and kickoff ## Release retrospective and kickoff
### Retrospective ### Retrospective
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
## Test coverage ## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby - [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
- [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=rake+karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript - [![JavaScript coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=karma)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-javascript) JavaScript
## Canonical source ## Canonical source
......
9.3.0-pre 9.5.0-pre
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 120" enable-background="new 0 0 12 120">
<path d="m12 6c0-3.309-2.691-6-6-6s-6 2.691-6 6c0 2.967 2.167 5.431 5 5.91v108.09h2v-108.09c2.833-.479 5-2.943 5-5.91m-6 4c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/>
</svg>
...@@ -5,7 +5,8 @@ import Cookies from 'js-cookie'; ...@@ -5,7 +5,8 @@ import Cookies from 'js-cookie';
class Activities { class Activities {
constructor() { constructor() {
Pager.init(20, true, false, this.updateTooltips); Pager.init(20, true, false, data => data, this.updateTooltips);
$('.event-filter-link').on('click', (e) => { $('.event-filter-link').on('click', (e) => {
e.preventDefault(); e.preventDefault();
this.toggleFilter(e.currentTarget); this.toggleFilter(e.currentTarget);
...@@ -19,7 +20,7 @@ class Activities { ...@@ -19,7 +20,7 @@ class Activities {
reloadActivities() { reloadActivities() {
$('.content_list').html(''); $('.content_list').html('');
Pager.init(20, true, false, this.updateTooltips); Pager.init(20, true, false, data => data, this.updateTooltips);
} }
toggleFilter(sender) { toggleFilter(sender) {
......
...@@ -77,7 +77,7 @@ const Api = { ...@@ -77,7 +77,7 @@ const Api = {
dataType: 'json', dataType: 'json',
}) })
.done(label => callback(label)) .done(label => callback(label))
.error(message => callback(message.responseJSON)); .fail(message => callback(message.responseJSON));
}, },
// Return group projects list. Filtered by query // Return group projects list. Filtered by query
...@@ -134,7 +134,7 @@ const Api = { ...@@ -134,7 +134,7 @@ const Api = {
dataType: 'json', dataType: 'json',
}) })
.done(file => callback(null, file)) .done(file => callback(null, file))
.error(callback); .fail(callback);
}, },
users(query, options) { users(query, options) {
......
This diff is collapsed.
import autosize from 'vendor/autosize'; import autosize from 'vendor/autosize';
$(() => { document.addEventListener('DOMContentLoaded', () => {
const $fields = $('.js-autosize'); const autosizeEls = document.querySelectorAll('.js-autosize');
$fields.on('autosize:resized', function resized() { autosize(autosizeEls);
const $field = $(this); autosize.update(autosizeEls);
$field.data('height', $field.outerHeight());
});
$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');
}); });
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json'; import isEmojiUnicodeSupported from '../emoji/support';
import emojiAliases from 'emojis/aliases.json';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
installCustomElements(window); installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap(); export default function installGlEmojiElement() {
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
function assembleFallbackImageSrc(inputName) {
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
...@@ -88,20 +22,29 @@ function installGlEmojiElement() { ...@@ -88,20 +22,29 @@ function installGlEmojiElement() {
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if ( if (
emojiUnicode &&
isEmojiUnicode && isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) { ) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) { } else {
import(/* webpackChunkName: 'emoji' */ '../emoji')
.then(({ emojiImageTag, emojiFallbackImageSrc }) => {
if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc); this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = assembleFallbackImageSrc(name); const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src); this.innerHTML = emojiImageTag(name, src);
} }
})
.catch(() => {
// do nothing
});
}
} }
}; };
...@@ -109,9 +52,3 @@ function installGlEmojiElement() { ...@@ -109,9 +52,3 @@ function installGlEmojiElement() {
prototype: GlEmojiElementProto, prototype: GlEmojiElementProto,
}); });
} }
export {
installGlEmojiElement,
glEmojiTag,
emojiImageTag,
};
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
function isEmojiNameValid(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
return name && emojiMap[name];
}
export default isEmojiNameValid;
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
function isFlagEmoji(emojiUnicode) {
const cp = emojiUnicode.codePointAt(0);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
function isKeycapEmoji(emojiUnicode) {
return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
}
// Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
// Check for `family_*`, `kiss_*`, `couple_*`
// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
const zwj = 8205; // parseInt('200D', 16)
const personStartCodePoint = 128102; // parseInt('1F466', 16)
const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
} else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
hasPersonEmoji = true;
}
});
return hasPersonEmoji && hasZwj;
}
// Helper so we don't have to run `isFlagEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult
);
}
// Helper so we don't have to run `isSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
!isHorseRacingSkinToneResult
);
}
// Helper so we don't have to run `isPersonZwjEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return (
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
}
// Takes in a support map and determines whether
// the given unicode emoji is supported on the platform.
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
}
export {
isEmojiUnicodeSupported,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
};
import AccessorUtilities from '../../lib/utils/accessor';
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
// woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
// family_mwgb
// Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
// horse_racing_tone5
// Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
'\u{1F575}\u{1F3FF}',
// person_with_ball_tone5
'\u{26F9}\u{1F3FF}',
// angel_tone5
'\u{1F47C}\u{1F3FF}',
],
// rofl, http://emojipedia.org/unicode-9.0/
'9.0': '\u{1F923}',
// metal, http://emojipedia.org/unicode-8.0/
'8.0': '\u{1F918}',
// spy, http://emojipedia.org/unicode-7.0/
'7.0': '\u{1F575}',
// expressionless, http://emojipedia.org/unicode-6.1/
6.1: '\u{1F611}',
// japanese_goblin, http://emojipedia.org/unicode-6.0/
'6.0': '\u{1F47A}',
// sailboat, http://emojipedia.org/unicode-5.2/
5.2: '\u{26F5}',
// mahjong, http://emojipedia.org/unicode-5.1/
5.1: '\u{1F004}',
// gear, http://emojipedia.org/unicode-4.1/
4.1: '\u{2699}',
// zap, http://emojipedia.org/unicode-4.0/
'4.0': '\u{26A1}',
// recycle, http://emojipedia.org/unicode-3.2/
3.2: '\u{267B}',
// information_source, http://emojipedia.org/unicode-3.0/
'3.0': '\u{2139}',
// heart, http://emojipedia.org/unicode-1.1/
1.1: '\u{2764}',
};
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
// Check for some sort of color other than black
if (hasColor && isVisible) {
return true;
}
return false;
}
const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
const isChrome = chromeMatches && chromeMatches.length > 0;
const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize);
canvas.height = (numTestEntries * fontSize);
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
writeIndex += 1;
});
});
// Read from the canvas
const resultMap = {};
let readIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
// Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData(
0,
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
}
}
readIndex += 1;
return isSatisfied && isValidEmoji;
}, true);
resultMap[testKey] = isTestSatisfied;
});
resultMap.meta = {
isChrome,
chromeVersion,
};
return resultMap;
}
function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
}
return unicodeSupportMap;
}
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import './details_behavior'; import './details_behavior';
import { installGlEmojiElement } from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
import './requires_input'; import './requires_input';
import './toggler_behavior'; import './toggler_behavior';
......
...@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { ...@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
e.preventDefault(); e.preventDefault();
const $form = $(e.target).closest('form'); const $form = $(e.target).closest('form');
const $submitButton = $form.find('input[type=submit], button[type=submit]'); const $submitButton = $form.find('input[type=submit], button[type=submit]').first();
if (!$submitButton.attr('disabled')) { if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]); $submitButton.trigger('click', [e]);
......
...@@ -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('branch_name', form.find('input[name="branch_name"]').val()); formData.append('branch_name', form.find('.js-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());
}); });
......
class CreateBranchDropdown {
constructor(el, targetBranchDropdown) {
this.targetBranchDropdown = targetBranchDropdown;
this.el = el;
this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back');
this.cancelButton = this.el.querySelector('.js-cancel-branch-btn');
this.newBranchField = this.el.querySelector('#new_branch_name');
this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn');
this.newBranchCreateButton.setAttribute('disabled', '');
this.addBindings();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
}
cleanup() {
this.cleanBindings();
document.removeEventListener('beforeunload', this.cleanupWrapper);
}
cleanBindings() {
this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.removeEventListener('click', this.resetFormWrapper);
this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper);
}
addBindings() {
this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this);
this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this);
this.resetFormWrapper = this.resetForm.bind(this);
this.handleCancelClickWrapper = this.handleCancelClick.bind(this);
this.createBranchWrapper = this.createBranch.bind(this);
this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper);
this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper);
this.dropdownBack.addEventListener('click', this.resetFormWrapper);
this.cancelButton.addEventListener('click', this.handleCancelClickWrapper);
this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper);
}
handleCancelClick(e) {
e.preventDefault();
e.stopPropagation();
this.resetForm();
this.dropdownBack.click();
}
handleNewBranchKeydown(e) {
const keyCode = e.which;
const ENTER_KEYCODE = 13;
if (keyCode === ENTER_KEYCODE) {
this.createBranch(e);
}
}
enableBranchCreateButton() {
if (this.newBranchField.value !== '') {
this.newBranchCreateButton.removeAttribute('disabled');
} else {
this.newBranchCreateButton.setAttribute('disabled', '');
}
}
resetForm() {
this.newBranchField.value = '';
this.enableBranchCreateButtonWrapper();
}
createBranch(e) {
e.preventDefault();
if (this.newBranchCreateButton.getAttribute('disabled') === '') {
return;
}
const newBranchName = this.newBranchField.value;
this.targetBranchDropdown.setNewBranch(newBranchName);
this.resetForm();
}
}
window.gl = window.gl || {};
gl.CreateBranchDropdown = CreateBranchDropdown;
...@@ -51,8 +51,9 @@ export default () => { ...@@ -51,8 +51,9 @@ export default () => {
methods: { methods: {
loadFile() { loadFile() {
this.$http.get(el.dataset.endpoint) this.$http.get(el.dataset.endpoint)
.then(response => response.json())
.then((res) => { .then((res) => {
this.json = res.json(); this.json = res;
this.loading = false; this.loading = false;
}) })
.catch((e) => { .catch((e) => {
......
/* eslint-disable class-methods-use-this */
const SELECT_ITEM_MSG = 'Select';
class TargetBranchDropDown {
constructor(dropdown) {
this.dropdown = dropdown;
this.$dropdown = $(dropdown);
this.fieldName = this.dropdown.getAttribute('data-field-name');
this.form = this.dropdown.closest('form');
this.createDropdown();
}
static bootstrap() {
const dropdowns = document.querySelectorAll('.js-project-branches-dropdown');
[].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown));
}
createDropdown() {
const self = this;
this.$dropdown.glDropdown({
selectable: true,
filterable: true,
search: {
fields: ['title'],
},
data: (term, callback) => $.ajax({
url: self.dropdown.getAttribute('data-refs-url'),
data: {
ref: self.dropdown.getAttribute('data-ref'),
show_all: true,
},
dataType: 'json',
}).done(refs => callback(self.dropdownData(refs))),
toggleLabel(item, el) {
if (el.is('.is-active')) {
return item.text;
}
return SELECT_ITEM_MSG;
},
clicked(options) {
options.e.preventDefault();
self.onClick.call(self);
},
fieldName: self.fieldName,
});
return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this);
}
onClick() {
this.enableSubmit();
this.$dropdown.trigger('change.branch');
}
enableSubmit() {
const submitBtn = this.form.querySelector('[type="submit"]');
if (this.branchInput && this.branchInput.value) {
submitBtn.removeAttribute('disabled');
} else {
submitBtn.setAttribute('disabled', '');
}
}
dropdownData(refs) {
const branchList = this.dropdownItems(refs);
this.cachedRefs = refs;
this.addDefaultBranch(branchList);
this.addNewBranch(branchList);
return { Branches: branchList };
}
dropdownItems(refs) {
return refs.map(this.dropdownItem);
}
dropdownItem(ref) {
return { id: ref, text: ref, title: ref };
}
addDefaultBranch(branchList) {
// when no branch is selected do nothing
if (!this.branchInput) {
return;
}
const branchInputVal = this.branchInput.value;
const currentBranchIndex = this.searchBranch(branchList, branchInputVal);
if (currentBranchIndex === -1) {
this.unshiftBranch(branchList, this.dropdownItem(branchInputVal));
}
}
addNewBranch(branchList) {
if (this.newBranch) {
this.unshiftBranch(branchList, this.newBranch);
}
}
searchBranch(branchList, branchName) {
return _.findIndex(branchList, el => branchName === el.id);
}
unshiftBranch(branchList, branch) {
const branchIndex = this.searchBranch(branchList, branch.id);
if (branchIndex === -1) {
branchList.unshift(branch);
}
}
setNewBranch(newBranchName) {
this.newBranch = this.dropdownItem(newBranchName);
this.refreshData();
this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName));
}
refreshData() {
this.glDropdown.fullData = this.dropdownData(this.cachedRefs);
this.clearFilter();
}
clearFilter() {
// apply an empty filter in order to refresh the data
this.glDropdown.filter.filter('');
this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = '';
}
selectBranch(index) {
const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index];
if (!branch.classList.contains('is-active')) {
branch.click();
} else {
this.closeDropdown();
}
}
closeDropdown() {
this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click();
}
get branchInput() {
return this.form.querySelector(`input[name="${this.fieldName}"]`);
}
get glDropdown() {
return this.$dropdown.data('glDropdown');
}
}
window.gl = window.gl || {};
gl.TargetBranchDropDown = TargetBranchDropDown;
...@@ -111,7 +111,7 @@ export default class BlobViewer { ...@@ -111,7 +111,7 @@ export default class BlobViewer {
BlobViewer.loadViewer(newViewer) BlobViewer.loadViewer(newViewer)
.then((viewer) => { .then((viewer) => {
$(viewer).syntaxHighlight(); $(viewer).renderGFM();
this.$fileHolder.trigger('highlight:line'); this.$fileHolder.trigger('highlight:line');
gl.utils.handleLocationHash(); gl.utils.handleLocationHash();
......
...@@ -8,6 +8,7 @@ import BlobFileDropzone from '../blob/blob_file_dropzone'; ...@@ -8,6 +8,7 @@ import BlobFileDropzone from '../blob/blob_file_dropzone';
$(() => { $(() => {
const editBlobForm = $('.js-edit-blob-form'); const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form'); const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relative-url-root'); const urlRoot = editBlobForm.data('relative-url-root');
...@@ -30,4 +31,8 @@ $(() => { ...@@ -30,4 +31,8 @@ $(() => {
'.btn-upload-file', '.btn-upload-file',
); );
} }
if (deleteBlobForm.length) {
new NewCommitForm(deleteBlobForm);
}
}); });
...@@ -70,6 +70,7 @@ $(() => { ...@@ -70,6 +70,7 @@ $(() => {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
this.filterManager = new FilteredSearchBoards(Store.filter, true); this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
// Listen for updateTokens event // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens); eventHub.$on('updateTokens', this.updateTokens);
...@@ -80,13 +81,16 @@ $(() => { ...@@ -80,13 +81,16 @@ $(() => {
mounted () { mounted () {
Store.disabled = this.disabled; Store.disabled = this.disabled;
gl.boardService.all() gl.boardService.all()
.then(response => response.json())
.then((resp) => { .then((resp) => {
resp.json().forEach((board) => { resp.forEach((board) => {
const list = Store.addList(board, this.defaultAvatar); const list = Store.addList(board, this.defaultAvatar);
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' }; list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
} }
}); });
...@@ -94,7 +98,8 @@ $(() => { ...@@ -94,7 +98,8 @@ $(() => {
Store.addBlankState(); Store.addBlankState();
this.loading = false; this.loading = false;
}).catch(() => new Flash('An error occurred. Please try again.')); })
.catch(() => new Flash('An error occurred. Please try again.'));
}, },
methods: { methods: {
updateTokens() { updateTokens() {
...@@ -127,7 +132,7 @@ $(() => { ...@@ -127,7 +132,7 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; return !this.store.lists.filter(list => !list.preset).length;
}, },
tooltipTitle() { tooltipTitle() {
if (this.disabled) { if (this.disabled) {
......
/* 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 AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list'; import boardList from './board_list';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
import './board_delete'; import './board_delete';
...@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({
disabled: Boolean, disabled: Boolean,
issueLinkBase: String, issueLinkBase: String,
rootPath: String, rootPath: String,
boardId: {
type: String,
required: true,
},
}, },
data () { data () {
return { return {
...@@ -78,8 +83,17 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -78,8 +83,17 @@ gl.issueBoards.Board = Vue.extend({
methods: { methods: {
showNewIssueForm() { showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
} }
}, },
},
mounted () { mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
...@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
}, },
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
},
}); });
...@@ -64,8 +64,9 @@ export default { ...@@ -64,8 +64,9 @@ export default {
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService.generateDefaultLists()
.then((resp) => { .then(resp => resp.json())
resp.json().forEach((listObj) => { .then((data) => {
data.forEach((listObj) => {
const list = Store.findList('title', listObj.title); const list = Store.findList('title', listObj.title);
list.id = listObj.id; list.id = listObj.id;
......
...@@ -57,6 +57,9 @@ export default { ...@@ -57,6 +57,9 @@ export default {
scrollTop() { scrollTop() {
return this.$refs.list.scrollTop + this.listHeight(); return this.$refs.list.scrollTop + this.listHeight();
}, },
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() { loadNextPage() {
const getIssues = this.list.nextPage(); const getIssues = this.list.nextPage();
const loadingDone = () => { const loadingDone = () => {
...@@ -108,6 +111,7 @@ export default { ...@@ -108,6 +111,7 @@ export default {
}, },
created() { created() {
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
}, },
mounted() { mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
...@@ -150,6 +154,7 @@ export default { ...@@ -150,6 +154,7 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
this.$refs.list.removeEventListener('scroll', this.onScroll); this.$refs.list.removeEventListener('scroll', this.onScroll);
}, },
template: ` template: `
...@@ -160,9 +165,11 @@ export default { ...@@ -160,9 +165,11 @@ export default {
v-if="loading"> v-if="loading">
<loading-icon /> <loading-icon />
</div> </div>
<transition name="slide-down">
<board-new-issue <board-new-issue
:list="list" :list="list"
v-if="list.type !== 'closed' && showIssueForm"/> v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<ul <ul
class="board-list" class="board-list"
v-show="!loading" v-show="!loading"
......
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
methods: { methods: {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
if (this.title.trim() === '') return; if (this.title.trim() === '') return Promise.resolve();
this.error = false; this.error = false;
...@@ -29,7 +29,10 @@ export default { ...@@ -29,7 +29,10 @@ export default {
assignees: [], assignees: [],
}); });
this.list.newIssue(issue) eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
return this.list.newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
...@@ -47,8 +50,6 @@ export default { ...@@ -47,8 +50,6 @@ export default {
// Show error message // Show error message
this.error = true; this.error = true;
}); });
this.cancel();
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
...@@ -75,6 +76,7 @@ export default { ...@@ -75,6 +76,7 @@ export default {
type="text" type="text"
v-model="title" v-model="title"
ref="input" ref="input"
autocomplete="off"
:id="list.id + '-title'" /> :id="list.id + '-title'" />
<div class="clearfix prepend-top-10"> <div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left" <button class="btn btn-success pull-left"
......
...@@ -32,12 +32,12 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -32,12 +32,12 @@ gl.issueBoards.BoardSidebar = Vue.extend({
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
}, },
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
},
milestoneTitle() { milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
} },
canRemove() {
return !this.list.preset;
},
}, },
watch: { watch: {
detail: { detail: {
......
...@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
<div class="card-assignee"> <div class="card-assignee">
<user-avatar-link <user-avatar-link
v-for="(assignee, index) in issue.assignees" v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
v-if="shouldRenderAssignee(index)" v-if="shouldRenderAssignee(index)"
class="js-no-trigger" class="js-no-trigger"
:link-href="assigneeUrl(assignee)" :link-href="assigneeUrl(assignee)"
......
...@@ -13,6 +13,7 @@ export default { ...@@ -13,6 +13,7 @@ export default {
FilteredSearchContainer.container = this.$el; FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store); this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens(); this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder(); this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton(); this.filteredSearch.toggleClearSearchButton();
......
...@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({ ...@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({
}, },
methods: { methods: {
addIssues() { addIssues() {
const list = this.modal.selectedList || this.state.lists[0]; const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues(); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.globalId); const issueIds = selectedIssues.map(issue => issue.globalId);
......
...@@ -88,9 +88,9 @@ gl.issueBoards.IssuesModal = Vue.extend({ ...@@ -88,9 +88,9 @@ gl.issueBoards.IssuesModal = Vue.extend({
return gl.boardService.getBacklog(queryData(this.filter.path, { return gl.boardService.getBacklog(queryData(this.filter.path, {
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
})).then((res) => { }))
const data = res.json(); .then(resp => resp.json())
.then((data) => {
if (clearIssues) { if (clearIssues) {
this.issues = []; this.issues = [];
} }
......
...@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ ...@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
}, },
computed: { computed: {
selected() { selected() {
return this.modal.selectedList || this.state.lists[0]; return this.modal.selectedList || this.state.lists[1];
}, },
}, },
destroyed() { destroyed() {
......
...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
template: ` template: `
<div <div
class="block list" class="block list">
v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchContainer from '../filtered_search/container';
export default class FilteredSearchBoards extends gl.FilteredSearchManager { export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) { constructor(store, updateUrl = false, cantEdit = []) {
super('boards'); super('boards');
this.store = store; this.store = store;
...@@ -11,6 +11,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -11,6 +11,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
this.cantEdit = cantEdit;
} }
updateObject(path) { updateObject(path) {
...@@ -40,4 +41,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -40,4 +41,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Get the placeholder back if search is empty // Get the placeholder back if search is empty
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
}
} }
...@@ -12,7 +12,9 @@ class List { ...@@ -12,7 +12,9 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; this.type = obj.list_type;
this.preset = ['closed', 'blank'].indexOf(this.type) > -1; this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
this.isExpanded = true;
this.page = 1; this.page = 1;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
...@@ -38,9 +40,8 @@ class List { ...@@ -38,9 +40,8 @@ class List {
save () { save () {
return gl.boardService.createList(this.label.id) return gl.boardService.createList(this.label.id)
.then((resp) => { .then(resp => resp.json())
const data = resp.json(); .then((data) => {
this.id = data.id; this.id = data.id;
this.type = data.list_type; this.type = data.list_type;
this.position = data.position; this.position = data.position;
...@@ -89,8 +90,8 @@ class List { ...@@ -89,8 +90,8 @@ class List {
} }
return gl.boardService.getIssuesForList(this.id, data) return gl.boardService.getIssuesForList(this.id, data)
.then((resp) => { .then(resp => resp.json())
const data = resp.json(); .then((data) => {
this.loading = false; this.loading = false;
this.issuesSize = data.size; this.issuesSize = data.size;
...@@ -103,13 +104,18 @@ class List { ...@@ -103,13 +104,18 @@ class List {
} }
newIssue (issue) { newIssue (issue) {
this.addIssue(issue); this.addIssue(issue, null, 0);
this.issuesSize += 1; this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue) return gl.boardService.newIssue(this.id, issue)
.then((resp) => { .then(resp => resp.json())
const data = resp.json(); .then((data) => {
issue.id = data.iid; issue.id = data.iid;
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
}
}); });
} }
......
...@@ -23,11 +23,6 @@ class BoardService { ...@@ -23,11 +23,6 @@ class BoardService {
url: bulkUpdatePath, url: bulkUpdatePath,
}, },
}); });
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
next();
});
} }
all () { all () {
......
...@@ -22,6 +22,7 @@ gl.issueBoards.BoardsStore = { ...@@ -22,6 +22,7 @@ gl.issueBoards.BoardsStore = {
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&'); this.filter.path = gl.utils.getUrlParamsArray().join('&');
this.detail = { issue: {} };
}, },
addList (listObj, defaultAvatar) { addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
...@@ -31,10 +32,14 @@ gl.issueBoards.BoardsStore = { ...@@ -31,10 +32,14 @@ gl.issueBoards.BoardsStore = {
}, },
new (listObj) { new (listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list list
.save() .save()
.then(() => { .then(() => {
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
}) })
.catch(() => { .catch(() => {
...@@ -47,7 +52,7 @@ gl.issueBoards.BoardsStore = { ...@@ -47,7 +52,7 @@ gl.issueBoards.BoardsStore = {
}, },
shouldAddBlankState () { shouldAddBlankState () {
// Decide whether to add the blank state // Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'closed')[0]); return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);
}, },
addBlankState () { addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
...@@ -100,7 +105,7 @@ gl.issueBoards.BoardsStore = { ...@@ -100,7 +105,7 @@ gl.issueBoards.BoardsStore = {
issueTo.removeLabel(listFrom.label); issueTo.removeLabel(listFrom.label);
} }
if (listTo.type === 'closed') { if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => { issueLists.forEach((list) => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
......
This diff is collapsed.
This diff is collapsed.
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import InputSetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
class CommentTypeToggle { class CommentTypeToggle {
constructor(opts = {}) { constructor(opts = {}) {
......
This diff is collapsed.
This diff is collapsed.
...@@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; ...@@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
// custom jQuery functions // custom jQuery functions
$.fn.extend({ $.fn.extend({
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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