Commit 397a0a1d authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'origin/master' into dm-tree-json

parents 2ae9c890 8cc9ab50

Too many changes to show.

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

---
engines:
brakeman:
enabled: true
bundler-audit:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
exclude_paths:
- "lib/api/v3/*"
eslint:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- Gemfile.lock
- "**.erb"
- "**.haml"
- "**.rb"
- "**.rhtml"
- "**.slim"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
exclude_paths:
- config/
- db/
- features/
- node_modules/
- spec/
- vendor/
- .yarn-cache/
- tmp/
- builds/
- coverage/
- public/
- shared/
- webpack-report/
- log/
- backups/
- coverage-javascript/
......@@ -430,6 +430,7 @@ gitlab:assets:compile:
USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
script:
- yarn install --pure-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
......@@ -440,21 +441,41 @@ gitlab:assets:compile:
- webpack-report/
karma:
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6"
stage: test
<<: *use-pg
<<: *dedicated-runner
<<: *except-docs
variables:
BABEL_ENV: "coverage"
CHROME_LOG_FILE: "chrome_debug.log"
script:
- bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
name: coverage-javascript
expire_in: 31d
when: always
paths:
- chrome_debug.log
- coverage-javascript/
codeclimate:
<<: *except-docs
before_script: []
image: docker:latest
stage: test
variables:
SETUP_DB: "false"
DOCKER_DRIVER: overlay
services:
- docker:dind
script:
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
artifacts:
paths: [codeclimate.json]
coverage:
stage: post-test
services: []
......
Please read this!
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=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.
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)
### 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 actually happens)
......
......@@ -3,8 +3,14 @@ Please read this!
Before opening a new issue, make sure to search for keywords in the issues
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
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.
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
(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.
2. What is the solution.
3. How does someone use this
Make sure these are completed before closing the issue,
with a link to the relevant commit.
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
......@@ -164,6 +164,11 @@ Style/DefWithParentheses:
Style/Documentation:
Enabled: false
# Multi-line method chaining should be done with leading dots.
Style/DotPosition:
Enabled: true
EnforcedStyle: leading
# This cop checks for uses of double negation (!!) to convert something
# to a boolean value. As this is both cryptic and usually redundant, it
# should be avoided.
......@@ -390,6 +395,15 @@ Style/OpMethod:
Style/ParenthesesAroundCondition:
Enabled: true
# This cop (by default) checks for uses of methods Hash#has_key? and
# Hash#has_value? where it enforces Hash#key? and Hash#value?
# It is configurable to enforce the inverse, using `verbose` method
# names also.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: false
# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
Enabled: true
......@@ -1055,6 +1069,13 @@ RSpec/NotToNot:
RSpec/RepeatedDescription:
Enabled: false
# Ensure RSpec hook blocks are always multi-line.
RSpec/SingleLineHook:
Enabled: true
Exclude:
- 'spec/factories/*'
- 'spec/requests/api/v3/*'
# Checks for stubbed test subjects.
RSpec/SubjectStub:
Enabled: false
......
......@@ -88,13 +88,6 @@ Security/YAMLLoad:
Style/BarePercentLiterals:
Enabled: false
# Offense count: 1403
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
Style/DotPosition:
Enabled: false
# Offense count: 5
# Cop supports --auto-correct.
Style/EachWithObject:
......@@ -236,13 +229,6 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
# Offense count: 45
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Enabled: false
# Offense count: 65
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
......
......@@ -11,11 +11,11 @@ linters:
# !global, !important, and !optional flags.
BangFormat:
enabled: false
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
ChainedClasses:
......@@ -25,13 +25,13 @@ linters:
# (e.g. `color: green` is a color keyword)
ColorKeyword:
enabled: false
# Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere
# else.
ColorVariable:
enabled: true
# Which form of comments to prefer in CSS.
Comment:
enabled: false
......@@ -39,7 +39,7 @@ linters:
# Reports @debug statements (which you probably left behind accidentally).
DebugStatement:
enabled: false
# Rule sets should be ordered as follows:
# - @extend declarations
# - @include declarations without inner @content
......@@ -54,19 +54,19 @@ linters:
# more information.
DisableLinterReason:
enabled: true
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
enabled: true
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: true
# Reports when you have an empty rule set.
EmptyRule:
enabled: true
# Reports when you have an @extend directive.
ExtendDirective:
enabled: false
......@@ -75,49 +75,49 @@ linters:
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
enabled: false
enabled: true
# HEX colors should use three-character values where possible.
HexLength:
enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
# Avoid using ID selectors.
IdSelector:
enabled: false
# The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension.
ImportPath:
enabled: false
# Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
# Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
......@@ -129,12 +129,12 @@ linters:
# Sort properties in a strict order.
PropertySortOrder:
enabled: false
# Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
# Configure which units are allowed for property values.
PropertyUnits:
enabled: false
......@@ -144,25 +144,25 @@ linters:
# be declared with one colon.
PseudoElement:
enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
# Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
......@@ -173,11 +173,11 @@ linters:
# individual selector occupy a single line.
SingleLinePerSelector:
enabled: true
# Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
......@@ -197,12 +197,12 @@ linters:
# colon.
SpaceAfterVariableName:
enabled: false
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
enabled: true
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: true
......@@ -210,7 +210,7 @@ linters:
# Parentheses should not be padded with spaces.
SpaceBetweenParens:
enabled: false
# Enforces that string literals should be written with a consistent form
# of quotes (single or double).
StringQuotes:
......@@ -241,7 +241,7 @@ linters:
# be unnecessary.
UnnecessaryParentReference:
enabled: false
# URLs should be valid and not contain protocols or domain names.
UrlFormat:
enabled: true
......
......@@ -2,6 +2,45 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.2.7 (2017-06-21)
- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm)
## 9.2.6 (2017-06-16)
- Fix the last coverage in trace log should be extracted. !11128 (dosuken123)
- Respect merge, instead of push, permissions for protected actions. !11648
- Fix pipeline_schedules pages throwing error 500. !11706 (dosuken123)
- Make backup task to continue on corrupt repositories. !11962
- Fix incorrect ETag cache key when relative instance URL is used. !11964
- Fix math rendering on blob pages.
- Invalidate cache for issue and MR counters more granularly.
- Fix terminals support for Kubernetes Service.
- Fix LFS timeouts when trying to save large files.
- Strip trailing whitespaces in submodule URLs.
- Make sure reCAPTCHA configuration is loaded when spam checks are initiated.
- Remove foreigh key on ci_trigger_schedules only if it exists.
## 9.2.5 (2017-06-07)
- No changes.
## 9.2.4 (2017-06-02)
- Fix visibility when referencing snippets.
## 9.2.3 (2017-05-31)
- Move uploads from 'public/uploads' to 'public/uploads/system'.
- Escapes html content before appending it to the DOM.
- Restrict API X-Frame-Options to same origin.
- Allow users autocomplete by author_id only for authenticated users.
## 9.2.2 (2017-05-25)
- Fix issue where real time pipelines were not cached. !11615
- Make all notes use equal padding.
## 9.2.1 (2017-05-23)
- Fix placement of note emoji on hover.
......@@ -207,6 +246,20 @@ entry.
- Fix preemptive scroll bar on user activity calendar.
- Pipeline chat notifications convert seconds to minutes and hours.
## 9.1.7 (2017-06-07)
- No changes.
## 9.1.6 (2017-06-02)
- Fix visibility when referencing snippets.
## 9.1.5 (2017-05-31)
- Move uploads from 'public/uploads' to 'public/uploads/system'.
- Restrict API X-Frame-Options to same origin.
- Allow users autocomplete by author_id only for authenticated users.
## 9.1.4 (2017-05-12)
- Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123)
......@@ -505,6 +558,20 @@ entry.
- Only send chat notifications for the default branch.
- Don't fill in the default kubernetes namespace.
## 9.0.10 (2017-06-07)
- No changes.
## 9.0.9 (2017-06-02)
- Fix visibility when referencing snippets.
## 9.0.8 (2017-05-31)
- Move uploads from 'public/uploads' to 'public/uploads/system'.
- Restrict API X-Frame-Options to same origin.
- Allow users autocomplete by author_id only for authenticated users.
## 9.0.7 (2017-05-05)
- Enforce project features when searching blobs and wikis.
......
......@@ -2,6 +2,7 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
......@@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
gem 'faraday', '~> 0.11.0'
gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
......@@ -85,7 +86,7 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 1.0'
gem 'carrierwave', '~> 1.1'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
......@@ -97,6 +98,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0'
# for Google storage
gem 'google-api-client', '~> 0.8.6'
......@@ -109,7 +111,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
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 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
......@@ -156,7 +158,7 @@ gem 'rufus-scheduler', '~> 3.4'
gem 'httparty', '~> 0.13.3'
# Colored output to console
gem 'rainbow', '~> 2.1.0'
gem 'rainbow', '~> 2.2'
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
......@@ -257,16 +259,31 @@ gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
# 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_js', '~> 1.2.0'
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.2.1'
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
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta5'
end
group :development do
......@@ -354,10 +371,10 @@ gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
# OAuth
gem 'oauth2', '~> 1.3.0'
gem 'oauth2', '~> 1.4'
# Soft deletion
gem 'paranoia', '~> 2.2'
gem 'paranoia', '~> 2.3.1'
# Health check
gem 'health_check', '~> 2.6.0'
......@@ -367,6 +384,10 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
gem 'gitaly', '~> 0.7.0'
gem 'gitaly', '~> 0.9.0'
gem 'toml-rb', '~> 0.3.15', require: false
# Feature toggles
gem 'flipper', '~> 0.10.2'
gem 'flipper-active_record', '~> 0.10.2'
......@@ -56,6 +56,7 @@ GEM
asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
ast (2.3.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
......@@ -82,6 +83,8 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
......@@ -105,7 +108,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (1.0.0)
carrierwave (1.1.0)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
......@@ -129,6 +132,8 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
......@@ -141,10 +146,8 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (1.0.6)
activesupport (~> 4.0)
deckar01-task_list (2.0.0)
html-pipeline
rack (~> 1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
......@@ -193,7 +196,7 @@ GEM
factory_girl_rails (4.7.0)
factory_girl (~> 4.7.0)
railties (>= 3.0.0)
faraday (0.11.0)
faraday (0.12.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday (>= 0.7.4, < 1.0)
......@@ -208,9 +211,18 @@ GEM
path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flipper (0.10.2)
flipper-active_record (0.10.2)
activerecord (>= 3.2, < 6)
flipper (~> 0.10.2)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
fog-aliyun (0.1.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
xml-simple (~> 1.1)
fog-aws (0.13.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
......@@ -265,7 +277,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly (0.7.0)
gitaly (0.9.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -341,7 +353,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.3.4)
grpc (1.2.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
......@@ -450,7 +462,9 @@ GEM
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
mmap2 (2.2.6)
mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
......@@ -466,8 +480,8 @@ GEM
mini_portile2 (~> 2.1.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.3.1)
faraday (>= 0.8, < 0.12)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
......@@ -499,11 +513,10 @@ GEM
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1)
addressable (~> 2.3)
jwt (~> 1.0)
jwt (~> 1.5.2)
multi_json (~> 1.3)
omniauth (>= 1.1.1)
omniauth-oauth2 (~> 1.3.1)
omniauth-oauth2 (>= 1.3.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
......@@ -533,11 +546,41 @@ GEM
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
paranoia (2.2.0)
activerecord (>= 4.0, < 5.1)
paranoia (2.3.1)
activerecord (>= 4.0, < 5.2)
parser (2.4.0.0)
ast (~> 2.2)
path_expander (1.0.1)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0)
railties (>= 4.0.0)
peek-gc (0.0.2)
peek
peek-host (1.0.0)
peek
peek-mysql2 (1.1.0)
atomic (>= 1.0.0)
mysql2
peek
peek-performance_bar (1.2.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
peek
pg
peek-rblineprof (0.2.0)
peek
rblineprof
peek-redis (1.2.0)
atomic (>= 1.0.0)
peek
redis
peek-sidekiq (1.0.3)
atomic (>= 1.0.0)
peek
sidekiq
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
......@@ -554,6 +597,8 @@ GEM
premailer-rails (1.9.2)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta5)
mmap2 (~> 2.2.6)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
......@@ -601,12 +646,16 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rails-i18n (4.0.9)
i18n (~> 0.7)
railties (~> 4.0)
railties (4.2.8)
actionpack (= 4.2.8)
activesupport (= 4.2.8)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
rainbow (2.2.2)
rake
raindrops (0.17.0)
rake (10.5.0)
rblineprof (0.3.6)
......@@ -646,7 +695,7 @@ GEM
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
rouge (2.1.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......@@ -694,7 +743,7 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.4)
ruby_parser (3.9.0)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
......@@ -727,7 +776,7 @@ GEM
sentry-raven (2.4.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.8.0)
sexp_processor (4.9.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
......@@ -879,6 +928,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
......@@ -886,7 +936,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.0)
carrierwave (~> 1.1)
charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
......@@ -896,7 +946,7 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 1.0.6)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
......@@ -907,9 +957,12 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
faraday (~> 0.11.0)
faraday (~> 0.12)
ffaker (~> 2.4)
flay (~> 2.8.0)
flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2)
fog-aliyun (~> 0.1.0)
fog-aws (~> 0.9)
fog-core (~> 1.44)
fog-google (~> 0.5)
......@@ -924,7 +977,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly (~> 0.7.0)
gitaly (~> 0.9.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
......@@ -963,7 +1016,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16)
net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.3.0)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
......@@ -982,10 +1035,20 @@ DEPENDENCIES
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
paranoia (~> 2.2)
paranoia (~> 2.3.1)
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.2.1)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
prometheus-client-mmap (~> 0.7.0.beta5)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......@@ -994,7 +1057,8 @@ DEPENDENCIES
rack-proxy (~> 0.6.0)
rails (= 4.2.8)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
rails-i18n (~> 4.0.9)
rainbow (~> 2.2)
rblineprof (~> 0.3.6)
rdoc (~> 4.2)
recaptcha (~> 3.0)
......@@ -1014,7 +1078,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.15.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8.4)
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
......@@ -1060,4 +1124,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.14.6
1.15.1
<?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';
class Activities {
constructor() {
Pager.init(20, true, false, this.updateTooltips);
Pager.init(20, true, false, data => data, this.updateTooltips);
$('.event-filter-link').on('click', (e) => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
......@@ -19,7 +20,7 @@ class Activities {
reloadActivities() {
$('.content_list').html('');
Pager.init(20, true, false, this.updateTooltips);
Pager.init(20, true, false, data => data, this.updateTooltips);
}
toggleFilter(sender) {
......
......@@ -77,7 +77,7 @@ const Api = {
dataType: 'json',
})
.done(label => callback(label))
.error(message => callback(message.responseJSON));
.fail(message => callback(message.responseJSON));
},
// Return group projects list. Filtered by query
......@@ -134,7 +134,7 @@ const Api = {
dataType: 'json',
})
.done(file => callback(null, file))
.error(callback);
.fail(callback);
},
users(query, options) {
......
......@@ -88,6 +88,7 @@ function installGlEmojiElement() {
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
) {
......
......@@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) {
// 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 &&
const firstCharacter = Array.from(emojiUnicode)[0];
return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
......
......@@ -35,7 +35,7 @@ export default class BlobFileDropzone {
this.removeFile(file);
});
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('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;
/* 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 {
BlobViewer.loadViewer(newViewer)
.then((viewer) => {
$(viewer).syntaxHighlight();
$(viewer).renderGFM();
this.$fileHolder.trigger('highlight:line');
gl.utils.handleLocationHash();
......
......@@ -70,6 +70,7 @@ $(() => {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
// Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
......@@ -87,6 +88,8 @@ $(() => {
if (list.type === 'closed') {
list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}
});
......@@ -127,7 +130,7 @@ $(() => {
},
computed: {
disabled() {
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
if (this.disabled) {
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
import './board_delete';
......@@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({
disabled: Boolean,
issueLinkBase: String,
rootPath: String,
boardId: {
type: String,
required: true,
},
},
data () {
return {
......@@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({
methods: {
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
toggleExpanded(e) {
if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
}
}
},
},
mounted () {
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
......@@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({
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;
}
},
});
......@@ -57,6 +57,9 @@ export default {
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
......@@ -108,6 +111,7 @@ export default {
},
created() {
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
},
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
......@@ -150,6 +154,7 @@ export default {
},
beforeDestroy() {
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);
},
template: `
......@@ -160,9 +165,11 @@ export default {
v-if="loading">
<loading-icon />
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<transition name="slide-down">
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
</transition>
<ul
class="board-list"
v-show="!loading"
......
......@@ -48,6 +48,7 @@ export default {
this.error = true;
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
},
cancel() {
......@@ -75,6 +76,7 @@ export default {
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'" />
<div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left"
......
......@@ -32,9 +32,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
showSidebar () {
return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
},
milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
......
......@@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
<div class="card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
v-if="shouldRenderAssignee(index)"
class="js-no-trigger"
:link-href="assigneeUrl(assignee)"
......
......@@ -13,6 +13,7 @@ export default {
FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
......
......@@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({
},
methods: {
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 issueIds = selectedIssues.map(issue => issue.globalId);
......
......@@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[0];
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
......
......@@ -2,7 +2,7 @@
import FilteredSearchContainer from '../filtered_search/container';
export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) {
constructor(store, updateUrl = false, cantEdit = []) {
super('boards');
this.store = store;
......@@ -11,6 +11,7 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
this.cantEdit = cantEdit;
}
updateObject(path) {
......@@ -40,4 +41,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Get the placeholder back if search is empty
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
}
}
......@@ -12,7 +12,9 @@ class List {
this.position = obj.position;
this.title = obj.title;
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.loading = true;
this.loadingMore = false;
......@@ -103,13 +105,19 @@ class List {
}
newIssue (issue) {
this.addIssue(issue);
this.addIssue(issue, null, 0);
this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
})
.then(() => {
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
}
});
}
......
......@@ -22,6 +22,7 @@ gl.issueBoards.BoardsStore = {
create () {
this.state.lists = [];
this.filter.path = gl.utils.getUrlParamsArray().join('&');
this.detail = { issue: {} };
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
......@@ -31,10 +32,14 @@ gl.issueBoards.BoardsStore = {
},
new (listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.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');
})
.catch(() => {
......@@ -47,7 +52,7 @@ gl.issueBoards.BoardsStore = {
},
shouldAddBlankState () {
// 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 () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
......@@ -100,7 +105,7 @@ gl.issueBoards.BoardsStore = {
issueTo.removeLabel(listFrom.label);
}
if (listTo.type === 'closed') {
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
......
This diff is collapsed.
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import VueResource from 'vue-resource';
import CommitPipelinesTable from './pipelines_table';
Vue.use(VueResource);
import commitPipelinesTable from './pipelines_table.vue';
/**
* Commits View > Pipelines Tab > Pipelines Table.
*
* Renders Pipelines table in pipelines tab in the commits show view.
* Used in:
* - Commit details View > Pipelines Tab > Pipelines Table.
* - Merge Request details View > Pipelines Tab > Pipelines Table.
* - New Merge Request View > Pipelines Tab > Pipelines Table.
*/
// export for use in merge_request_tabs.js (TODO: remove this hack)
const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load
// vue.js in merge_request_tabs.js)
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
$(() => {
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
document.addEventListener('DOMContentLoaded', () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
const table = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
}
});
<script>
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
export default {
props: {
endpoint: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
},
mixins: [
pipelinesMixin,
],
data() {
const store = new PipelineStore();
return {
store,
state: store.state,
};
},
computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
},
created() {
this.service = new PipelinesService(this.endpoint);
},
methods: {
successCallback(resp) {
const response = resp.json();
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = response.pipelines || response;
this.setCommonData(pipelines);
},
},
};
</script>
<template>
<div class="content-list pipelines">
<loading-icon
label="Loading pipelines"
size="3"
v-if="isLoading"
/>
<empty-state
v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath"
/>
<error-state
v-if="shouldRenderErrorState"
/>
<div
class="table-holder"
v-if="shouldRenderTable">
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
/>
</div>
</div>
</template>
......@@ -7,6 +7,8 @@ window.CommitsList = (function() {
CommitsList.timer = null;
CommitsList.init = function(limit) {
this.$contentList = $('.content_list');
$("body").on("click", ".day-commits-table li.commit", function(e) {
if (e.target.nodeName !== "A") {
location.href = $(this).attr("url");
......@@ -14,9 +16,9 @@ window.CommitsList = (function() {
return false;
}
});
Pager.init(limit, false, false, function() {
gl.utils.localTimeAgo($('.js-timeago'));
});
Pager.init(limit, false, false, this.processCommits);
this.content = $("#commits-list");
this.searchField = $("#commits-search");
this.lastSearch = this.searchField.val();
......@@ -62,5 +64,34 @@ window.CommitsList = (function() {
});
};
// Prepare loaded data.
CommitsList.processCommits = (data) => {
let processedData = data;
const $processedData = $(processedData);
const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last();
const lastShownDay = $commitsHeadersLast.data('day');
const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first();
const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day');
let commitsCount;
// If commits headers show the same date,
// remove the last header and change the previous one.
if (lastShownDay === loadedShownDayFirst) {
// Last shown commits count under the last commits header.
commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length;
// Remove duplicate of commits header.
processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`);
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
$commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
return processedData;
};
return CommitsList;
})();
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
......
......@@ -75,26 +75,32 @@
</script>
<template>
<div class="col-lg-9 col-lg-offset-3 append-bottom-default deploy-keys">
<div class="append-bottom-default deploy-keys">
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
label="Loading deploy keys"
/>
/>
<div v-else-if="hasKeys">
<keys-panel
title="Enabled deploy keys for this project"
:keys="keys.enabled_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
</div>
</div>
</template>
......@@ -11,6 +11,10 @@
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
components: {
actionBtn,
......@@ -19,6 +23,9 @@
timeagoDate() {
return gl.utils.getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
},
methods: {
isEnabled(id) {
......@@ -33,7 +40,8 @@
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon">
class="fa fa-key key-icon"
>
</i>
</div>
<div class="deploy-key-content key-list-item-info">
......@@ -45,7 +53,8 @@
</div>
<div
v-if="deployKey.can_push"
class="write-access-allowed">
class="write-access-allowed"
>
Write access allowed
</div>
</div>
......@@ -53,7 +62,8 @@
<a
v-for="project in deployKey.projects"
class="label deploy-project-label"
:href="project.full_path">
:href="project.full_path"
>
{{ project.full_name }}
</a>
</div>
......@@ -61,20 +71,30 @@
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-small"
:href="editDeployKeyPath"
>
Edit
</a>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"/>
type="enable"
/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove" />
type="remove"
/>
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable" />
type="disable"
/>
</div>
</div>
</template>
......@@ -20,6 +20,10 @@
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
components: {
key,
......@@ -34,18 +38,22 @@
({{ keys.length }})
</h5>
<ul class="well-list"
v-if="keys.length">
v-if="keys.length"
>
<li
v-for="deployKey in keys"
:key="deployKey.id">
<key
:deploy-key="deployKey"
:store="store" />
:store="store"
:endpoint="endpoint"
/>
</li>
</ul>
<div
class="settings-message text-center"
v-else-if="showHelpBox">
v-else-if="showHelpBox"
>
No deploy keys found. Create one with the form above.
</div>
</div>
......
......@@ -16,6 +16,13 @@ const JumpToDiscussion = Vue.extend({
};
},
computed: {
buttonText: function () {
if (this.discussionId) {
return 'Jump to next unresolved discussion';
} else {
return 'Jump to first unresolved discussion';
}
},
allResolved: function () {
return this.unresolvedDiscussionCount === 0;
},
......
......@@ -2,8 +2,7 @@
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global Build */
/* global Issuable */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
......@@ -55,6 +54,7 @@ import UsersSelect from './users_select';
import RefSelectDropdown from './ref_select_dropdown';
import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels';
(function() {
var Dispatcher;
......@@ -79,7 +79,18 @@ import ShortcutsBlob from './shortcuts_blob';
path = page.split(':');
shortcut_handler = null;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
$('.js-gfm-input').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
function initBlob() {
new LineHighlighter();
......@@ -118,18 +129,15 @@ import ShortcutsBlob from './shortcuts_blob';
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
case 'projects:builds:show':
new Build();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
......@@ -159,9 +167,6 @@ import ShortcutsBlob from './shortcuts_blob';
case 'admin:projects:index':
new ProjectsList();
break;
case 'dashboard:groups:index':
new GroupsList();
break;
case 'explore:groups:index':
new GroupsList();
......@@ -182,7 +187,7 @@ import ShortcutsBlob from './shortcuts_blob';
case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'));
new gl.GLForm($('.milestone-form'), true);
break;
case 'projects:compare:show':
new gl.Diff();
......@@ -194,7 +199,7 @@ import ShortcutsBlob from './shortcuts_blob';
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.issue-form'));
new gl.GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
......@@ -205,7 +210,7 @@ import ShortcutsBlob from './shortcuts_blob';
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.merge-request-form'));
new gl.GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
......@@ -214,12 +219,24 @@ import ShortcutsBlob from './shortcuts_blob';
break;
case 'projects:tags:new':
new ZenMode();
new gl.GLForm($('.tag-form'));
new gl.GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
new gl.GLForm($('.snippet-form'), true);
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new gl.GLForm($('.snippet-form'), false);
break;
case 'projects:releases:edit':
new ZenMode();
new gl.GLForm($('.release-form'));
new gl.GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
......@@ -321,25 +338,14 @@ import ShortcutsBlob from './shortcuts_blob';
shortcut_handler = new ShortcutsNavigation();
new TreeView();
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
shortcut_handler = true;
break;
case 'projects:blob:new':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:create':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blob:show':
new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
initBlob();
break;
case 'projects:blob:edit':
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:blame:show':
initBlob();
break;
......@@ -363,9 +369,11 @@ import ShortcutsBlob from './shortcuts_blob';
new ProjectFork();
break;
case 'projects:artifacts:browse':
new ShortcutsNavigation();
new BuildArtifacts();
break;
case 'projects:artifacts:file':
new ShortcutsNavigation();
new BlobViewer();
break;
case 'help:index':
......@@ -381,6 +389,8 @@ import ShortcutsBlob from './shortcuts_blob';
// Initialize Protected Tag Settings
new ProtectedTagCreate();
new ProtectedTagEditList();
// Initialize expandable settings panels
initSettingsPanels();
break;
case 'projects:ci_cd:show':
new gl.ProjectVariables();
......@@ -392,6 +402,9 @@ import ShortcutsBlob from './shortcuts_blob';
case 'users:show':
new UserCallout();
break;
case 'admin:conversational_development_index:show':
new UserCallout();
break;
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
......@@ -471,7 +484,7 @@ import ShortcutsBlob from './shortcuts_blob';
new gl.Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'));
new gl.GLForm($('.wiki-form'), true);
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
......
......@@ -8,7 +8,7 @@ const Keyboard = function () {
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider):not(.hidden)'), 0);
var listItems = [];
for(var i = 0; i < itemElements.length; i++) {
var listItem = itemElements[i];
......
......@@ -63,6 +63,9 @@ const AjaxFilter = {
return AjaxCache.retrieve(url)
.then((data) => {
this._loadData(data, config);
if (config.onLoadingFinished) {
config.onLoadingFinished(data);
}
})
.catch(config.onError);
},
......
......@@ -5,7 +5,7 @@ import './preview_markdown';
window.DropzoneInput = (function() {
function DropzoneInput(form) {
var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile;
var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile, addFileToForm;
Dropzone.autoDiscover = false;
divHover = '<div class="div-dropzone-hover"></div>';
iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
......@@ -71,6 +71,7 @@ window.DropzoneInput = (function() {
pasteText(response.link.markdown, shouldPad);
// Show 'Attach a file' link only when all files have been uploaded.
if (!processingFileCount) $attachButton.removeClass('hide');
addFileToForm(response.link.url);
},
error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
// If 'error' event is fired by dropzone, the second parameter is error message.
......@@ -194,9 +195,14 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
formTextarea.get(0).dispatchEvent(new Event('input'));
return formTextarea.trigger('input');
};
addFileToForm = function(path) {
$(form).append('<input type="hidden" name="files[]" value="' + _.escape(path) + '">');
};
getFilename = function(e) {
var value;
if (window.clipboardData && window.clipboardData.getData) {
......@@ -281,6 +287,10 @@ window.DropzoneInput = (function() {
$uploadingErrorMessage.html(message);
};
closeAlertMessage = function() {
return form.find('.div-dropzone-alert').alert('close');
};
form.find('.markdown-selector').click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......@@ -7,6 +8,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
import Poll from '../../lib/utils/poll';
import environmentsMixin from '../mixins/environments_mixin';
export default {
......@@ -16,6 +19,10 @@ export default {
loadingIcon,
},
mixins: [
environmentsMixin,
],
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
......@@ -35,6 +42,7 @@ export default {
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
isMakingRequest: false,
// Pagination Properties,
paginationInformation: {},
......@@ -65,17 +73,43 @@ export default {
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
this.fetchEnvironments();
const poll = new Poll({
resource: this.service,
method: 'get',
data: { scope, page },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
// We need to verify if any folder is open to also fecth it
this.openFolders = this.store.getOpenFolders();
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
......@@ -104,29 +138,13 @@ export default {
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
});
return this.service.get({ scope, page })
.then(this.successCallback)
.catch(this.errorCallback);
},
fetchChildEnvironments(folder, folderUrl) {
......@@ -146,9 +164,34 @@ export default {
},
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.'));
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.'));
}
},
successCallback(resp) {
this.saveData(resp);
// If folders are open while polling we need to open them again
if (this.openFolders.length) {
this.openFolders.map((folder) => {
// TODO - Move this to the backend
const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
this.store.updateFolder(folder, 'isOpen', true);
return this.fetchChildEnvironments(folder, folderUrl);
});
}
},
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
},
},
};
......@@ -187,7 +230,7 @@ export default {
</div>
</div>
<div class="content-list environments-container">
<div class="environments-container">
<loading-icon
label="Loading environments"
size="3"
......@@ -212,7 +255,7 @@ export default {
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
New environment
</a>
</div>
......
......@@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
/**
......@@ -403,6 +403,14 @@ export default {
return '';
},
displayEnvironmentActions() {
return this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.hasStopAction ||
this.canRetry;
},
/**
* Constructs folder URL based on the current location and the folder id.
*
......@@ -421,14 +429,21 @@ export default {
};
</script>
<template>
<tr :class="{ 'js-child-row': model.isChildren }">
<td>
<div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"
role="row">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header"
role="rowheader">
Environment
</div>
<a
v-if="!model.isFolder"
class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
{{model.name}}
<span class="flex-truncate-child">{{model.name}}</span>
</a>
<span
v-else
......@@ -461,9 +476,9 @@ export default {
{{model.size}}
</span>
</span>
</td>
</div>
<td class="deployment-column">
<div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
</span>
......@@ -478,21 +493,27 @@ export default {
:tooltip-text="deploymentUser.username"
/>
</span>
</td>
</div>
<td class="environments-build-cell">
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a
v-if="shouldRenderBuildName"
class="build-link"
:href="buildPath">
{{buildName}}
</a>
</td>
</div>
<td>
<div class="table-section section-25" role="gridcell">
<div
v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Commit
</div>
<div
v-if="!model.isFolder && hasLastDeploymentKey"
class="js-commit-component">
class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
......@@ -501,25 +522,34 @@ export default {
:title="commitTitle"
:author="commitAuthor"/>
</div>
<p
<div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
class="commit-title table-mobile-content">
No deployments yet
</p>
</td>
</div>
</div>
<td>
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
Updated
</div>
<span
v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago">
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
</td>
</div>
<div
v-if="!model.isFolder && displayEnvironmentActions"
class="table-section section-30 table-button-footer"
role="gridcell">
<td class="environments-actions">
<div
v-if="!model.isFolder"
class="btn-group pull-right"
class="btn-group table-action-buttons"
role="group">
<actions-component
......@@ -553,6 +583,6 @@ export default {
:retry-url="retryUrl"
/>
</div>
</td>
</tr>
</div>
</div>
</template>
......@@ -19,7 +19,7 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
......
......@@ -43,7 +43,7 @@ export default {
<template>
<button
type="button"
class="btn"
class="btn hidden-xs hidden-sm"
@click="onClick"
:disabled="isLoading">
......
......@@ -47,7 +47,7 @@ export default {
<template>
<button
type="button"
class="btn stop-env-link has-tooltip"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"
......
......@@ -29,7 +29,7 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip"
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"
......
......@@ -45,68 +45,59 @@ export default {
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="environments-name">
Environment
</th>
<th class="environments-deploy">
Last deployment
</th>
<th class="environments-build">
Job
</th>
<th class="environments-commit">
Commit
</th>
<th class="environments-date">
Updated
</th>
<th class="environments-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in environments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader">
Environment
</div>
<div class="table-section section-10 environments-deploy" role="columnheader">
Deployment
</div>
<div class="table-section section-15 environments-build" role="columnheader">
Job
</div>
<div class="table-section section-25 environments-commit" role="columnheader">
Commit
</div>
<div class="table-section section-10 environments-date" role="columnheader">
Updated
</div>
</div>
<template
v-for="model in environments"
v-bind:model="model">
<div
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6">
<loading-icon size="2" />
</td>
</tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<div v-if="isLoadingFolderContent">
<loading-icon size="2" />
</div>
<template v-else>
<tr
is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<template v-else>
<div
is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<tr>
<td
colspan="6"
class="text-center">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
<div>
<div class="text-center prepend-top-10">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</div>
</div>
</template>
</template>
</tbody>
</table>
</template>
</div>
</template>
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor';
export default {
components: {
......@@ -15,6 +18,10 @@ export default {
loadingIcon,
},
mixins: [
environmentsMixin,
],
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
......@@ -76,33 +83,39 @@ export default {
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.', 'alert');
});
const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'get',
data: { scope, page },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('postAction');
},
methods: {
......@@ -117,6 +130,37 @@ export default {
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get({ scope, page })
.then(this.successCallback)
.catch(this.errorCallback);
},
successCallback(resp) {
this.saveData(resp);
},
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
},
postAction(endpoint) {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.'));
}
},
},
};
</script>
......
export default {
methods: {
saveData(resp) {
const response = {
headers: resp.headers,
body: resp.json(),
};
this.isLoading = false;
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
},
},
};
......@@ -10,7 +10,8 @@ export default class EnvironmentsService {
this.folderResults = 3;
}
get(scope, page) {
get(options = {}) {
const { scope, page } = options;
return this.environments.get({ scope, page });
}
......
......@@ -153,4 +153,9 @@ export default class EnvironmentsStore {
return updatedEnvironments;
}
getOpenFolders() {
const environments = this.state.environments;
return environments.filter(env => env.isFolder && env.isOpen);
}
}
......@@ -8,39 +8,87 @@ export default class FilterableList {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
this.isBusy = false;
}
getFilterEndpoint() {
return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`;
}
getPagePath() {
return this.getFilterEndpoint();
}
initSearch() {
this.debounceFilter = _.debounce(this.filterResults.bind(this), 500);
// Wrap to prevent passing event arguments to .filterResults;
this.debounceFilter = _.debounce(this.onFilterInput.bind(this), 500);
this.listFilterElement.removeEventListener('input', this.debounceFilter);
this.unbindEvents();
this.bindEvents();
}
onFilterInput() {
const $form = $(this.filterForm);
const queryData = {};
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
}
this.filterResults(queryData);
if (this.setDefaultFilterOption) {
this.setDefaultFilterOption();
}
}
bindEvents() {
this.listFilterElement.addEventListener('input', this.debounceFilter);
}
filterResults() {
const form = this.filterForm;
const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`;
unbindEvents() {
this.listFilterElement.removeEventListener('input', this.debounceFilter);
}
filterResults(queryData) {
if (this.isBusy) {
return false;
}
$(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({
url: form.getAttribute('action'),
data: $(form).serialize(),
url: this.getFilterEndpoint(),
data: queryData,
type: 'GET',
dataType: 'json',
context: this,
complete() {
$(this.listHolderElement).fadeTo(250, 1);
complete: this.onFilterComplete,
beforeSend: () => {
this.isBusy = true;
},
success(data) {
this.listHolderElement.innerHTML = data.html;
// Change url so if user reload a page - search results are saved
return window.history.replaceState({
page: filterUrl,
}, document.title, filterUrl);
success: (response, textStatus, xhr) => {
this.onFilterSuccess(response, xhr, queryData);
},
});
}
onFilterSuccess(response, xhr, queryData) {
if (response.html) {
this.listHolderElement.innerHTML = response.html;
}
// Change url so if user reload a page - search results are saved
const currentPath = this.getPagePath(queryData);
return window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
}
onFilterComplete() {
this.isBusy = false;
$(this.listHolderElement).fadeTo(250, 1);
}
}
......@@ -56,7 +56,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
}
renderContent() {
const dropdownData = gl.FilteredSearchTokenKeys.get()
const dropdownData = this.tokenKeys.get()
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
......
......@@ -18,6 +18,9 @@ class DropdownUser extends gl.FilteredSearchDropdown {
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onLoadingFinished: () => {
this.hideCurrentUser();
},
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
......@@ -28,6 +31,11 @@ class DropdownUser extends gl.FilteredSearchDropdown {
this.tokenKeys = tokenKeys;
}
hideCurrentUser() {
const currenUserItem = this.dropdown.querySelector('.js-current-user');
currenUserItem.classList.add('hidden');
}
itemClicked(e) {
super.itemClicked(e,
selected => selected.querySelector('.dropdown-light-content').innerText.trim());
......
......@@ -102,10 +102,13 @@ class DropdownUtils {
if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
if (value && value.innerText) {
if (valueContainer && valueContainer.dataset.originalValue) {
valueText = valueContainer.dataset.originalValue;
} else if (value && value.innerText) {
valueText = value.innerText;
}
......
......@@ -2,9 +2,9 @@ import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_token_keys';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
import './filtered_search_token_keys';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';
import AjaxCache from '~/lib/utils/ajax_cache';
import '~/flash'; /* global Flash */
import AjaxCache from '../lib/utils/ajax_cache';
import '../flash'; /* global Flash */
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
......@@ -36,15 +37,22 @@ class FilteredSearchVisualTokens {
}
}
static createVisualTokenElementHTML() {
static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return `
<div class="selectable" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
${removeTokenMarkup}
</div>
</div>
`;
......@@ -75,22 +83,52 @@ class FilteredSearchVisualTokens {
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
if (tokenValue === 'none') {
return Promise.resolve();
}
const username = tokenValue.replace(/^@/, '');
return UsersCache.retrieve(username)
.then((user) => {
if (!user) {
return;
}
/* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
${user.name}
`;
/* eslint-enable no-param-reassign */
})
// ignore error and leave username in the search bar
.catch(() => { });
}
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
tokenValueContainer.querySelector('.value').innerText = tokenValue;
const tokenValueElement = tokenValueContainer.querySelector('.value');
tokenValueElement.innerText = tokenValue;
if (tokenName.toLowerCase() === 'label') {
const tokenType = tokenName.toLowerCase();
if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
} else if ((tokenType === 'author') || (tokenType === 'assignee')) {
FilteredSearchVisualTokens.updateUserTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue,
);
}
}
static addVisualTokenElement(name, value, isSearchTerm) {
static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else {
li.innerHTML = '<div class="name"></div>';
......@@ -114,20 +152,20 @@ class FilteredSearchVisualTokens {
}
}
static addFilterVisualToken(tokenName, tokenValue) {
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false);
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value, false);
addVisualTokenElement(previousTokenName, value, false, canEdit);
}
}
......@@ -146,6 +184,12 @@ class FilteredSearchVisualTokens {
if (!lastVisualToken) return '';
const valueContainer = lastVisualToken.querySelector('.value-container');
const originalValue = valueContainer && valueContainer.dataset.originalValue;
if (originalValue) {
return originalValue;
}
const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name');
......@@ -198,17 +242,28 @@ class FilteredSearchVisualTokens {
const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token);
const name = token.querySelector('.name');
const value = token.querySelector('.value');
const nameElement = token.querySelector('.name');
let value;
if (token.classList.contains('filtered-search-token') && value) {
FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
input.value = value.innerText;
} else {
// token is a search term
input.value = name.innerText;
if (token.classList.contains('filtered-search-token')) {
FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
const valueContainerElement = token.querySelector('.value-container');
value = valueContainerElement.dataset.originalValue;
if (!value) {
const valueElement = valueContainerElement.querySelector('.value');
value = valueElement.innerText;
}
}
// token is a search term
if (!value) {
value = nameElement.innerText;
}
input.value = value;
// Opens dropdown
const inputEvent = new Event('input');
input.dispatchEvent(inputEvent);
......
......@@ -7,8 +7,21 @@ window.Flash = (function() {
return $(this).fadeOut();
};
function Flash(message, type, parent) {
var flash, textDiv;
/**
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
*
* @param {String} message Flash message
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {Object} parent Reference to Parent element under which Flash needs to appear
* @param {Object} actionConfig Map of config to show action on banner
* @param {String} href URL to which action link should point (default '#')
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
*/
function Flash(message, type, parent, actionConfig) {
var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
......@@ -30,6 +43,23 @@ window.Flash = (function() {
text: message
});
textDiv.appendTo(flash);
if (actionConfig) {
const actionLinkConfig = {
class: 'flash-action',
href: actionConfig.href || '#',
text: actionConfig.title
};
if (!actionConfig.href) {
actionLinkConfig.role = 'button';
}
actionLink = $('<a/>', actionLinkConfig);
actionLink.appendTo(flash);
this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
}
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
}
......
......@@ -2,6 +2,7 @@ import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
......@@ -33,8 +34,9 @@ class GfmAutoComplete {
const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
// This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~)
// Needed for quick actions with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
$input.on('clear-commands-cache.atwho', () => this.clearCache());
});
}
......@@ -46,8 +48,8 @@ class GfmAutoComplete {
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
if (this.enableMap.labels) this.setupLabels($input);
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-slash-commands="true"]').atwho({
// We don't instantiate the quick actions autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-quick-actions="true"]').atwho({
at: '/',
alias: 'commands',
searchKey: 'search',
......@@ -375,11 +377,14 @@ class GfmAutoComplete {
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
} else {
$.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => {
this.loadData($input, at, data);
}).fail(() => { this.isLoadingData[at] = false; });
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
}
}
loadData($input, at, data) {
this.isLoadingData[at] = false;
this.cachedData[at] = data;
......@@ -389,6 +394,10 @@ class GfmAutoComplete {
return $input.trigger('keyup');
}
clearCache() {
this.cachedData = {};
}
static isLoading(data) {
let dataToInspect = data;
if (data && data.length > 0) {
......
......@@ -248,7 +248,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
_this.focusTextInput(true);
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
......@@ -468,8 +468,8 @@ GitLabDropdown = (function() {
// Process the data to make sure rendered data
// matches the correct layout
if (this.fullData && hasMultiSelect && this.options.processData) {
const inputValue = this.filterInput.val();
const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
}
......@@ -728,8 +728,20 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) { this.filterInput.focus(); }
GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
if (this.options.filterable) {
$(':focus').blur();
this.dropdown.one('transitionend', () => {
this.filterInput.focus();
});
if (triggerFocus) {
// This triggers after a ajax request
// in case of slow requests, the dropdown transition could already be finished
this.dropdown.trigger('transitionend');
}
}
};
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
......@@ -740,6 +752,12 @@ GitLabDropdown = (function() {
$input.attr('id', this.options.inputId);
}
if (this.options.multiSelect) {
Object.keys(selectedObject).forEach((attribute) => {
$input.attr(`data-${attribute}`, selectedObject[attribute]);
});
}
if (this.options.inputMeta) {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
......
......@@ -31,9 +31,13 @@ class GlFieldErrors {
* and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) {
if (!event.currentTarget.checkValidity()) {
event.preventDefault();
event.stopPropagation();
const $form = $(event.currentTarget);
if (!$form.attr('novalidate')) {
if (!event.currentTarget.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
}
}
......
<script>
export default {
props: {
groups: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
},
};
</script>
<template>
<ul class="content-list group-list-tree">
<group-item
v-for="(group, index) in groups"
:key="index"
:group="group"
:base-group="baseGroup"
:collection="groups"
/>
</ul>
</template>
<script>
import eventHub from '../event_hub';
export default {
props: {
group: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
collection: {
type: Object,
required: false,
default: () => ({}),
},
},
methods: {
onClickRowGroup(e) {
e.stopPropagation();
// Skip for buttons
if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
if (this.group.hasSubgroups) {
eventHub.$emit('toggleSubGroups', this.group);
} else {
window.location.href = this.group.groupPath;
}
}
},
onLeaveGroup(e) {
e.preventDefault();
// eslint-disable-next-line no-alert
if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
this.leaveGroup();
}
},
leaveGroup() {
eventHub.$emit('leaveGroup', this.group, this.collection);
},
},
computed: {
groupDomId() {
return `group-${this.group.id}`;
},
rowClass() {
return {
'group-row': true,
'is-open': this.group.isOpen,
'has-subgroups': this.group.hasSubgroups,
'no-description': !this.group.description,
};
},
visibilityIcon() {
return {
fa: true,
'fa-globe': this.group.visibility === 'public',
'fa-shield': this.group.visibility === 'internal',
'fa-lock': this.group.visibility === 'private',
};
},
fullPath() {
let fullPath = '';
if (this.group.isOrphan) {
// check if current group is baseGroup
if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
// Remove baseGroup prefix from our current group.fullName. e.g:
// baseGroup.fullName: `level1`
// group.fullName: `level1 / level2 / level3`
// Result: `level2 / level3`
const gfn = this.group.fullName;
const bfn = this.baseGroup.fullName;
const length = bfn.length;
const start = gfn.indexOf(bfn);
const extraPrefixChars = 3;
fullPath = gfn.substr(start + length + extraPrefixChars);
} else {
fullPath = this.group.fullName;
}
} else {
fullPath = this.group.name;
}
return fullPath;
},
hasGroups() {
return Object.keys(this.group.subGroups).length > 0;
},
},
};
</script>
<template>
<li
@click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
>
<div
class="group-row-contents">
<div
class="controls">
<a
v-if="group.canEdit"
class="edit-group btn"
:href="group.editPath">
<i
class="fa fa-cogs"
aria-hidden="true"
>
</i>
</a>
<a
@click="onLeaveGroup"
:href="group.leavePath"
class="leave-group btn"
title="Leave this group">
<i
class="fa fa-sign-out"
aria-hidden="true"
>
</i>
</a>
</div>
<div
class="stats">
<span
class="number-projects">
<i
class="fa fa-bookmark"
aria-hidden="true"
>
</i>
{{group.numberProjects}}
</span>
<span
class="number-users">
<i
class="fa fa-users"
aria-hidden="true"
>
</i>
{{group.numberUsers}}
</span>
<span
class="group-visibility">
<i
:class="visibilityIcon"
aria-hidden="true"
>
</i>
</span>
</div>
<div
class="folder-toggle-wrap">
<span
class="folder-caret"
v-if="group.hasSubgroups">
<i
v-if="group.isOpen"
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-caret-right"
aria-hidden="true"
>
</i>
</span>
<span class="folder-icon">
<i
v-if="group.isOpen"
class="fa fa-folder-open"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-folder"
aria-hidden="true">
</i>
</span>
</div>
<div
class="avatar-container s40 hidden-xs">
<a
:href="group.groupPath">
<img
class="avatar s40"
:src="group.avatarUrl"
/>
</a>
</div>
<div
class="title">
<a
:href="group.groupPath">{{fullPath}}</a>
<template v-if="group.permissions.humanGroupAccess">
as
<span class="access-type">{{group.permissions.humanGroupAccess}}</span>
</template>
</div>
<div
class="description">{{group.description}}</div>
</div>
<group-folder
v-if="group.isOpen && hasGroups"
:groups="group.subGroups"
:baseGroup="group"
/>
</li>
</template>
<script>
import tablePagination from '~/vue_shared/components/table_pagination.vue';
import eventHub from '../event_hub';
export default {
props: {
groups: {
type: Object,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
},
components: {
tablePagination,
},
methods: {
change(page) {
const filterGroupsParam = gl.utils.getParameterByName('filter_groups');
const sortParam = gl.utils.getParameterByName('sort');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
},
},
};
</script>
<template>
<div class="groups-list-tree-container">
<group-folder
:groups="groups"
/>
<table-pagination
:change="change"
:pageInfo="pageInfo"
/>
</div>
</template>
import Vue from 'vue';
export default new Vue();
import FilterableList from '~/filterable_list';
import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
super(form, filter, holder);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
this.$dropdown = $('.js-group-filter-dropdown-wrap');
}
getFilterEndpoint() {
return this.filterEndpoint;
}
getPagePath(queryData) {
const params = queryData ? $.param(queryData) : '';
const queryString = params ? `?${params}` : '';
return `${this.pagePath}${queryString}`;
}
bindEvents() {
super.bindEvents();
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
}
onFormSubmit(e) {
e.preventDefault();
const $form = $(this.form);
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
const queryData = {};
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
}
this.filterResults(queryData);
this.setDefaultFilterOption();
}
setDefaultFilterOption() {
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
this.$dropdown.find('.dropdown-label').text(defaultOption);
}
onOptionClick(e) {
e.preventDefault();
const queryData = {};
const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href);
if (sortParam) {
queryData.sort = sortParam;
}
this.filterResults(queryData);
// Active selected option
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
// Clear current value on search form
this.form.querySelector('[name="filter_groups"]').value = '';
}
onFilterSuccess(data, xhr, queryData) {
super.onFilterSuccess(data, xhr, queryData);
const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
'X-Page': xhr.getResponseHeader('X-Page'),
'X-Total': xhr.getResponseHeader('X-Total'),
'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'),
'X-Next-Page': xhr.getResponseHeader('X-Next-Page'),
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
};
eventHub.$emit('updateGroups', data);
eventHub.$emit('updatePagination', paginationData);
}
}
/* global Flash */
import Vue from 'vue';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
import GroupItem from './components/group_item.vue';
import GroupsStore from './stores/groups_store';
import GroupsService from './services/groups_service';
import eventHub from './event_hub';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('dashboard-group-app');
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
if (!el) {
return;
}
Vue.component('groups-component', GroupsComponent);
Vue.component('group-folder', GroupFolder);
Vue.component('group-item', GroupItem);
// eslint-disable-next-line no-new
new Vue({
el,
data() {
this.store = new GroupsStore();
this.service = new GroupsService(el.dataset.endpoint);
return {
store: this.store,
isLoading: true,
state: this.store.state,
loading: true,
};
},
computed: {
isEmpty() {
return Object.keys(this.state.groups).length === 0;
},
},
methods: {
fetchGroups(parentGroup) {
let parentId = null;
let getGroups = null;
let page = null;
let sort = null;
let pageParam = null;
let sortParam = null;
let filterGroups = null;
let filterGroupsParam = null;
if (parentGroup) {
parentId = parentGroup.id;
} else {
this.isLoading = true;
}
pageParam = gl.utils.getParameterByName('page');
if (pageParam) {
page = pageParam;
}
filterGroupsParam = gl.utils.getParameterByName('filter_groups');
if (filterGroupsParam) {
filterGroups = filterGroupsParam;
}
sortParam = gl.utils.getParameterByName('sort');
if (sortParam) {
sort = sortParam;
}
getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
getGroups
.then(response => response.json())
.then((response) => {
this.isLoading = false;
this.updateGroups(response, parentGroup);
})
.catch(this.handleErrorResponse);
return getGroups;
},
fetchPage(page, filterGroups, sort) {
this.isLoading = true;
return this.service
.getGroups(null, page, filterGroups, sort)
.then((response) => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
this.updateGroups(response.json());
this.updatePagination(response.headers);
})
.catch(this.handleErrorResponse);
},
toggleSubGroups(parentGroup = null) {
if (!parentGroup.isOpen) {
this.store.resetGroups(parentGroup);
this.fetchGroups(parentGroup);
}
this.store.toggleSubGroups(parentGroup);
},
leaveGroup(group, collection) {
this.service.leaveGroup(group.leavePath)
.then((response) => {
$.scrollTo(0);
this.store.removeGroup(group, collection);
// eslint-disable-next-line no-new
new Flash(response.json().notice, 'notice');
})
.catch((response) => {
let message = 'An error occurred. Please try again.';
if (response.status === 403) {
message = 'Failed to leave the group. Please make sure you are not the only owner';
}
// eslint-disable-next-line no-new
new Flash(message);
});
},
updateGroups(groups, parentGroup) {
this.store.setGroups(groups, parentGroup);
},
updatePagination(headers) {
this.store.storePagination(headers);
},
handleErrorResponse() {
this.isLoading = false;
$.scrollTo(0);
// eslint-disable-next-line no-new
new Flash('An error occurred. Please try again.');
},
},
created() {
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleSubGroups', this.toggleSubGroups);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updateGroups', this.updateGroups);
eventHub.$on('updatePagination', this.updatePagination);
},
beforeMount() {
let groupFilterList = null;
const form = document.querySelector('form#group-filter-form');
const filter = document.querySelector('.js-groups-list-filter');
const holder = document.querySelector('.js-groups-list-holder');
const opts = {
form,
filter,
holder,
filterEndpoint: el.dataset.endpoint,
pagePath: el.dataset.path,
};
groupFilterList = new GroupFilterableList(opts);
groupFilterList.initSearch();
},
mounted() {
this.fetchGroups()
.then((response) => {
this.updatePagination(response.headers);
this.isLoading = false;
})
.catch(this.handleErrorResponse);
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleSubGroups', this.toggleSubGroups);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updateGroups', this.updateGroups);
eventHub.$off('updatePagination', this.updatePagination);
},
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class GroupsService {
constructor(endpoint) {
this.groups = Vue.resource(endpoint);
}
getGroups(parentId, page, filterGroups, sort) {
const data = {};
if (parentId) {
data.parent_id = parentId;
} else {
// Do not send the following param for sub groups
if (page) {
data.page = page;
}
if (filterGroups) {
data.filter_groups = filterGroups;
}
if (sort) {
data.sort = sort;
}
}
return this.groups.get(data);
}
// eslint-disable-next-line class-methods-use-this
leaveGroup(endpoint) {
return Vue.http.delete(endpoint);
}
}
import Vue from 'vue';
export default class GroupsStore {
constructor() {
this.state = {};
this.state.groups = {};
this.state.pageInfo = {};
}
setGroups(rawGroups, parent) {
const parentGroup = parent;
const tree = this.buildTree(rawGroups, parentGroup);
if (parentGroup) {
parentGroup.subGroups = tree;
} else {
this.state.groups = tree;
}
return tree;
}
// eslint-disable-next-line class-methods-use-this
resetGroups(parent) {
const parentGroup = parent;
parentGroup.subGroups = {};
}
storePagination(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
}
buildTree(rawGroups, parentGroup) {
const groups = this.decorateGroups(rawGroups);
const tree = {};
const mappedGroups = {};
const orphans = [];
// Map groups to an object
groups.map((group) => {
mappedGroups[group.id] = group;
mappedGroups[group.id].subGroups = {};
return group;
});
Object.keys(mappedGroups).map((key) => {
const currentGroup = mappedGroups[key];
if (currentGroup.parentId) {
// If the group is not at the root level, add it to its parent array of subGroups.
const findParentGroup = mappedGroups[currentGroup.parentId];
if (findParentGroup) {
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[currentGroup.id] = currentGroup;
} else {
// Means the groups hast no direct parent.
// Save for later processing, we will add them to its corresponding base group
orphans.push(currentGroup);
}
} else {
// If the group is at the root level, add it to first level elements array.
tree[currentGroup.id] = currentGroup;
}
return key;
});
// Hopefully this array will be empty for most cases
if (orphans.length) {
orphans.map((orphan) => {
let found = false;
const currentOrphan = orphan;
Object.keys(tree).map((key) => {
const group = tree[key];
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
group.subGroups[currentOrphan.id] = currentOrphan;
group.isOpen = true;
currentOrphan.isOrphan = true;
found = true;
}
return key;
});
if (!found) {
currentOrphan.isOrphan = true;
tree[currentOrphan.id] = currentOrphan;
}
return orphan;
});
}
return tree;
}
decorateGroups(rawGroups) {
this.groups = rawGroups.map(this.decorateGroup);
return this.groups;
}
// eslint-disable-next-line class-methods-use-this
decorateGroup(rawGroup) {
return {
id: rawGroup.id,
fullName: rawGroup.full_name,
fullPath: rawGroup.full_path,
avatarUrl: rawGroup.avatar_url,
name: rawGroup.name,
hasSubgroups: rawGroup.has_subgroups,
canEdit: rawGroup.can_edit,
description: rawGroup.description,
webUrl: rawGroup.web_url,
groupPath: rawGroup.group_path,
parentId: rawGroup.parent_id,
visibility: rawGroup.visibility,
leavePath: rawGroup.leave_path,
editPath: rawGroup.edit_path,
isOpen: false,
isOrphan: false,
numberProjects: rawGroup.number_projects_with_delimiter,
numberUsers: rawGroup.number_users_with_delimiter,
permissions: {
humanGroupAccess: rawGroup.permissions.human_group_access,
},
subGroups: {},
};
}
// eslint-disable-next-line class-methods-use-this
removeGroup(group, collection) {
Vue.delete(collection, group.id);
}
// eslint-disable-next-line class-methods-use-this
toggleSubGroups(toggleGroup) {
const group = toggleGroup;
group.isOpen = !group.isOpen;
return group;
}
}
......@@ -56,6 +56,8 @@
if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>');
} else if (job.import_status === 'scheduled') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
} else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else {
......
/* eslint-disable no-new */
import IntegrationSettingsForm from './integration_settings_form';
$(() => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
});
/* global Flash */
export default class IntegrationSettingsForm {
constructor(formSelector) {
this.$form = $(formSelector);
// Form Metadata
this.canTestService = this.$form.data('can-test');
this.testEndPoint = this.$form.data('test-url');
// Form Child Elements
this.$serviceToggle = this.$form.find('#service_active');
this.$submitBtn = this.$form.find('button[type="submit"]');
this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
}
init() {
// Initialize View
this.toggleServiceState(this.$serviceToggle.is(':checked'));
// Bind Event Listeners
this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
this.$submitBtn.on('click', e => this.handleSettingsSave(e));
}
handleSettingsSave(e) {
// Check if Service is marked active, as if not marked active,
// We can skip testing it and directly go ahead to allow form to
// be submitted
if (!this.$serviceToggle.is(':checked')) {
return;
}
// Service was marked active so now we check;
// 1) If form contents are valid
// 2) If this service can be tested
// If both conditions are true, we override form submission
// and test the service using provided configuration.
if (this.$form.get(0).checkValidity() && this.canTestService) {
e.preventDefault();
this.testSettings(this.$form.serialize());
}
}
handleServiceToggle(e) {
this.toggleServiceState($(e.currentTarget).is(':checked'));
}
/**
* Change Form's validation enforcement based on service status (active/inactive)
*/
toggleServiceState(serviceActive) {
this.toggleSubmitBtnLabel(serviceActive);
if (serviceActive) {
this.$form.removeAttr('novalidate');
} else if (!this.$form.attr('novalidate')) {
this.$form.attr('novalidate', 'novalidate');
}
}
/**
* Toggle Submit button label based on Integration status and ability to test service
*/
toggleSubmitBtnLabel(serviceActive) {
let btnLabel = 'Save changes';
if (serviceActive && this.canTestService) {
btnLabel = 'Test settings and save changes';
}
this.$submitBtnLabel.text(btnLabel);
}
/**
* Toggle Submit button state based on provided boolean value of `saveTestActive`
* When enabled, it does two things, and reverts back when disabled
*
* 1. It shows load spinner on submit button
* 2. Makes submit button disabled
*/
toggleSubmitBtnState(saveTestActive) {
if (saveTestActive) {
this.$submitBtn.disable();
this.$submitBtnLoader.removeClass('hidden');
} else {
this.$submitBtn.enable();
this.$submitBtnLoader.addClass('hidden');
}
}
/* eslint-disable promise/catch-or-return, no-new */
/**
* Test Integration config
*/
testSettings(formData) {
this.toggleSubmitBtnState(true);
$.ajax({
type: 'PUT',
url: this.testEndPoint,
data: formData,
})
.done((res) => {
if (res.error) {
new Flash(res.message, null, null, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
this.$form.submit();
},
});
} else {
this.$form.submit();
}
})
.fail(() => {
new Flash('Something went wrong on our end.');
})
.always(() => {
this.toggleSubmitBtnState(false);
});
}
}
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
/* global Flash */
export default {
init({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.willUpdateLabels = false;
this.bindEvents();
},
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
},
onFormSubmit(e) {
e.preventDefault();
return this.submit();
},
submit() {
const _this = this;
const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => this.onFormSubmitFailure());
},
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
return new Flash("Issue update failed");
},
getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
},
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
});
}
});
return labels;
},
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
*/
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
});
return result;
},
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
*/
getFormDataAsObject() {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
}
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
},
setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select');
$labelSelect.data('common', this.getOriginalCommonIds());
$labelSelect.data('marked', this.getOriginalMarkedIds());
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
},
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalIndeterminateIds() {
const uniqueIds = [];
const labelIds = [];
let issuableLabels = [];
// Collect unique label IDs for all checked issues
this.getElement('.selected_issue:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);
}
});
// Store array of IDs per issuable
labelIds.push(issuableLabels);
});
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
// Return IDs that are present but not in all selected issueables
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
},
getElement(selector) {
this.scopeEl = this.scopeEl || $('.content');
return this.scopeEl.find(selector);
},
};
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset class="checkbox">
<label for="issue-confidential">
<input
type="checkbox"
value="1"
id="issue-confidential"
v-model="formState.confidential" />
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</template>
This diff is collapsed.
This diff is collapsed.
<script>
export default {
computed: {
currentPath() {
return location.pathname;
},
},
};
</script>
<template>
<div class="alert alert-danger">
Someone edited the issue at the same time you did. Please check out
<a
:href="currentPath"
target="_blank"
rel="nofollow">the issue</a>
and make sure your changes will not unintentionally remove theirs.
</div>
</template>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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